feat: input_number signal support, implement Clone for a lot of error types

This commit is contained in:
2026-07-14 22:53:42 -04:00
parent 38c30d87d0
commit e3c54a996e
10 changed files with 196 additions and 32 deletions

View File

@@ -1,4 +1,4 @@
use std::convert::Infallible;
use std::{convert::Infallible, sync::Arc};
use pyo3::{
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 {
/// 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`]
ExtractStateMachineError { source: TypeByNameValidationError },
ExtractStateMachineError {
source: Arc<TypeByNameValidationError>,
},
}
#[derive(Debug, Snafu)]
#[derive(Debug, Clone, Snafu)]
pub enum GetServicesError {
/// 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`]
ExtractServiceRegistryError { source: TypeByNameValidationError },
ExtractServiceRegistryError {
source: Arc<TypeByNameValidationError>,
},
}
impl HomeAssistant {
@@ -74,15 +78,23 @@ impl HomeAssistant {
let states = self
.0
.getattr(py, "states")
.map_err(Arc::new)
.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> {
let services = self
.0
.getattr(py, "services")
.map_err(Arc::new)
.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 pyo3::{FromPyObject, Py, PyAny, Python};
use snafu::{ResultExt, Snafu};
@@ -29,15 +31,15 @@ impl HomeAssistantLight {
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Clone, Snafu)]
pub enum GetStateObjectError {
/// couldn't get the state machine registry
GetStatesError { source: GetStatesError },
/// 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
> },
>> },
/// this entity does not have a state object in the registry
EntityMissing,
@@ -55,6 +57,7 @@ impl HomeAssistantLight {
let entity_id = self.entity_id();
let state_object = states
.get(py, entity_id)
.map_err(Arc::new)
.context(GetStateSnafu)?
.ok_or(GetStateObjectError::EntityMissing)?;

View File

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

View File

@@ -2,7 +2,7 @@ use std::{future::Future, sync::Arc};
use emitter_and_signal::{Signal, SignalExt};
use pyo3::{FromPyObject, Py, PyAny, PyErr, Python};
use python_utils::{FromPyFromStr, FromPyObjectViaParse, ToStrToPy};
use python_utils::{FromPyFromStr, ToStrToPy};
use snafu::{ResultExt, Snafu};
use string_literal::StringLiteral;
@@ -12,6 +12,7 @@ use crate::{
entity_id::EntityId,
home_assistant::HomeAssistant,
object_id::ObjectId,
state::HomeAssistantState,
state_object::{self, StateObject, StateObjectSignalError},
unit_of_measurement::power::UnitOfMeasurement,
};
@@ -47,7 +48,11 @@ pub fn signal<'py>(
object_id: ObjectId,
) -> 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>>,
),
CreateSignalError,
@@ -55,7 +60,7 @@ pub fn signal<'py>(
let entity_id = EntityId(Domain::Sensor, object_id);
let (signal, task1) =
StateObject::<FromPyObjectViaParse<f64>, PowerSensorAttributes, Py<PyAny>>::signal(
StateObject::<HomeAssistantState<f64>, PowerSensorAttributes, Py<PyAny>>::signal(
py,
home_assistant,
entity_id,
@@ -65,18 +70,17 @@ pub fn signal<'py>(
let (signal, task2) = signal
.map(|state_object_result_option| {
state_object_result_option.map(|state_object_result| {
Arc::new(
Result::as_ref(&state_object_result)
.map(|state_object| {
let amount = state_object.state.0;
let unit_of_measurement = state_object.attributes.unit_of_measurement;
let power = unit_of_measurement.into_uom(amount);
power
})
.map_err(|e| todo!()),
)
Result::as_ref(&state_object_result)
.map(
|StateObject {
state, attributes, ..
}| {
state
.as_ref()
.map(|&amount| attributes.unit_of_measurement.into_uom(amount))
},
)
.map_err(Clone::clone)
})
})
.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:
/// * `unavailable` (the device is likely offline or unreachable from the Home Assistant instance)
/// * `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")]
pub enum ErrorState {
Unavailable,

View File

@@ -47,3 +47,42 @@ impl<State: FromStr> FromStr for HomeAssistantState<State> {
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<
Result<
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 current = state_machine
.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)
.transpose();