chore: convert into a workspace
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"arbitrary-value",
|
"arbitrary-value",
|
||||||
|
"driver/kasa",
|
||||||
|
"emitter-and-signal",
|
||||||
|
"entrypoint",
|
||||||
|
"protocol",
|
||||||
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
62
entrypoint/Cargo.toml
Normal file
62
entrypoint/Cargo.toml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
[package]
|
||||||
|
name = "smart-home-in-rust-with-home-assistant"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[lib]
|
||||||
|
name = "smart_home_in_rust_with_home_assistant"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
arbitrary-value = { path = "../arbitrary-value", features = ["pyo3"] }
|
||||||
|
arc-swap = "1.7.1"
|
||||||
|
async-gate = "0.4.0"
|
||||||
|
axum = { version = "0.8.1", default-features = false, features = [
|
||||||
|
"http1",
|
||||||
|
"tokio",
|
||||||
|
] }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
chrono-tz = { workspace = true }
|
||||||
|
deranged = { workspace = true, features = ["serde"] }
|
||||||
|
derive_more = { workspace = true, features = [
|
||||||
|
"display",
|
||||||
|
"from",
|
||||||
|
"from_str",
|
||||||
|
"into",
|
||||||
|
"try_from",
|
||||||
|
"try_into",
|
||||||
|
] }
|
||||||
|
driver-kasa = { path = "../driver/kasa" }
|
||||||
|
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||||
|
im = { version = "15.1.0", features = ["rayon"] }
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
protocol = { path = "../protocol" }
|
||||||
|
pyo3 = { workspace = true, features = [
|
||||||
|
"auto-initialize",
|
||||||
|
"chrono",
|
||||||
|
"extension-module",
|
||||||
|
] }
|
||||||
|
pyo3-async-runtimes = { workspace = true, features = [
|
||||||
|
"attributes",
|
||||||
|
"tokio-runtime",
|
||||||
|
] }
|
||||||
|
shadow-rs = { version = "1.0.1", default-features = false }
|
||||||
|
smol_str = "0.3.2"
|
||||||
|
snafu = { workspace = true }
|
||||||
|
strum = { version = "0.27.1", features = ["derive"] }
|
||||||
|
tokio = { workspace = true, features = [
|
||||||
|
"macros",
|
||||||
|
"rt",
|
||||||
|
"rt-multi-thread",
|
||||||
|
"sync",
|
||||||
|
"time",
|
||||||
|
] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-appender = "0.2.3"
|
||||||
|
tracing-subscriber = "0.3.17"
|
||||||
|
ulid = "1.2.0"
|
||||||
|
uom = "0.36.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
shadow-rs = "1.0.1"
|
@@ -59,7 +59,7 @@ impl<'py> FromPyObject<'py> for EntityId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'py> IntoPyObject<'py> for &EntityId {
|
impl<'py> IntoPyObject<'py> for EntityId {
|
||||||
type Target = PyString;
|
type Target = PyString;
|
||||||
type Output = Bound<'py, Self::Target>;
|
type Output = Bound<'py, Self::Target>;
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
39
entrypoint/src/home_assistant/event/context/context.rs
Normal file
39
entrypoint/src/home_assistant/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
entrypoint/src/home_assistant/event/context/id.rs
Normal file
38
entrypoint/src/home_assistant/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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,58 @@
|
|||||||
|
use pyo3::exceptions::PyValueError;
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use crate::home_assistant::{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,
|
||||||
|
>;
|
@@ -4,7 +4,7 @@ use pyo3::prelude::*;
|
|||||||
|
|
||||||
use crate::python_utils::{detach, validate_type_by_name};
|
use crate::python_utils::{detach, validate_type_by_name};
|
||||||
|
|
||||||
use super::state_machine::StateMachine;
|
use super::{service_registry::ServiceRegistry, state_machine::StateMachine};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HomeAssistant(Py<PyAny>);
|
pub struct HomeAssistant(Py<PyAny>);
|
||||||
@@ -52,4 +52,9 @@ impl HomeAssistant {
|
|||||||
let states = self.0.getattr(py, "states")?;
|
let states = self.0.getattr(py, "states")?;
|
||||||
states.extract(py)
|
states.extract(py)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, PyErr> {
|
||||||
|
let services = self.0.getattr(py, "services")?;
|
||||||
|
services.extract(py)
|
||||||
|
}
|
||||||
}
|
}
|
8
entrypoint/src/home_assistant/light/attributes.rs
Normal file
8
entrypoint/src/home_assistant/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
entrypoint/src/home_assistant/light/mod.rs
Normal file
54
entrypoint/src/home_assistant/light/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use attributes::LightAttributes;
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
use state::LightState;
|
||||||
|
|
||||||
|
use crate::home_assistant::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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
103
entrypoint/src/home_assistant/light/protocol.rs
Normal file
103
entrypoint/src/home_assistant/light/protocol.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use super::service::{turn_off::TurnOff, turn_on::TurnOn};
|
||||||
|
use super::{state::LightState, GetStateObjectError, HomeAssistantLight};
|
||||||
|
use crate::home_assistant::{
|
||||||
|
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
|
||||||
|
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?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToggleError = PyErr;
|
||||||
|
|
||||||
|
async fn toggle(&mut self) -> Result<(), Self::ToggleError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
2
entrypoint/src/home_assistant/light/service/mod.rs
Normal file
2
entrypoint/src/home_assistant/light/service/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod turn_off;
|
||||||
|
pub mod turn_on;
|
33
entrypoint/src/home_assistant/light/service/turn_off.rs
Normal file
33
entrypoint/src/home_assistant/light/service/turn_off.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use pyo3::IntoPyObject;
|
||||||
|
|
||||||
|
use crate::home_assistant::{
|
||||||
|
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
entrypoint/src/home_assistant/light/service/turn_on.rs
Normal file
32
entrypoint/src/home_assistant/light/service/turn_on.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use std::{convert::Infallible, str::FromStr};
|
||||||
|
|
||||||
|
use pyo3::IntoPyObject;
|
||||||
|
|
||||||
|
use crate::home_assistant::{
|
||||||
|
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
entrypoint/src/home_assistant/light/state.rs
Normal file
22
entrypoint/src/home_assistant/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)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,8 @@
|
|||||||
|
use crate::python_utils::{detach, validate_type_by_name};
|
||||||
|
use arbitrary_value::{arbitrary::Arbitrary, map::Map};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
use pyo3::{prelude::*, types::PyTuple};
|
use pyo3::{prelude::*, types::PyTuple};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
arbitrary::{arbitrary::Arbitrary, map::Map},
|
|
||||||
python_utils::{detach, validate_type_by_name},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HassLogger(Py<PyAny>);
|
pub struct HassLogger(Py<PyAny>);
|
||||||
|
|
||||||
@@ -55,8 +53,12 @@ pub struct LogData<ExcInfo> {
|
|||||||
|
|
||||||
impl HassLogger {
|
impl HassLogger {
|
||||||
pub fn new(py: Python<'_>, name: &str) -> PyResult<Self> {
|
pub fn new(py: Python<'_>, name: &str) -> PyResult<Self> {
|
||||||
let logging = py.import("logging")?;
|
static LOGGING_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||||
let logger = logging.call_method1("getLogger", (name,))?;
|
|
||||||
|
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()?)
|
Ok(logger.extract()?)
|
||||||
}
|
}
|
@@ -2,7 +2,12 @@ pub mod domain;
|
|||||||
pub mod entity_id;
|
pub mod entity_id;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod home_assistant;
|
pub mod home_assistant;
|
||||||
|
pub mod light;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
pub mod object_id;
|
pub mod object_id;
|
||||||
|
pub mod service;
|
||||||
|
pub mod service_registry;
|
||||||
|
pub mod slug;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod state_machine;
|
pub mod state_machine;
|
||||||
|
pub mod state_object;
|
21
entrypoint/src/home_assistant/object_id.rs
Normal file
21
entrypoint/src/home_assistant/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
entrypoint/src/home_assistant/service/mod.rs
Normal file
11
entrypoint/src/home_assistant/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
entrypoint/src/home_assistant/service/service_domain.rs
Normal file
21
entrypoint/src/home_assistant/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
entrypoint/src/home_assistant/service/service_id.rs
Normal file
21
entrypoint/src/home_assistant/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)
|
||||||
|
}
|
||||||
|
}
|
56
entrypoint/src/home_assistant/service_registry.rs
Normal file
56
entrypoint/src/home_assistant/service_registry.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use crate::python_utils::{detach, validate_type_by_name};
|
||||||
|
|
||||||
|
use super::{event::context::context::Context, service::IntoServiceCall};
|
||||||
|
|
||||||
|
#[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))
|
||||||
|
}
|
||||||
|
}
|
@@ -1,19 +1,26 @@
|
|||||||
use std::{str::FromStr, sync::Arc};
|
use std::str::FromStr;
|
||||||
|
|
||||||
use pyo3::{exceptions::PyValueError, PyErr};
|
use pyo3::{exceptions::PyValueError, PyErr};
|
||||||
|
use smol_str::SmolStr;
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
#[derive(Debug, Clone, derive_more::Display)]
|
#[derive(Debug, Clone, derive_more::Display)]
|
||||||
pub struct ObjectId(Arc<str>);
|
pub struct Slug(SmolStr);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Snafu)]
|
#[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}"))]
|
#[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 ObjectIdParsingError {
|
pub struct SlugParsingError {
|
||||||
encountered: char,
|
encountered: char,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ObjectId {
|
impl From<SlugParsingError> for PyErr {
|
||||||
type Err = ObjectIdParsingError;
|
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> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
for c in s.chars() {
|
for c in s.chars() {
|
||||||
@@ -21,16 +28,10 @@ impl FromStr for ObjectId {
|
|||||||
'a'..='z' => {}
|
'a'..='z' => {}
|
||||||
'0'..='9' => {}
|
'0'..='9' => {}
|
||||||
'_' => {}
|
'_' => {}
|
||||||
_ => return Err(ObjectIdParsingError { encountered: c }),
|
_ => return Err(SlugParsingError { encountered: c }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(s.into()))
|
Ok(Self(s.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ObjectIdParsingError> for PyErr {
|
|
||||||
fn from(error: ObjectIdParsingError) -> Self {
|
|
||||||
PyValueError::new_err(error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
71
entrypoint/src/home_assistant/state.rs
Normal file
71
entrypoint/src/home_assistant/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)
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,7 @@ use crate::{
|
|||||||
python_utils::{detach, validate_type_by_name},
|
python_utils::{detach, validate_type_by_name},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::state::State;
|
use super::state_object::StateObject;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StateMachine(Py<PyAny>);
|
pub struct StateMachine(Py<PyAny>);
|
||||||
@@ -21,11 +21,16 @@ impl<'py> FromPyObject<'py> for StateMachine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StateMachine {
|
impl StateMachine {
|
||||||
pub fn get<Attributes: for<'py> FromPyObject<'py>, ContextEvent: for<'py> FromPyObject<'py>>(
|
pub fn get<
|
||||||
|
'py,
|
||||||
|
State: FromPyObject<'py>,
|
||||||
|
Attributes: FromPyObject<'py>,
|
||||||
|
ContextEvent: FromPyObject<'py>,
|
||||||
|
>(
|
||||||
&self,
|
&self,
|
||||||
py: Python<'_>,
|
py: Python<'py>,
|
||||||
entity_id: EntityId,
|
entity_id: EntityId,
|
||||||
) -> PyResult<Option<State<Attributes, ContextEvent>>> {
|
) -> PyResult<Option<StateObject<State, Attributes, ContextEvent>>> {
|
||||||
let args = (entity_id.to_string(),);
|
let args = (entity_id.to_string(),);
|
||||||
let state = self.0.call_method1(py, "get", args)?;
|
let state = self.0.call_method1(py, "get", args)?;
|
||||||
state.extract(py)
|
state.extract(py)
|
139
entrypoint/src/home_assistant/state_object.rs
Normal file
139
entrypoint/src/home_assistant/state_object.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
use super::{
|
||||||
|
event::{context::context::Context, specific::state_changed},
|
||||||
|
home_assistant::HomeAssistant,
|
||||||
|
};
|
||||||
|
use crate::home_assistant::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>>| {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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)?
|
||||||
|
};
|
||||||
|
tracing::debug!(?untrack, "as any");
|
||||||
|
|
||||||
|
let is_callable = untrack.is_callable();
|
||||||
|
tracing::debug!(?is_callable);
|
||||||
|
|
||||||
|
// let untrack = untrack.downcast_into::<PyFunction>()?;
|
||||||
|
// tracing::debug!(?untrack, "as downcast");
|
||||||
|
|
||||||
|
let untrack = untrack.unbind();
|
||||||
|
tracing::debug!(?untrack, "as unbound");
|
||||||
|
|
||||||
|
Ok(untrack)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Ok(untrack) = untrack {
|
||||||
|
tracing::debug!("untrack is ok, going to wait for the next relevant event...");
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
biased;
|
||||||
|
_ = publisher.all_unsubscribed() => {
|
||||||
|
tracing::debug!("calling untrack");
|
||||||
|
let res = Python::with_gil(|py| untrack.call0(py));
|
||||||
|
tracing::debug!(?res);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
new_state = new_state_receiver.recv() => {
|
||||||
|
match new_state {
|
||||||
|
Some(new_state) => {
|
||||||
|
tracing::debug!("publishing new state");
|
||||||
|
publisher.publish(new_state.map(Arc::new))
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
tracing::debug!("channel dropped");
|
||||||
|
break
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::debug!("untrack is err");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((store, task))
|
||||||
|
}
|
||||||
|
}
|
86
entrypoint/src/lib.rs
Normal file
86
entrypoint/src/lib.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use std::{str::FromStr, time::Duration};
|
||||||
|
|
||||||
|
use driver_kasa::connection::LB130USHandle;
|
||||||
|
use home_assistant::{
|
||||||
|
home_assistant::HomeAssistant, light::HomeAssistantLight, object_id::ObjectId,
|
||||||
|
};
|
||||||
|
use protocol::light::Light;
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
use shadow_rs::shadow;
|
||||||
|
use tokio::time::interval;
|
||||||
|
use tracing::{level_filters::LevelFilter, Level};
|
||||||
|
use tracing_subscriber::{
|
||||||
|
fmt::{self, format::FmtSpan},
|
||||||
|
layer::SubscriberExt,
|
||||||
|
registry,
|
||||||
|
util::SubscriberInitExt,
|
||||||
|
Layer,
|
||||||
|
};
|
||||||
|
use tracing_to_home_assistant::TracingToHomeAssistant;
|
||||||
|
|
||||||
|
mod home_assistant;
|
||||||
|
mod python_utils;
|
||||||
|
mod tracing_to_home_assistant;
|
||||||
|
|
||||||
|
shadow!(build_info);
|
||||||
|
|
||||||
|
async fn real_main(home_assistant: HomeAssistant) -> ! {
|
||||||
|
registry()
|
||||||
|
.with(
|
||||||
|
fmt::layer()
|
||||||
|
.pretty()
|
||||||
|
.with_span_events(FmtSpan::ACTIVE)
|
||||||
|
.with_filter(LevelFilter::from_level(Level::TRACE)),
|
||||||
|
)
|
||||||
|
.with(TracingToHomeAssistant)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let built_at = build_info::BUILD_TIME;
|
||||||
|
tracing::info!(built_at);
|
||||||
|
|
||||||
|
// let lamp = HomeAssistantLight {
|
||||||
|
// home_assistant,
|
||||||
|
// object_id: ObjectId::from_str("jacob_s_lamp_top").unwrap(),
|
||||||
|
// };
|
||||||
|
|
||||||
|
let ip = [10, 0, 3, 71];
|
||||||
|
let port = 9999;
|
||||||
|
|
||||||
|
let some_light = LB130USHandle::new(
|
||||||
|
(ip, port).into(),
|
||||||
|
Duration::from_secs(10),
|
||||||
|
(64).try_into().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut interval = interval(Duration::from_secs(20));
|
||||||
|
interval.tick().await;
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
tracing::info!("about to call get_sysinfo");
|
||||||
|
let sysinfo_res = some_light.get_sysinfo().await;
|
||||||
|
tracing::info!(?sysinfo_res, "got sys info");
|
||||||
|
|
||||||
|
// let is_on = lamp.is_on().await;
|
||||||
|
// tracing::info!(?is_on);
|
||||||
|
// let is_off = lamp.is_off().await;
|
||||||
|
// tracing::info!(?is_off);
|
||||||
|
|
||||||
|
// let something = lamp.turn_on().await;
|
||||||
|
// tracing::info!(?something);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, PyAny>> {
|
||||||
|
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async {
|
||||||
|
real_main(home_assistant).await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Python module implemented in Rust.
|
||||||
|
#[pymodule]
|
||||||
|
fn smart_home_in_rust_with_home_assistant(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
|
module.add_function(wrap_pyfunction!(main, module)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
use pyo3::{exceptions::PyTypeError, prelude::*};
|
use pyo3::{exceptions::PyTypeError, prelude::*};
|
||||||
|
|
||||||
/// Create a GIL-independent reference (similar to [`Arc`](std::sync::Arc))
|
/// Create a GIL-independent reference
|
||||||
pub fn detach<T>(bound: &Bound<T>) -> Py<T> {
|
pub fn detach<T>(bound: &Bound<T>) -> Py<T> {
|
||||||
let py = bound.py();
|
let py = bound.py();
|
||||||
bound.as_unbound().clone_ref(py)
|
bound.as_unbound().clone_ref(py)
|
@@ -1,14 +0,0 @@
|
|||||||
use pyo3::prelude::*;
|
|
||||||
|
|
||||||
use super::id::Id;
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
use pyo3::prelude::*;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Id {
|
|
||||||
Ulid(Ulid),
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,37 +0,0 @@
|
|||||||
use pyo3::exceptions::PyValueError;
|
|
||||||
use pyo3::prelude::*;
|
|
||||||
|
|
||||||
use crate::home_assistant::{entity_id::EntityId, state::State};
|
|
||||||
|
|
||||||
#[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<OldAttributes, OldStateContextEvent, NewAttributes, NewStateContextEvent> {
|
|
||||||
pub entity_id: EntityId,
|
|
||||||
pub old_state: Option<State<OldAttributes, OldStateContextEvent>>,
|
|
||||||
pub new_state: Option<State<NewAttributes, NewStateContextEvent>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A state changed event is fired when on state write the state is changed.
|
|
||||||
pub type Event<OldAttributes, OldStateContextEvent, NewAttributes, NewStateContextEvent, Context> =
|
|
||||||
super::super::event::Event<
|
|
||||||
Type,
|
|
||||||
Data<OldAttributes, OldStateContextEvent, NewAttributes, NewStateContextEvent>,
|
|
||||||
Context,
|
|
||||||
>;
|
|
@@ -1,17 +0,0 @@
|
|||||||
use chrono::{DateTime, Utc};
|
|
||||||
use pyo3::prelude::*;
|
|
||||||
|
|
||||||
use crate::home_assistant::entity_id::EntityId;
|
|
||||||
|
|
||||||
use super::event::context::context::Context;
|
|
||||||
|
|
||||||
#[derive(Debug, FromPyObject)]
|
|
||||||
pub struct State<Attributes, ContextEvent> {
|
|
||||||
pub entity_id: EntityId,
|
|
||||||
pub state: String,
|
|
||||||
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>,
|
|
||||||
}
|
|
61
src/lib.rs
61
src/lib.rs
@@ -1,61 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use home_assistant::home_assistant::HomeAssistant;
|
|
||||||
use pyo3::prelude::*;
|
|
||||||
use shadow_rs::shadow;
|
|
||||||
use tokio::time::interval;
|
|
||||||
use tracing::{level_filters::LevelFilter, Level};
|
|
||||||
use tracing_subscriber::{
|
|
||||||
fmt::{self, format::FmtSpan},
|
|
||||||
layer::SubscriberExt,
|
|
||||||
registry,
|
|
||||||
util::SubscriberInitExt,
|
|
||||||
Layer,
|
|
||||||
};
|
|
||||||
use tracing_to_home_assistant::TracingToHomeAssistant;
|
|
||||||
|
|
||||||
mod arbitrary;
|
|
||||||
mod home_assistant;
|
|
||||||
mod python_utils;
|
|
||||||
mod store;
|
|
||||||
mod tracing_to_home_assistant;
|
|
||||||
|
|
||||||
shadow!(build_info);
|
|
||||||
|
|
||||||
async fn real_main(home_assistant: HomeAssistant) -> ! {
|
|
||||||
registry()
|
|
||||||
.with(
|
|
||||||
fmt::layer()
|
|
||||||
.pretty()
|
|
||||||
.with_span_events(FmtSpan::ACTIVE)
|
|
||||||
.with_filter(LevelFilter::from_level(Level::TRACE)),
|
|
||||||
)
|
|
||||||
.with(TracingToHomeAssistant)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let built_at = build_info::BUILD_TIME;
|
|
||||||
tracing::info!(built_at);
|
|
||||||
|
|
||||||
let duration = Duration::from_millis(5900);
|
|
||||||
let mut interval = interval(duration);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let instant = interval.tick().await;
|
|
||||||
|
|
||||||
tracing::debug!(?instant, "it is now");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pyfunction]
|
|
||||||
fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, PyAny>> {
|
|
||||||
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async {
|
|
||||||
real_main(home_assistant).await;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Python module implemented in Rust.
|
|
||||||
#[pymodule]
|
|
||||||
fn smart_home_in_rust_with_home_assistant(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
||||||
m.add_function(wrap_pyfunction!(main, m)?)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
116
src/store/mod.rs
116
src/store/mod.rs
@@ -1,116 +0,0 @@
|
|||||||
use std::future::Future;
|
|
||||||
|
|
||||||
use tokio::{
|
|
||||||
sync::{mpsc, watch},
|
|
||||||
task::{JoinError, JoinHandle},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PublisherStream<T> {
|
|
||||||
receiver: mpsc::Receiver<Publisher<T>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> PublisherStream<T> {
|
|
||||||
pub async fn wait(&mut self) -> Option<Publisher<T>> {
|
|
||||||
self.receiver.recv().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Publisher<T> {
|
|
||||||
sender: watch::Sender<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Publisher<T> {
|
|
||||||
pub async fn all_unsubscribed(&self) {
|
|
||||||
self.sender.closed().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn publish(&self, value: T) {
|
|
||||||
self.sender.send_replace(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Store<T> {
|
|
||||||
sender: watch::Sender<T>,
|
|
||||||
publisher_sender: mpsc::Sender<Publisher<T>>,
|
|
||||||
producer_join_handle: JoinHandle<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Store<T> {
|
|
||||||
pub fn new<Fut: Future<Output = ()> + Send + 'static>(
|
|
||||||
initial: T,
|
|
||||||
producer: impl FnOnce(PublisherStream<T>) -> Fut,
|
|
||||||
) -> Self {
|
|
||||||
let (sender, _) = watch::channel(initial);
|
|
||||||
let (publisher_sender, publisher_receiver) = mpsc::channel(1);
|
|
||||||
|
|
||||||
let subscribers_stream = PublisherStream {
|
|
||||||
receiver: publisher_receiver,
|
|
||||||
};
|
|
||||||
|
|
||||||
let producer_join_handle = tokio::spawn(producer(subscribers_stream));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
publisher_sender,
|
|
||||||
sender,
|
|
||||||
producer_join_handle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe(&self) -> Result<Subscription<T>, ProducerExited> {
|
|
||||||
let receiver = self.sender.subscribe();
|
|
||||||
|
|
||||||
if self.sender.receiver_count() == 1 {
|
|
||||||
if let Err(e) = self.publisher_sender.try_send(Publisher {
|
|
||||||
sender: self.sender.clone(),
|
|
||||||
}) {
|
|
||||||
match e {
|
|
||||||
mpsc::error::TrySendError::Full(_) => unreachable!(),
|
|
||||||
mpsc::error::TrySendError::Closed(_) => return Err(ProducerExited),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Subscription { receiver })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signify that no one can ever subscribe again,
|
|
||||||
/// and wait for the producer task to complete.
|
|
||||||
pub fn run(self) -> impl Future<Output = Result<(), JoinError>> {
|
|
||||||
self.producer_join_handle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Subscription<T> {
|
|
||||||
receiver: watch::Receiver<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct ProducerExited;
|
|
||||||
|
|
||||||
impl<T> Subscription<T> {
|
|
||||||
pub async fn changed(&mut self) -> Result<(), ProducerExited> {
|
|
||||||
self.receiver.changed().await.map_err(|_| ProducerExited)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&mut self) -> T::Owned
|
|
||||||
where
|
|
||||||
T: ToOwned,
|
|
||||||
{
|
|
||||||
self.receiver.borrow_and_update().to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn for_each<Fut: Future<Output = ()>>(mut self, mut func: impl FnMut(T::Owned) -> Fut)
|
|
||||||
where
|
|
||||||
T: ToOwned,
|
|
||||||
{
|
|
||||||
loop {
|
|
||||||
func(self.get()).await;
|
|
||||||
if self.changed().await.is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user