chore+feat(home-assistant): update to pyo3 0.27 and update extraction errors, switch out SmolStr for Arc<str>, tighten up light service calls and implement some for notify, start implementing units of measurement like for power

This commit is contained in:
2026-01-07 02:10:03 -05:00
parent 97aef026b2
commit fa36b39e81
35 changed files with 1255 additions and 259 deletions

View File

@@ -3,7 +3,9 @@ use pyo3::prelude::*;
use snafu::{ResultExt, Snafu};
use state::LightState;
use crate::state::HomeAssistantState;
use crate::{
home_assistant::GetStatesError, state::HomeAssistantState, state_machine::GetStateError,
};
use super::{
domain::Domain, entity_id::EntityId, home_assistant::HomeAssistant, object_id::ObjectId,
@@ -29,7 +31,15 @@ impl HomeAssistantLight {
#[derive(Debug, Snafu)]
pub enum GetStateObjectError {
PythonError { source: PyErr },
/// 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<
<StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>> as FromPyObject<'static, 'static>>::Error
> },
/// this entity does not have a state object in the registry
EntityMissing,
}
@@ -40,12 +50,12 @@ impl HomeAssistantLight {
StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>>,
GetStateObjectError,
> {
Python::with_gil(|py| {
let states = self.home_assistant.states(py).context(PythonSnafu)?;
Python::attach(|py| {
let states = self.home_assistant.states(py).context(GetStatesSnafu)?;
let entity_id = self.entity_id();
let state_object = states
.get(py, entity_id)
.context(PythonSnafu)?
.context(GetStateSnafu)?
.ok_or(GetStateObjectError::EntityMissing)?;
Ok(state_object)

View File

@@ -1,5 +1,7 @@
use super::service::{turn_off::TurnOff, turn_on::TurnOn};
use super::{state::LightState, GetStateObjectError, HomeAssistantLight};
use super::{GetStateObjectError, HomeAssistantLight};
use crate::home_assistant::GetServicesError;
use crate::service_registry::CallServiceError;
use crate::{
event::context::context::Context,
state::{ErrorState, HomeAssistantState, UnexpectedState},
@@ -35,21 +37,31 @@ impl GetState for HomeAssistantLight {
}
}
#[derive(Debug, Snafu)]
pub enum SetStateError {
/// couldn't get the service registry
GetServicesError { source: GetServicesError },
/// couldn't call the service
CallServiceError { source: CallServiceError },
}
impl SetState for HomeAssistantLight {
type Error = PyErr;
type Error = SetStateError;
async fn set_state(&mut self, state: protocol::light::State) -> Result<(), Self::Error> {
let context: Option<Context<()>> = None;
let target: Option<()> = None;
let services = Python::with_gil(|py| self.home_assistant.services(py))?;
let services =
Python::attach(|py| self.home_assistant.services(py)).context(GetServicesSnafu)?;
let _: IsNone = match state {
protocol::light::State::Off => {
services
.call_service(
TurnOff {
entity_id: self.entity_id(),
object_id: self.object_id.clone(),
},
context,
target,
@@ -61,7 +73,7 @@ impl SetState for HomeAssistantLight {
services
.call_service(
TurnOn {
entity_id: self.entity_id(),
object_id: self.object_id.clone(),
},
context,
target,
@@ -69,7 +81,8 @@ impl SetState for HomeAssistantLight {
)
.await
}
}?;
}
.context(CallServiceSnafu)?;
Ok(())
}

View File

@@ -3,13 +3,12 @@ use std::str::FromStr;
use pyo3::IntoPyObject;
use crate::{
entity_id::EntityId,
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
domain::Domain, entity_id::EntityId, object_id::ObjectId, service::{IntoServiceCall, service_domain::ServiceDomain, service_id::ServiceId}
};
#[derive(Debug, Clone)]
pub struct TurnOff {
pub entity_id: EntityId,
pub object_id: ObjectId,
}
#[derive(Debug, Clone, IntoPyObject)]
@@ -24,7 +23,8 @@ impl IntoServiceCall for TurnOff {
let service_domain = ServiceDomain::from_str("light").expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
let service_id = ServiceId::from_str("turn_off").expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
let Self { entity_id } = self;
let Self { object_id } = self;
let entity_id = EntityId(Domain::Light, object_id);
let service_data = TurnOffServiceData { entity_id };

View File

@@ -3,13 +3,12 @@ use std::str::FromStr;
use pyo3::IntoPyObject;
use crate::{
entity_id::EntityId,
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
domain::Domain, entity_id::EntityId, object_id::ObjectId, service::{IntoServiceCall, service_domain::ServiceDomain, service_id::ServiceId}
};
#[derive(Debug, Clone)]
pub struct TurnOn {
pub entity_id: EntityId,
pub object_id: ObjectId,
}
#[derive(Debug, Clone, IntoPyObject)]
@@ -24,7 +23,9 @@ impl IntoServiceCall for TurnOn {
let service_domain = ServiceDomain::from_str("light").expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
let service_id = ServiceId::from_str("turn_on").expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
let Self { entity_id } = self;
let Self { object_id } = self;
let entity_id = EntityId(Domain::Light, object_id);
let service_data = TurnOnServiceData { entity_id };
(service_domain, service_id, service_data)

View File

@@ -1,6 +1,10 @@
use std::str::FromStr;
use pyo3::{exceptions::PyValueError, prelude::*};
use pyo3::{
exceptions::{PyException, PyValueError},
prelude::*,
};
use snafu::{ResultExt, Snafu};
use strum::EnumString;
#[derive(Debug, Clone, EnumString, strum::Display)]
@@ -10,12 +14,36 @@ pub enum LightState {
Off,
}
impl<'py> FromPyObject<'py> for LightState {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
let s = ob.extract::<String>()?;
#[derive(Debug, Snafu)]
pub enum ExtractLightStateError {
/// couldn't extract the object as a string
ExtractStringError { source: PyErr },
let state =
LightState::from_str(&s).map_err(|err| PyValueError::new_err(err.to_string()))?;
/// couldn't parse the string as a [`LightState`]
ParseError {
source: <LightState as FromStr>::Err,
},
}
impl From<ExtractLightStateError> for PyErr {
fn from(error: ExtractLightStateError) -> Self {
match &error {
ExtractLightStateError::ExtractStringError { .. } => {
PyException::new_err(error.to_string())
}
ExtractLightStateError::ParseError { .. } => PyValueError::new_err(error.to_string()),
}
}
}
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
impl<'a, 'py> FromPyObject<'a, 'py> for LightState {
type Error = ExtractLightStateError;
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
let state = LightState::from_str(&s).context(ParseSnafu)?;
Ok(state)
}