Compare commits

...

2 Commits

10 changed files with 199 additions and 35 deletions

View File

@@ -1,4 +1,4 @@
use std::convert::Infallible; use std::{convert::Infallible, sync::Arc};
use pyo3::{ use pyo3::{
types::PyAnyMethods as _, Borrowed, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, Python, types::PyAnyMethods as _, Borrowed, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, Python,
@@ -33,22 +33,26 @@ impl<'py> IntoPyObject<'py> for &HomeAssistant {
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Clone, Snafu)]
pub enum GetStatesError { pub enum GetStatesError {
/// couldn't get the `states` attribute on the Home Assistant object /// couldn't get the `states` attribute on the Home Assistant object
GetStatesAttributeError { source: PyErr }, GetStatesAttributeError { source: Arc<PyErr> },
/// couldn't extract the `states` as a [`StateMachine`] /// couldn't extract the `states` as a [`StateMachine`]
ExtractStateMachineError { source: TypeByNameValidationError }, ExtractStateMachineError {
source: Arc<TypeByNameValidationError>,
},
} }
#[derive(Debug, Snafu)] #[derive(Debug, Clone, Snafu)]
pub enum GetServicesError { pub enum GetServicesError {
/// couldn't get the `services` attribute on the Home Assistant object /// couldn't get the `services` attribute on the Home Assistant object
GetServicesAttributeError { source: PyErr }, GetServicesAttributeError { source: Arc<PyErr> },
/// couldn't extract the `states` as a [`ServiceRegistry`] /// couldn't extract the `states` as a [`ServiceRegistry`]
ExtractServiceRegistryError { source: TypeByNameValidationError }, ExtractServiceRegistryError {
source: Arc<TypeByNameValidationError>,
},
} }
impl HomeAssistant { impl HomeAssistant {
@@ -74,15 +78,23 @@ impl HomeAssistant {
let states = self let states = self
.0 .0
.getattr(py, "states") .getattr(py, "states")
.map_err(Arc::new)
.context(GetStatesAttributeSnafu)?; .context(GetStatesAttributeSnafu)?;
states.extract(py).context(ExtractStateMachineSnafu) states
.extract(py)
.map_err(Arc::new)
.context(ExtractStateMachineSnafu)
} }
pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, GetServicesError> { pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, GetServicesError> {
let services = self let services = self
.0 .0
.getattr(py, "services") .getattr(py, "services")
.map_err(Arc::new)
.context(GetServicesAttributeSnafu)?; .context(GetServicesAttributeSnafu)?;
services.extract(py).context(ExtractServiceRegistrySnafu) services
.extract(py)
.map_err(Arc::new)
.context(ExtractServiceRegistrySnafu)
} }
} }

View File

@@ -0,0 +1,16 @@
use pyo3::FromPyObject;
use super::InputNumberMode;
#[derive(Debug, FromPyObject)]
#[pyo3(from_item_all)]
pub struct InputNumberAttributes {
initial: Option<f64>,
editable: bool,
min: f64,
max: f64,
step: f64,
mode: InputNumberMode,
// todo: CustomUnitOfMeasurement type? probably not?
unit_of_measurement: Option<String>,
}

View File

@@ -1 +1,73 @@
use std::{future::Future, sync::Arc};
use emitter_and_signal::{Signal, SignalExt};
use pyo3::{Py, PyAny, PyErr, Python};
use snafu::{ResultExt, Snafu};
use crate::{
domain::Domain,
entity_id::EntityId,
home_assistant::HomeAssistant,
object_id::ObjectId,
state::HomeAssistantState,
state_object::{self, StateObject, StateObjectSignalError},
};
mod attributes;
mod mode;
pub use attributes::InputNumberAttributes;
pub use mode::InputNumberMode;
#[derive(Debug, Snafu)]
pub enum CreateSignalError {
/// couldn't get the underlying state object signal
StateObjectSignalError {
source: state_object::CreateSignalError,
},
/// couldn't map the state object to a power value
MappedSignalError {
source: emitter_and_signal::signal_ext::ProducerAlreadyExited,
},
}
pub fn signal<'py>(
py: Python<'py>,
home_assistant: &'py HomeAssistant,
object_id: ObjectId,
) -> Result<
(
Signal<Option<Arc<Result<HomeAssistantState<f64>, StateObjectSignalError<Arc<PyErr>>>>>>,
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
),
CreateSignalError,
> {
let entity_id = EntityId(Domain::InputNumber, object_id);
let (signal, task1) =
StateObject::<HomeAssistantState<f64>, InputNumberAttributes, Py<PyAny>>::signal(
py,
home_assistant,
entity_id,
)
.context(StateObjectSignalSnafu)?;
let (signal, task2) = signal
.map(|state_object_arc_result_option| {
state_object_arc_result_option.map(|state_object_arc_result| {
Arc::new(
(&*state_object_arc_result)
.as_ref()
.map(|state_object| state_object.state.clone())
.map_err(|e| e.clone()),
)
})
})
.context(MappedSignalSnafu)?;
Ok((
signal,
async move { tokio::try_join!(task1, task2).map(|_| ()) },
))
}

View File

@@ -0,0 +1,10 @@
use python_utils::{FromPyFromStr, ToStrToPy};
use strum::EnumString;
#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
#[strum(serialize_all = "snake_case")]
pub enum InputNumberMode {
Box,
#[default]
Slider,
}

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use attributes::LightAttributes; use attributes::LightAttributes;
use pyo3::{FromPyObject, Py, PyAny, Python}; use pyo3::{FromPyObject, Py, PyAny, Python};
use snafu::{ResultExt, Snafu}; use snafu::{ResultExt, Snafu};
@@ -29,15 +31,15 @@ impl HomeAssistantLight {
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Clone, Snafu)]
pub enum GetStateObjectError { pub enum GetStateObjectError {
/// couldn't get the state machine registry /// couldn't get the state machine registry
GetStatesError { source: GetStatesError }, GetStatesError { source: GetStatesError },
/// this state object exists in the state machine registry, but it couldn't be extracted as a light state object /// this state object exists in the state machine registry, but it couldn't be extracted as a light state object
GetStateError { source: GetStateError< GetStateError { source: Arc<GetStateError<
<StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>> as FromPyObject<'static, 'static>>::Error <StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>> as FromPyObject<'static, 'static>>::Error
> }, >> },
/// this entity does not have a state object in the registry /// this entity does not have a state object in the registry
EntityMissing, EntityMissing,
@@ -55,6 +57,7 @@ impl HomeAssistantLight {
let entity_id = self.entity_id(); let entity_id = self.entity_id();
let state_object = states let state_object = states
.get(py, entity_id) .get(py, entity_id)
.map_err(Arc::new)
.context(GetStateSnafu)? .context(GetStateSnafu)?
.ok_or(GetStateObjectError::EntityMissing)?; .ok_or(GetStateObjectError::EntityMissing)?;

View File

@@ -30,9 +30,7 @@ impl GetState for HomeAssistantLight {
HomeAssistantState::Err(error_state) => { HomeAssistantState::Err(error_state) => {
Err(GetStateError::Error { state: error_state }) Err(GetStateError::Error { state: error_state })
} }
HomeAssistantState::UnexpectedErr(state) => { HomeAssistantState::Unexpected(state) => Err(GetStateError::UnexpectedError { state }),
Err(GetStateError::UnexpectedError { state })
}
} }
} }
} }

View File

@@ -2,7 +2,7 @@ use std::{future::Future, sync::Arc};
use emitter_and_signal::{Signal, SignalExt}; use emitter_and_signal::{Signal, SignalExt};
use pyo3::{FromPyObject, Py, PyAny, PyErr, Python}; use pyo3::{FromPyObject, Py, PyAny, PyErr, Python};
use python_utils::{FromPyFromStr, FromPyObjectViaParse, ToStrToPy}; use python_utils::{FromPyFromStr, ToStrToPy};
use snafu::{ResultExt, Snafu}; use snafu::{ResultExt, Snafu};
use string_literal::StringLiteral; use string_literal::StringLiteral;
@@ -12,6 +12,7 @@ use crate::{
entity_id::EntityId, entity_id::EntityId,
home_assistant::HomeAssistant, home_assistant::HomeAssistant,
object_id::ObjectId, object_id::ObjectId,
state::HomeAssistantState,
state_object::{self, StateObject, StateObjectSignalError}, state_object::{self, StateObject, StateObjectSignalError},
unit_of_measurement::power::UnitOfMeasurement, unit_of_measurement::power::UnitOfMeasurement,
}; };
@@ -47,7 +48,11 @@ pub fn signal<'py>(
object_id: ObjectId, object_id: ObjectId,
) -> Result< ) -> Result<
( (
Signal<Option<Arc<Result<uom::si::f64::Power, StateObjectSignalError<PyErr>>>>>, Signal<
Option<
Result<HomeAssistantState<uom::si::f64::Power>, StateObjectSignalError<Arc<PyErr>>>,
>,
>,
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>, impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
), ),
CreateSignalError, CreateSignalError,
@@ -55,7 +60,7 @@ pub fn signal<'py>(
let entity_id = EntityId(Domain::Sensor, object_id); let entity_id = EntityId(Domain::Sensor, object_id);
let (signal, task1) = let (signal, task1) =
StateObject::<FromPyObjectViaParse<f64>, PowerSensorAttributes, Py<PyAny>>::signal( StateObject::<HomeAssistantState<f64>, PowerSensorAttributes, Py<PyAny>>::signal(
py, py,
home_assistant, home_assistant,
entity_id, entity_id,
@@ -65,18 +70,17 @@ pub fn signal<'py>(
let (signal, task2) = signal let (signal, task2) = signal
.map(|state_object_result_option| { .map(|state_object_result_option| {
state_object_result_option.map(|state_object_result| { state_object_result_option.map(|state_object_result| {
Arc::new( Result::as_ref(&state_object_result)
Result::as_ref(&state_object_result) .map(
.map(|state_object| { |StateObject {
let amount = state_object.state.0; state, attributes, ..
let unit_of_measurement = state_object.attributes.unit_of_measurement; }| {
state
let power = unit_of_measurement.into_uom(amount); .as_ref()
.map(|&amount| attributes.unit_of_measurement.into_uom(amount))
power },
}) )
.map_err(|e| todo!()), .map_err(Clone::clone)
)
}) })
}) })
.context(MappedSignalSnafu)?; .context(MappedSignalSnafu)?;

View File

@@ -4,7 +4,7 @@ use strum::EnumString;
/// A state in Home Assistant that is known to represent an error of some kind: /// A state in Home Assistant that is known to represent an error of some kind:
/// * `unavailable` (the device is likely offline or unreachable from the Home Assistant instance) /// * `unavailable` (the device is likely offline or unreachable from the Home Assistant instance)
/// * `unknown` (I don't know how to explain this one) /// * `unknown` (I don't know how to explain this one)
#[derive(Debug, Clone, EnumString, strum::Display, FromPyFromStr, ToStrToPy)] #[derive(Debug, Clone, Copy, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
pub enum ErrorState { pub enum ErrorState {
Unavailable, Unavailable,

View File

@@ -12,7 +12,7 @@ pub use unexpected_state::UnexpectedState;
pub enum HomeAssistantState<State> { pub enum HomeAssistantState<State> {
Ok(State), Ok(State),
Err(ErrorState), Err(ErrorState),
UnexpectedErr(UnexpectedState), Unexpected(UnexpectedState),
} }
impl<State> From<String> for HomeAssistantState<State> impl<State> From<String> for HomeAssistantState<State>
@@ -28,7 +28,7 @@ where
return HomeAssistantState::Err(error); return HomeAssistantState::Err(error);
} }
HomeAssistantState::UnexpectedErr(UnexpectedState(s.into())) HomeAssistantState::Unexpected(UnexpectedState(s.into()))
} }
} }
@@ -44,6 +44,45 @@ impl<State: FromStr> FromStr for HomeAssistantState<State> {
return Ok(HomeAssistantState::Err(error)); return Ok(HomeAssistantState::Err(error));
} }
Ok(HomeAssistantState::UnexpectedErr(UnexpectedState(s.into()))) Ok(HomeAssistantState::Unexpected(UnexpectedState(s.into())))
}
}
impl<State> HomeAssistantState<State> {
pub fn as_ref(&self) -> HomeAssistantState<&State> {
match self {
HomeAssistantState::Ok(state) => HomeAssistantState::Ok(state),
HomeAssistantState::Err(error_state) => HomeAssistantState::Err(*error_state),
HomeAssistantState::Unexpected(unexpected_state) => {
// TODO: just considered cheap enough to clone implicitly
HomeAssistantState::Unexpected(unexpected_state.clone())
}
}
}
pub fn map<Mapped>(self, f: impl FnOnce(State) -> Mapped) -> HomeAssistantState<Mapped> {
match self {
HomeAssistantState::Ok(state) => HomeAssistantState::Ok(f(state)),
HomeAssistantState::Err(error_state) => HomeAssistantState::Err(error_state),
HomeAssistantState::Unexpected(unexpected_state) => {
HomeAssistantState::Unexpected(unexpected_state)
}
}
}
}
impl<State> HomeAssistantState<&State> {
pub fn cloned(self) -> HomeAssistantState<State>
where
State: Clone,
{
self.map(Clone::clone)
}
pub fn copied(self) -> HomeAssistantState<State>
where
State: Copy,
{
self.map(|&s| s)
} }
} }

View File

@@ -62,7 +62,7 @@ impl<
Arc< Arc<
Result< Result<
Self, Self,
StateObjectSignalError<<Self as FromPyObject<'a, 'py>>::Error>, StateObjectSignalError<Arc<<Self as FromPyObject<'a, 'py>>::Error>>,
>, >,
>, >,
>, >,
@@ -74,6 +74,16 @@ impl<
let state_machine = home_assistant.states(py).context(GetStatesSnafu)?; let state_machine = home_assistant.states(py).context(GetStatesSnafu)?;
let current = state_machine let current = state_machine
.get(py, entity_id.clone()) .get(py, entity_id.clone())
.map_err(|e| match e {
GetStateError::GetStateObjectError { source } => {
GetStateError::GetStateObjectError { source }
}
GetStateError::ExtractStateObjectError { source } => {
GetStateError::ExtractStateObjectError {
source: Arc::new(source),
}
}
})
.context(GetFromStateMachineSnafu) .context(GetFromStateMachineSnafu)
.transpose(); .transpose();