chore: extract python_utils
and home-assistant
to their own crates
This commit is contained in:
32
home-assistant/Cargo.toml
Normal file
32
home-assistant/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "home-assistant"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[dependencies]
|
||||
arbitrary-value = { path = "../arbitrary-value" }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
derive_more = { workspace = true, features = [
|
||||
"display",
|
||||
"from",
|
||||
"from_str",
|
||||
"into",
|
||||
"try_from",
|
||||
"try_into",
|
||||
] }
|
||||
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||
once_cell = "1.21.3"
|
||||
protocol = { path = "../protocol" }
|
||||
pyo3 = { workspace = true }
|
||||
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
|
||||
python-utils = { path = "../python-utils" }
|
||||
smol_str = "0.3.2"
|
||||
snafu = { workspace = true }
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
tokio = { workspace = true }
|
||||
tracing = { optional = true, workspace = true }
|
||||
ulid = "1.2.0"
|
34
home-assistant/src/domain.rs
Normal file
34
home-assistant/src/domain.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(Debug, Clone, EnumString, strum::Display)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Domain {
|
||||
Automation,
|
||||
BinarySensor,
|
||||
Button,
|
||||
Camera,
|
||||
Climate,
|
||||
Conversation,
|
||||
Cover,
|
||||
DeviceTracker,
|
||||
Group,
|
||||
InputDatetime,
|
||||
InputNumber,
|
||||
InputSelect,
|
||||
InputText,
|
||||
Light,
|
||||
Lock,
|
||||
MediaPlayer,
|
||||
Notify,
|
||||
Person,
|
||||
Remote,
|
||||
Scene,
|
||||
Select,
|
||||
Sensor,
|
||||
Sun,
|
||||
Switch,
|
||||
Tag,
|
||||
Update,
|
||||
Weather,
|
||||
Zone,
|
||||
}
|
71
home-assistant/src/entity_id.rs
Normal file
71
home-assistant/src/entity_id.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::{convert::Infallible, fmt::Display, str::FromStr};
|
||||
|
||||
use pyo3::{exceptions::PyValueError, prelude::*, types::PyString};
|
||||
use snafu::{ResultExt, Snafu};
|
||||
|
||||
use super::{
|
||||
domain::Domain,
|
||||
object_id::{ObjectId, ObjectIdParsingError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EntityId(pub Domain, pub ObjectId);
|
||||
|
||||
#[derive(Debug, Clone, Snafu)]
|
||||
pub enum EntityIdParsingError {
|
||||
#[snafu(display("entity IDs have a dot / period in them, e.g. light.kitchen_lamp"))]
|
||||
MissingDot,
|
||||
|
||||
#[snafu(display("could not parse the domain part of the entity ID"))]
|
||||
ParsingDomain { source: <Domain as FromStr>::Err },
|
||||
|
||||
#[snafu(display("could not parse the object ID part of the entity ID"))]
|
||||
ParsingObjectId { source: ObjectIdParsingError },
|
||||
}
|
||||
|
||||
impl FromStr for EntityId {
|
||||
type Err = EntityIdParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (domain, object_id) = s.split_once('.').ok_or(EntityIdParsingError::MissingDot)?;
|
||||
|
||||
let domain = domain.parse().context(ParsingDomainSnafu)?;
|
||||
let object_id = object_id.parse().context(ParsingObjectIdSnafu)?;
|
||||
|
||||
Ok(Self(domain, object_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EntityId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let Self(domain, object_id) = self;
|
||||
|
||||
write!(f, "{domain}.{object_id}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EntityIdParsingError> for PyErr {
|
||||
fn from(error: EntityIdParsingError) -> Self {
|
||||
PyValueError::new_err(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> FromPyObject<'py> for EntityId {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.extract()?;
|
||||
let entity_id = EntityId::from_str(s)?;
|
||||
|
||||
Ok(entity_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for EntityId {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let s = self.to_string();
|
||||
s.into_pyobject(py)
|
||||
}
|
||||
}
|
39
home-assistant/src/event/context/context.rs
Normal file
39
home-assistant/src/event/context/context.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::id::Id;
|
||||
use once_cell::sync::OnceCell;
|
||||
use pyo3::{prelude::*, types::PyType};
|
||||
|
||||
/// The context that triggered something.
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct Context<Event> {
|
||||
pub id: Id,
|
||||
pub user_id: Option<String>,
|
||||
pub parent_id: Option<String>,
|
||||
/// In order to prevent cycles, the user must decide to pass [`Py<PyAny>`] for the `Event` type here
|
||||
/// or for the `Context` type in [`Event`]
|
||||
pub origin_event: Event,
|
||||
}
|
||||
|
||||
impl<'py, Event: IntoPyObject<'py>> IntoPyObject<'py> for Context<Event> {
|
||||
type Target = PyAny;
|
||||
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
|
||||
type Error = PyErr;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
static HOMEASSISTANT_CORE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||
|
||||
let homeassistant_core = HOMEASSISTANT_CORE
|
||||
.get_or_try_init(|| Result::<_, PyErr>::Ok(py.import("homeassistant.core")?.unbind()))?
|
||||
.bind(py);
|
||||
|
||||
let context_class = homeassistant_core.getattr("Context")?;
|
||||
let context_class = context_class.downcast_into::<PyType>()?;
|
||||
|
||||
let context_instance = context_class.call1((self.user_id, self.parent_id, self.id))?;
|
||||
|
||||
context_instance.setattr("origin_event", self.origin_event)?;
|
||||
|
||||
Ok(context_instance)
|
||||
}
|
||||
}
|
38
home-assistant/src/event/context/id.rs
Normal file
38
home-assistant/src/event/context/id.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::{prelude::*, types::PyString};
|
||||
use smol_str::SmolStr;
|
||||
use ulid::Ulid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Id {
|
||||
Ulid(Ulid),
|
||||
Other(SmolStr),
|
||||
}
|
||||
|
||||
impl<'py> FromPyObject<'py> for Id {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.extract::<String>()?;
|
||||
|
||||
if let Ok(ulid) = s.parse() {
|
||||
Ok(Id::Ulid(ulid))
|
||||
} else {
|
||||
Ok(Id::Other(s.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Id {
|
||||
type Target = PyString;
|
||||
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self {
|
||||
Id::Ulid(ulid) => ulid.to_string().into_pyobject(py),
|
||||
Id::Other(id) => id.as_str().into_pyobject(py),
|
||||
}
|
||||
}
|
||||
}
|
2
home-assistant/src/event/context/mod.rs
Normal file
2
home-assistant/src/event/context/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod context;
|
||||
pub mod id;
|
27
home-assistant/src/event/event.rs
Normal file
27
home-assistant/src/event/event.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::event_origin::EventOrigin;
|
||||
|
||||
/// Representation of an event within the bus.
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct Event<Type, Data, Context> {
|
||||
pub event_type: Type,
|
||||
pub data: Data,
|
||||
pub origin: EventOrigin,
|
||||
/// In order to prevent cycles, the user must decide to pass [`Py<PyAny>`] for the `Context` type here
|
||||
/// or for the `Event` type in [`Context`]
|
||||
pub context: Context,
|
||||
time_fired_timestamp: f64,
|
||||
}
|
||||
|
||||
impl<Type, Data, Context> Event<Type, Data, Context> {
|
||||
pub fn time_fired(&self) -> Option<DateTime<Utc>> {
|
||||
const NANOS_PER_SEC: i32 = 1_000_000_000;
|
||||
|
||||
let secs = self.time_fired_timestamp as i64;
|
||||
let nsecs = (self.time_fired_timestamp.fract() * (NANOS_PER_SEC as f64)) as u32;
|
||||
|
||||
DateTime::from_timestamp(secs, nsecs)
|
||||
}
|
||||
}
|
21
home-assistant/src/event/event_origin.rs
Normal file
21
home-assistant/src/event/event_origin.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::{exceptions::PyValueError, prelude::*};
|
||||
|
||||
#[derive(Debug, Clone, strum::EnumString, strum::Display)]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
pub enum EventOrigin {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
impl<'py> FromPyObject<'py> for EventOrigin {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.str()?;
|
||||
let s = s.extract()?;
|
||||
let event_origin =
|
||||
EventOrigin::from_str(s).map_err(|err| PyValueError::new_err(err.to_string()))?;
|
||||
|
||||
Ok(event_origin)
|
||||
}
|
||||
}
|
4
home-assistant/src/event/mod.rs
Normal file
4
home-assistant/src/event/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod context;
|
||||
pub mod event;
|
||||
pub mod event_origin;
|
||||
pub mod specific;
|
1
home-assistant/src/event/specific/mod.rs
Normal file
1
home-assistant/src/event/specific/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod state_changed;
|
58
home-assistant/src/event/specific/state_changed.rs
Normal file
58
home-assistant/src/event/specific/state_changed.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use crate::{entity_id::EntityId, state_object::StateObject};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Type;
|
||||
|
||||
impl<'py> FromPyObject<'py> for Type {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.extract::<&str>()?;
|
||||
|
||||
if s == "state_changed" {
|
||||
Ok(Type)
|
||||
} else {
|
||||
Err(PyValueError::new_err(format!(
|
||||
"expected a string of value 'state_changed', but got {s}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
#[pyo3(from_item_all)]
|
||||
pub struct Data<
|
||||
OldState,
|
||||
OldAttributes,
|
||||
OldStateContextEvent,
|
||||
NewState,
|
||||
NewAttributes,
|
||||
NewStateContextEvent,
|
||||
> {
|
||||
pub entity_id: EntityId,
|
||||
pub old_state: Option<StateObject<OldState, OldAttributes, OldStateContextEvent>>,
|
||||
pub new_state: Option<StateObject<NewState, NewAttributes, NewStateContextEvent>>,
|
||||
}
|
||||
|
||||
/// A state changed event is fired when on state write the state is changed.
|
||||
pub type Event<
|
||||
OldState,
|
||||
OldAttributes,
|
||||
OldStateContextEvent,
|
||||
NewState,
|
||||
NewAttributes,
|
||||
NewStateContextEvent,
|
||||
Context,
|
||||
> = super::super::event::Event<
|
||||
Type,
|
||||
Data<
|
||||
OldState,
|
||||
OldAttributes,
|
||||
OldStateContextEvent,
|
||||
NewState,
|
||||
NewAttributes,
|
||||
NewStateContextEvent,
|
||||
>,
|
||||
Context,
|
||||
>;
|
60
home-assistant/src/home_assistant.rs
Normal file
60
home-assistant/src/home_assistant.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use python_utils::{detach, validate_type_by_name};
|
||||
|
||||
use super::{service_registry::ServiceRegistry, state_machine::StateMachine};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HomeAssistant(Py<PyAny>);
|
||||
|
||||
impl<'source> FromPyObject<'source> for HomeAssistant {
|
||||
fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult<Self> {
|
||||
// region: Validation
|
||||
validate_type_by_name(ob, "HomeAssistant")?;
|
||||
// endregion: Validation
|
||||
|
||||
Ok(Self(detach(ob)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &HomeAssistant {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.0.bind(py).to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl HomeAssistant {
|
||||
/// Return the representation
|
||||
pub fn repr(&self, py: Python<'_>) -> Result<String, PyErr> {
|
||||
let bound = self.0.bind(py);
|
||||
let repr = bound.repr()?;
|
||||
repr.extract()
|
||||
}
|
||||
|
||||
/// Return if Home Assistant is running.
|
||||
pub fn is_running(&self, py: Python<'_>) -> Result<bool, PyErr> {
|
||||
let is_running = self.0.getattr(py, "is_running")?;
|
||||
is_running.extract(py)
|
||||
}
|
||||
/// Return if Home Assistant is stopping.
|
||||
pub fn is_stopping(&self, py: Python<'_>) -> Result<bool, PyErr> {
|
||||
let is_stopping = self.0.getattr(py, "is_stopping")?;
|
||||
is_stopping.extract(py)
|
||||
}
|
||||
|
||||
pub fn states(&self, py: Python<'_>) -> Result<StateMachine, PyErr> {
|
||||
let states = self.0.getattr(py, "states")?;
|
||||
states.extract(py)
|
||||
}
|
||||
|
||||
pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, PyErr> {
|
||||
let services = self.0.getattr(py, "services")?;
|
||||
services.extract(py)
|
||||
}
|
||||
}
|
13
home-assistant/src/lib.rs
Normal file
13
home-assistant/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod domain;
|
||||
pub mod entity_id;
|
||||
pub mod event;
|
||||
pub mod home_assistant;
|
||||
pub mod light;
|
||||
pub mod logger;
|
||||
pub mod object_id;
|
||||
pub mod service;
|
||||
pub mod service_registry;
|
||||
pub mod slug;
|
||||
pub mod state;
|
||||
pub mod state_machine;
|
||||
pub mod state_object;
|
8
home-assistant/src/light/attributes.rs
Normal file
8
home-assistant/src/light/attributes.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
#[pyo3(from_item_all)]
|
||||
pub struct LightAttributes {
|
||||
min_color_temp_kelvin: Option<u16>, // TODO: only here to allow compilation!
|
||||
max_color_temp_kelvin: Option<u16>, // TODO: only here to allow compilation!
|
||||
}
|
54
home-assistant/src/light/mod.rs
Normal file
54
home-assistant/src/light/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use attributes::LightAttributes;
|
||||
use pyo3::prelude::*;
|
||||
use snafu::{ResultExt, Snafu};
|
||||
use state::LightState;
|
||||
|
||||
use crate::state::HomeAssistantState;
|
||||
|
||||
use super::{
|
||||
domain::Domain, entity_id::EntityId, home_assistant::HomeAssistant, object_id::ObjectId,
|
||||
state_object::StateObject,
|
||||
};
|
||||
|
||||
mod attributes;
|
||||
mod protocol;
|
||||
mod service;
|
||||
mod state;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HomeAssistantLight {
|
||||
pub home_assistant: HomeAssistant,
|
||||
pub object_id: ObjectId,
|
||||
}
|
||||
|
||||
impl HomeAssistantLight {
|
||||
fn entity_id(&self) -> EntityId {
|
||||
EntityId(Domain::Light, self.object_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub enum GetStateObjectError {
|
||||
PythonError { source: PyErr },
|
||||
EntityMissing,
|
||||
}
|
||||
|
||||
impl HomeAssistantLight {
|
||||
fn get_state_object(
|
||||
&self,
|
||||
) -> Result<
|
||||
StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>>,
|
||||
GetStateObjectError,
|
||||
> {
|
||||
Python::with_gil(|py| {
|
||||
let states = self.home_assistant.states(py).context(PythonSnafu)?;
|
||||
let entity_id = self.entity_id();
|
||||
let state_object = states
|
||||
.get(py, entity_id)
|
||||
.context(PythonSnafu)?
|
||||
.ok_or(GetStateObjectError::EntityMissing)?;
|
||||
|
||||
Ok(state_object)
|
||||
})
|
||||
}
|
||||
}
|
108
home-assistant/src/light/protocol.rs
Normal file
108
home-assistant/src/light/protocol.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use super::service::{turn_off::TurnOff, turn_on::TurnOn};
|
||||
use super::{state::LightState, GetStateObjectError, HomeAssistantLight};
|
||||
use crate::{
|
||||
event::context::context::Context,
|
||||
state::{ErrorState, HomeAssistantState, UnexpectedState},
|
||||
};
|
||||
use arbitrary_value::arbitrary::Arbitrary;
|
||||
use protocol::light::Light;
|
||||
use pyo3::prelude::*;
|
||||
use snafu::{ResultExt, Snafu};
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub enum IsStateError {
|
||||
GetStateObjectError { source: GetStateObjectError },
|
||||
Error { state: ErrorState },
|
||||
UnexpectedError { state: UnexpectedState },
|
||||
}
|
||||
|
||||
impl Light for HomeAssistantLight {
|
||||
type IsOnError = IsStateError;
|
||||
|
||||
async fn is_on(&self) -> Result<bool, Self::IsOnError> {
|
||||
let state_object = self.get_state_object().context(GetStateObjectSnafu)?;
|
||||
let state = state_object.state;
|
||||
|
||||
match state {
|
||||
HomeAssistantState::Ok(light_state) => Ok(matches!(light_state, LightState::On)),
|
||||
HomeAssistantState::Err(state) => Err(IsStateError::Error { state }),
|
||||
HomeAssistantState::UnexpectedErr(state) => {
|
||||
Err(IsStateError::UnexpectedError { state })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type IsOffError = IsStateError;
|
||||
|
||||
async fn is_off(&self) -> Result<bool, Self::IsOffError> {
|
||||
let state_object = self.get_state_object().context(GetStateObjectSnafu)?;
|
||||
let state = state_object.state;
|
||||
|
||||
match state {
|
||||
HomeAssistantState::Ok(light_state) => Ok(matches!(light_state, LightState::Off)),
|
||||
HomeAssistantState::Err(state) => Err(IsStateError::Error { state }),
|
||||
HomeAssistantState::UnexpectedErr(state) => {
|
||||
Err(IsStateError::UnexpectedError { state })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TurnOnError = PyErr;
|
||||
|
||||
async fn turn_on(&mut self) -> Result<(), Self::TurnOnError> {
|
||||
let context: Option<Context<()>> = None;
|
||||
let target: Option<()> = None;
|
||||
|
||||
let services = Python::with_gil(|py| self.home_assistant.services(py))?;
|
||||
// TODO
|
||||
let service_response: Arbitrary = services
|
||||
.call_service(
|
||||
TurnOn {
|
||||
entity_id: self.entity_id(),
|
||||
},
|
||||
context,
|
||||
target,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!(?service_response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type TurnOffError = PyErr;
|
||||
|
||||
async fn turn_off(&mut self) -> Result<(), Self::TurnOffError> {
|
||||
let context: Option<Context<()>> = None;
|
||||
let target: Option<()> = None;
|
||||
|
||||
let services = Python::with_gil(|py| self.home_assistant.services(py))?;
|
||||
// TODO
|
||||
let service_response: Arbitrary // TODO: a type that validates as None
|
||||
= services
|
||||
.call_service(
|
||||
TurnOff {
|
||||
entity_id: self.entity_id(),
|
||||
},
|
||||
context,
|
||||
target,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!(?service_response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type ToggleError = PyErr;
|
||||
|
||||
async fn toggle(&mut self) -> Result<(), Self::ToggleError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
2
home-assistant/src/light/service/mod.rs
Normal file
2
home-assistant/src/light/service/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod turn_off;
|
||||
pub mod turn_on;
|
33
home-assistant/src/light/service/turn_off.rs
Normal file
33
home-assistant/src/light/service/turn_off.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::IntoPyObject;
|
||||
|
||||
use crate::{
|
||||
entity_id::EntityId,
|
||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TurnOff {
|
||||
pub entity_id: EntityId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoPyObject)]
|
||||
pub struct TurnOffServiceData {
|
||||
entity_id: EntityId,
|
||||
}
|
||||
|
||||
impl IntoServiceCall for TurnOff {
|
||||
type ServiceData = TurnOffServiceData;
|
||||
|
||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||
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 service_data = TurnOffServiceData { entity_id };
|
||||
|
||||
(service_domain, service_id, service_data)
|
||||
}
|
||||
}
|
32
home-assistant/src/light/service/turn_on.rs
Normal file
32
home-assistant/src/light/service/turn_on.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::IntoPyObject;
|
||||
|
||||
use crate::{
|
||||
entity_id::EntityId,
|
||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TurnOn {
|
||||
pub entity_id: EntityId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoPyObject)]
|
||||
pub struct TurnOnServiceData {
|
||||
entity_id: EntityId,
|
||||
}
|
||||
|
||||
impl IntoServiceCall for TurnOn {
|
||||
type ServiceData = TurnOnServiceData;
|
||||
|
||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||
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 service_data = TurnOnServiceData { entity_id };
|
||||
|
||||
(service_domain, service_id, service_data)
|
||||
}
|
||||
}
|
22
home-assistant/src/light/state.rs
Normal file
22
home-assistant/src/light/state.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::{exceptions::PyValueError, prelude::*};
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(Debug, Clone, EnumString, strum::Display)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum LightState {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
impl<'py> FromPyObject<'py> for LightState {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.extract::<String>()?;
|
||||
|
||||
let state =
|
||||
LightState::from_str(&s).map_err(|err| PyValueError::new_err(err.to_string()))?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
182
home-assistant/src/logger.rs
Normal file
182
home-assistant/src/logger.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use arbitrary_value::{arbitrary::Arbitrary, map::Map};
|
||||
use once_cell::sync::OnceCell;
|
||||
use pyo3::{prelude::*, types::PyTuple};
|
||||
use python_utils::{detach, validate_type_by_name};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HassLogger(Py<PyAny>);
|
||||
|
||||
impl<'source> FromPyObject<'source> for HassLogger {
|
||||
fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult<Self> {
|
||||
// region: Validation
|
||||
validate_type_by_name(ob, "HassLogger")?;
|
||||
// endregion: Validation
|
||||
|
||||
Ok(Self(detach(ob)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoPyObject)]
|
||||
pub struct LogData<ExcInfo> {
|
||||
/// If exc_info does not evaluate as false, it causes exception information to be added to the logging message.
|
||||
/// If an exception tuple (in the format returned by sys.exc_info()) or an exception instance is provided, it is used;
|
||||
/// otherwise, sys.exc_info() is called to get the exception information.
|
||||
exc_info: Option<ExcInfo>,
|
||||
|
||||
/// If true, stack information is added to the logging message, including the actual logging call.
|
||||
/// Note that this is not the same stack information as that displayed through specifying exc_info:
|
||||
/// The former is stack frames from the bottom of the stack up to the logging call in the current thread,
|
||||
/// whereas the latter is information about stack frames which have been unwound,
|
||||
/// following an exception, while searching for exception handlers.
|
||||
///
|
||||
/// You can specify stack_info independently of exc_info,
|
||||
/// e.g. to just show how you got to a certain point in your code, even when no exceptions were raised.
|
||||
/// The stack frames are printed following a header line which says:
|
||||
///
|
||||
/// Stack (most recent call last):
|
||||
///
|
||||
/// This mimics the `Traceback (most recent call last):` which is used when displaying exception frames.
|
||||
stack_info: bool,
|
||||
|
||||
/// If greater than 1, the corresponding number of stack frames are skipped
|
||||
/// when computing the line number and function name set in the LogRecord created for the logging event.
|
||||
/// This can be used in logging helpers so that the function name, filename and line number recorded
|
||||
/// are not the information for the helper function/method, but rather its caller.
|
||||
stacklevel: u16,
|
||||
|
||||
/// This can be used to pass a dictionary which is used to populate the __dict__ of the LogRecord
|
||||
/// created for the logging event with user-defined attributes.
|
||||
/// These custom attributes can then be used as you like.
|
||||
/// For example, they could be incorporated into logged messages.
|
||||
extra: Map,
|
||||
}
|
||||
|
||||
impl HassLogger {
|
||||
pub fn new(py: Python<'_>, name: &str) -> PyResult<Self> {
|
||||
static LOGGING_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||
|
||||
let logging_module = LOGGING_MODULE
|
||||
.get_or_try_init(|| Result::<_, PyErr>::Ok(py.import("logging")?.unbind()))?
|
||||
.bind(py);
|
||||
let logger = logging_module.call_method1("getLogger", (name,))?;
|
||||
|
||||
Ok(logger.extract()?)
|
||||
}
|
||||
|
||||
pub fn debug<'py, ExcInfo: IntoPyObject<'py>>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
msg: &str,
|
||||
args: Vec<Arbitrary>,
|
||||
log_data: Option<LogData<ExcInfo>>,
|
||||
) -> PyResult<()> {
|
||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||
for arg in args {
|
||||
let arg = arg.into_pyobject(py)?;
|
||||
all_args.push(arg);
|
||||
}
|
||||
let all_args = PyTuple::new(py, all_args)?;
|
||||
|
||||
let kwargs = log_data
|
||||
.map(|log_data| log_data.into_pyobject(py))
|
||||
.transpose()?;
|
||||
|
||||
self.0.call_method(py, "debug", all_args, kwargs.as_ref())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn info<'py, ExcInfo: IntoPyObject<'py>>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
msg: &str,
|
||||
args: Vec<Arbitrary>,
|
||||
log_data: Option<LogData<ExcInfo>>,
|
||||
) -> PyResult<()> {
|
||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||
for arg in args {
|
||||
let arg = arg.into_pyobject(py)?;
|
||||
all_args.push(arg);
|
||||
}
|
||||
let all_args = PyTuple::new(py, all_args)?;
|
||||
|
||||
let kwargs = log_data
|
||||
.map(|log_data| log_data.into_pyobject(py))
|
||||
.transpose()?;
|
||||
|
||||
self.0.call_method(py, "info", all_args, kwargs.as_ref())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn warning<'py, ExcInfo: IntoPyObject<'py>>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
msg: &str,
|
||||
args: Vec<Arbitrary>,
|
||||
log_data: Option<LogData<ExcInfo>>,
|
||||
) -> PyResult<()> {
|
||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||
for arg in args {
|
||||
let arg = arg.into_pyobject(py)?;
|
||||
all_args.push(arg);
|
||||
}
|
||||
let all_args = PyTuple::new(py, all_args)?;
|
||||
|
||||
let kwargs = log_data
|
||||
.map(|log_data| log_data.into_pyobject(py))
|
||||
.transpose()?;
|
||||
|
||||
self.0
|
||||
.call_method(py, "warning", all_args, kwargs.as_ref())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn error<'py, ExcInfo: IntoPyObject<'py>>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
msg: &str,
|
||||
args: Vec<Arbitrary>,
|
||||
log_data: Option<LogData<ExcInfo>>,
|
||||
) -> PyResult<()> {
|
||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||
for arg in args {
|
||||
let arg = arg.into_pyobject(py)?;
|
||||
all_args.push(arg);
|
||||
}
|
||||
let all_args = PyTuple::new(py, all_args)?;
|
||||
|
||||
let kwargs = log_data
|
||||
.map(|log_data| log_data.into_pyobject(py))
|
||||
.transpose()?;
|
||||
|
||||
self.0.call_method(py, "error", all_args, kwargs.as_ref())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn critical<'py, ExcInfo: IntoPyObject<'py>>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
msg: &str,
|
||||
args: Vec<Arbitrary>,
|
||||
log_data: Option<LogData<ExcInfo>>,
|
||||
) -> PyResult<()> {
|
||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||
for arg in args {
|
||||
let arg = arg.into_pyobject(py)?;
|
||||
all_args.push(arg);
|
||||
}
|
||||
let all_args = PyTuple::new(py, all_args)?;
|
||||
|
||||
let kwargs = log_data
|
||||
.map(|log_data| log_data.into_pyobject(py))
|
||||
.transpose()?;
|
||||
|
||||
self.0
|
||||
.call_method(py, "critical", all_args, kwargs.as_ref())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
21
home-assistant/src/object_id.rs
Normal file
21
home-assistant/src/object_id.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::{prelude::*, types::PyString};
|
||||
|
||||
use super::slug::Slug;
|
||||
|
||||
pub use super::slug::SlugParsingError as ObjectIdParsingError;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
||||
pub struct ObjectId(pub Slug);
|
||||
|
||||
impl<'py> IntoPyObject<'py> for ObjectId {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let s = self.to_string();
|
||||
s.into_pyobject(py)
|
||||
}
|
||||
}
|
11
home-assistant/src/service/mod.rs
Normal file
11
home-assistant/src/service/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use service_domain::ServiceDomain;
|
||||
use service_id::ServiceId;
|
||||
|
||||
pub mod service_domain;
|
||||
pub mod service_id;
|
||||
|
||||
pub trait IntoServiceCall {
|
||||
type ServiceData;
|
||||
|
||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData);
|
||||
}
|
21
home-assistant/src/service/service_domain.rs
Normal file
21
home-assistant/src/service/service_domain.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::{prelude::*, types::PyString};
|
||||
|
||||
use super::super::slug::Slug;
|
||||
|
||||
pub use super::super::slug::SlugParsingError as ServiceDomainParsingError;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
||||
pub struct ServiceDomain(pub Slug);
|
||||
|
||||
impl<'py> IntoPyObject<'py> for ServiceDomain {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let s = self.to_string();
|
||||
s.into_pyobject(py)
|
||||
}
|
||||
}
|
21
home-assistant/src/service/service_id.rs
Normal file
21
home-assistant/src/service/service_id.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::{prelude::*, types::PyString};
|
||||
|
||||
use super::super::slug::Slug;
|
||||
|
||||
pub use super::super::slug::SlugParsingError as ServiceIdParsingError;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
||||
pub struct ServiceId(pub Slug);
|
||||
|
||||
impl<'py> IntoPyObject<'py> for ServiceId {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let s = self.to_string();
|
||||
s.into_pyobject(py)
|
||||
}
|
||||
}
|
54
home-assistant/src/service_registry.rs
Normal file
54
home-assistant/src/service_registry.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use super::{event::context::context::Context, service::IntoServiceCall};
|
||||
use pyo3::prelude::*;
|
||||
use python_utils::{detach, validate_type_by_name};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceRegistry(Py<PyAny>);
|
||||
|
||||
impl<'py> FromPyObject<'py> for ServiceRegistry {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
// region: Validation
|
||||
validate_type_by_name(ob, "ServiceRegistry")?;
|
||||
// endregion: Validation
|
||||
|
||||
Ok(Self(detach(ob)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceRegistry {
|
||||
pub async fn call_service<
|
||||
ServiceData: for<'py> IntoPyObject<'py>,
|
||||
Target: for<'py> IntoPyObject<'py>,
|
||||
Event: for<'py> IntoPyObject<'py>,
|
||||
ServiceResponse: for<'py> FromPyObject<'py>,
|
||||
>(
|
||||
&self,
|
||||
service_call: impl IntoServiceCall<ServiceData = ServiceData>,
|
||||
context: Option<Context<Event>>,
|
||||
target: Option<Target>,
|
||||
return_response: bool,
|
||||
) -> PyResult<ServiceResponse> {
|
||||
let (domain, service, service_data) = service_call.into_service_call();
|
||||
|
||||
let blocking = true;
|
||||
|
||||
let args = (
|
||||
domain,
|
||||
service,
|
||||
service_data,
|
||||
blocking,
|
||||
context,
|
||||
target,
|
||||
return_response,
|
||||
);
|
||||
|
||||
let future = Python::with_gil::<_, PyResult<_>>(|py| {
|
||||
let service_registry = self.0.bind(py);
|
||||
let awaitable = service_registry.call_method("async_call", args, None)?;
|
||||
pyo3_async_runtimes::tokio::into_future(awaitable)
|
||||
})?;
|
||||
|
||||
let service_response = future.await?;
|
||||
Python::with_gil(|py| service_response.extract(py))
|
||||
}
|
||||
}
|
37
home-assistant/src/slug.rs
Normal file
37
home-assistant/src/slug.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::{exceptions::PyValueError, PyErr};
|
||||
use smol_str::SmolStr;
|
||||
use snafu::Snafu;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display)]
|
||||
pub struct Slug(SmolStr);
|
||||
|
||||
#[derive(Debug, Clone, Snafu)]
|
||||
#[snafu(display("expected a lowercase ASCII alphabetical character (i.e. a through z) or a digit (i.e. 0 through 9) or an underscore (i.e. _) but encountered {encountered}"))]
|
||||
pub struct SlugParsingError {
|
||||
encountered: char,
|
||||
}
|
||||
|
||||
impl From<SlugParsingError> for PyErr {
|
||||
fn from(error: SlugParsingError) -> Self {
|
||||
PyValueError::new_err(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Slug {
|
||||
type Err = SlugParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'a'..='z' => {}
|
||||
'0'..='9' => {}
|
||||
'_' => {}
|
||||
_ => return Err(SlugParsingError { encountered: c }),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(s.into()))
|
||||
}
|
||||
}
|
71
home-assistant/src/state.rs
Normal file
71
home-assistant/src/state.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::{convert::Infallible, str::FromStr};
|
||||
|
||||
use pyo3::{exceptions::PyValueError, prelude::*};
|
||||
use smol_str::SmolStr;
|
||||
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)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ErrorState {
|
||||
Unavailable,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl<'py> FromPyObject<'py> for ErrorState {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.extract::<String>()?;
|
||||
|
||||
let state =
|
||||
ErrorState::from_str(&s).map_err(|err| PyValueError::new_err(err.to_string()))?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
||||
pub struct UnexpectedState(pub SmolStr);
|
||||
|
||||
impl<'py> FromPyObject<'py> for UnexpectedState {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.extract::<String>()?;
|
||||
let s = SmolStr::new(s);
|
||||
|
||||
Ok(UnexpectedState(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display)]
|
||||
pub enum HomeAssistantState<State> {
|
||||
Ok(State),
|
||||
Err(ErrorState),
|
||||
UnexpectedErr(UnexpectedState),
|
||||
}
|
||||
|
||||
impl<State: FromStr> FromStr for HomeAssistantState<State> {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
|
||||
if let Ok(ok) = State::from_str(s) {
|
||||
return Ok(HomeAssistantState::Ok(ok));
|
||||
}
|
||||
|
||||
if let Ok(error) = ErrorState::from_str(s) {
|
||||
return Ok(HomeAssistantState::Err(error));
|
||||
}
|
||||
|
||||
Ok(HomeAssistantState::UnexpectedErr(UnexpectedState(s.into())))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, State: FromStr + FromPyObject<'py>> FromPyObject<'py> for HomeAssistantState<State> {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.extract::<String>()?;
|
||||
|
||||
let Ok(state) = s.parse();
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
34
home-assistant/src/state_machine.rs
Normal file
34
home-assistant/src/state_machine.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use super::entity_id::EntityId;
|
||||
use super::state_object::StateObject;
|
||||
use pyo3::prelude::*;
|
||||
use python_utils::{detach, validate_type_by_name};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StateMachine(Py<PyAny>);
|
||||
|
||||
impl<'py> FromPyObject<'py> for StateMachine {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
// region: Validation
|
||||
validate_type_by_name(ob, "StateMachine")?;
|
||||
// endregion: Validation
|
||||
|
||||
Ok(Self(detach(ob)))
|
||||
}
|
||||
}
|
||||
|
||||
impl StateMachine {
|
||||
pub fn get<
|
||||
'py,
|
||||
State: FromPyObject<'py>,
|
||||
Attributes: FromPyObject<'py>,
|
||||
ContextEvent: FromPyObject<'py>,
|
||||
>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
entity_id: EntityId,
|
||||
) -> PyResult<Option<StateObject<State, Attributes, ContextEvent>>> {
|
||||
let args = (entity_id.to_string(),);
|
||||
let state = self.0.call_method1(py, "get", args)?;
|
||||
state.extract(py)
|
||||
}
|
||||
}
|
151
home-assistant/src/state_object.rs
Normal file
151
home-assistant/src/state_object.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use super::{
|
||||
event::{context::context::Context, specific::state_changed},
|
||||
home_assistant::HomeAssistant,
|
||||
};
|
||||
use crate::entity_id::EntityId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use emitter_and_signal::signal::Signal;
|
||||
use once_cell::sync::OnceCell;
|
||||
use pyo3::{
|
||||
prelude::*,
|
||||
types::{PyCFunction, PyDict, PyTuple},
|
||||
};
|
||||
use std::{future::Future, sync::Arc};
|
||||
use tokio::{select, sync::mpsc};
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct StateObject<State, Attributes, ContextEvent> {
|
||||
pub entity_id: EntityId,
|
||||
pub state: State,
|
||||
pub attributes: Attributes,
|
||||
pub last_changed: Option<DateTime<Utc>>,
|
||||
pub last_reported: Option<DateTime<Utc>>,
|
||||
pub last_updated: Option<DateTime<Utc>>,
|
||||
pub context: Context<ContextEvent>,
|
||||
}
|
||||
|
||||
impl<
|
||||
State: Send + Sync + 'static + for<'py> FromPyObject<'py>,
|
||||
Attributes: Send + Sync + 'static + for<'py> FromPyObject<'py>,
|
||||
ContextEvent: Send + Sync + 'static + for<'py> FromPyObject<'py>,
|
||||
> StateObject<State, Attributes, ContextEvent>
|
||||
{
|
||||
pub fn store(
|
||||
py: Python<'_>,
|
||||
home_assistant: &HomeAssistant,
|
||||
entity_id: EntityId,
|
||||
) -> PyResult<(
|
||||
Signal<Option<Arc<Self>>>,
|
||||
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
|
||||
)> {
|
||||
let state_machine = home_assistant.states(py)?;
|
||||
let current = state_machine.get(py, entity_id.clone())?;
|
||||
|
||||
let py_home_assistant = home_assistant.into_pyobject(py)?.unbind();
|
||||
|
||||
let (store, task) = Signal::new(current.map(Arc::new), |mut publisher_stream| async move {
|
||||
while let Some(publisher) = publisher_stream.wait().await {
|
||||
let (new_state_sender, mut new_state_receiver) = mpsc::channel(8);
|
||||
|
||||
let untrack = Python::with_gil::<_, PyResult<_>>(|py| {
|
||||
static EVENT_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||
|
||||
let event_module = EVENT_MODULE
|
||||
.get_or_try_init(|| {
|
||||
Result::<_, PyErr>::Ok(
|
||||
py.import("homeassistant.helpers.event")?.unbind(),
|
||||
)
|
||||
})?
|
||||
.bind(py);
|
||||
|
||||
let untrack = {
|
||||
let callback =
|
||||
move |args: &Bound<'_, PyTuple>,
|
||||
_kwargs: Option<&Bound<'_, PyDict>>| {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("calling the closure");
|
||||
|
||||
if let Ok((event,)) = args.extract::<(
|
||||
state_changed::Event<
|
||||
State,
|
||||
Attributes,
|
||||
ContextEvent,
|
||||
State,
|
||||
Attributes,
|
||||
ContextEvent,
|
||||
Py<PyAny>,
|
||||
>,
|
||||
)>() {
|
||||
let new_state = event.data.new_state;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("sending a new state"); // TODO: remove
|
||||
new_state_sender.try_send(new_state).unwrap();
|
||||
}
|
||||
};
|
||||
let callback = PyCFunction::new_closure(py, None, None, callback)?;
|
||||
let args = (
|
||||
py_home_assistant.clone_ref(py),
|
||||
vec![entity_id.clone()],
|
||||
callback,
|
||||
);
|
||||
event_module.call_method1("async_track_state_change_event", args)?
|
||||
};
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?untrack, "as any");
|
||||
|
||||
let is_callable = untrack.is_callable();
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?is_callable);
|
||||
|
||||
// let untrack = untrack.downcast_into::<PyFunction>()?;
|
||||
// tracing::debug!(?untrack, "as downcast");
|
||||
|
||||
let untrack = untrack.unbind();
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?untrack, "as unbound");
|
||||
|
||||
Ok(untrack)
|
||||
});
|
||||
|
||||
if let Ok(untrack) = untrack {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("untrack is ok, going to wait for the next relevant event...");
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = publisher.all_unsubscribed() => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("calling untrack");
|
||||
let res = Python::with_gil(|py| untrack.call0(py));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?res);
|
||||
break;
|
||||
}
|
||||
new_state = new_state_receiver.recv() => {
|
||||
match new_state {
|
||||
Some(new_state) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("publishing new state");
|
||||
publisher.publish(new_state.map(Arc::new))
|
||||
},
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("channel dropped");
|
||||
break
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("untrack is err");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok((store, task))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user