Compare commits
7 Commits
97aef026b2
...
fde59d05ab
| Author | SHA1 | Date | |
|---|---|---|---|
| fde59d05ab | |||
| 48f29ea7d6 | |||
| eff0ad2bf8 | |||
| 2edf095906 | |||
| 8032699013 | |||
| 5cc30c5371 | |||
| fa36b39e81 |
1477
Cargo.lock
generated
1477
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
32
Cargo.toml
32
Cargo.toml
@@ -5,6 +5,7 @@ members = [
|
|||||||
"emitter-and-signal",
|
"emitter-and-signal",
|
||||||
"entrypoint",
|
"entrypoint",
|
||||||
"home-assistant",
|
"home-assistant",
|
||||||
|
"persisted",
|
||||||
"protocol",
|
"protocol",
|
||||||
"python-utils",
|
"python-utils",
|
||||||
]
|
]
|
||||||
@@ -14,17 +15,22 @@ resolver = "2"
|
|||||||
license = "Unlicense"
|
license = "Unlicense"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
backon = "1.5"
|
backon = "1.6"
|
||||||
chrono = "0.4.40"
|
bytes = "1.11"
|
||||||
chrono-tz = "0.10.1"
|
chrono = "0.4.42"
|
||||||
deranged = "0.4"
|
chrono-tz = "0.10.4"
|
||||||
derive_more = "2.0.1"
|
deranged = "0.5"
|
||||||
ext-trait = "2.0.0"
|
derive_more = "2.1.0"
|
||||||
|
ext-trait = "2.0.1"
|
||||||
|
mitsein = "0.8"
|
||||||
palette = "0.7"
|
palette = "0.7"
|
||||||
pyo3 = "0.24.0"
|
pyo3 = "0.27"
|
||||||
pyo3-async-runtimes = "0.24.0"
|
pyo3-async-runtimes = "0.27"
|
||||||
serde = "1.0.219"
|
serde = "1.0.228"
|
||||||
snafu = "0.8.5"
|
snafu = "0.8.9"
|
||||||
strum = "0.27.1"
|
strum = "0.27.2"
|
||||||
tokio = "1.32.0"
|
tokio = "1.48.0"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.43"
|
||||||
|
typed-builder = "0.22"
|
||||||
|
typed-builder-macro = "0.22"
|
||||||
|
url = "2.5"
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ clap = { version = "4", features = ["derive", "env"] }
|
|||||||
deranged = { workspace = true, features = ["serde"] }
|
deranged = { workspace = true, features = ["serde"] }
|
||||||
driver-kasa = { path = "../driver/kasa" }
|
driver-kasa = { path = "../driver/kasa" }
|
||||||
emitter-and-signal = { path = "../emitter-and-signal" }
|
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||||
home-assistant = { path = "../home-assistant" }
|
home-assistant = { path = "../home-assistant", features = ["tracing"] }
|
||||||
im = { version = "15.1.0", features = ["rayon"] }
|
im = { version = "15.1.0", features = ["rayon"] }
|
||||||
|
persisted = { path = "../persisted" }
|
||||||
protocol = { path = "../protocol" }
|
protocol = { path = "../protocol" }
|
||||||
pyo3 = { workspace = true, features = [
|
pyo3 = { workspace = true, features = [
|
||||||
"auto-initialize",
|
"auto-initialize",
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
use std::{path::PathBuf, str::FromStr, time::Duration};
|
use std::{num::NonZeroUsize, path::PathBuf, str::FromStr, time::Duration};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use driver_kasa::connection::LB130USHandle;
|
use driver_kasa::connection::LB130USHandle;
|
||||||
use home_assistant::{
|
use home_assistant::{
|
||||||
home_assistant::HomeAssistant, light::HomeAssistantLight, object_id::ObjectId,
|
event::context::context::Context, home_assistant::HomeAssistant, light::HomeAssistantLight,
|
||||||
|
notify::service::mobile_app::StandardNotification, object_id::ObjectId,
|
||||||
};
|
};
|
||||||
|
use persisted::{persisted, Config, Keyspace, Partition, PartitionCreateOptions};
|
||||||
use protocol::light::{IsOff, IsOn};
|
use protocol::light::{IsOff, IsOn};
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use shadow_rs::shadow;
|
use shadow_rs::shadow;
|
||||||
use tokio::time::interval;
|
use tokio::{task::spawn_blocking, time::interval};
|
||||||
use tracing::{level_filters::LevelFilter, Level};
|
use tracing::{level_filters::LevelFilter, Level};
|
||||||
use tracing_appender::rolling::{self, RollingFileAppender};
|
use tracing_appender::rolling::{self, RollingFileAppender};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
@@ -100,28 +102,49 @@ async fn real_main(
|
|||||||
// object_id: ObjectId::from_str("jacob_s_lamp_side").unwrap(),
|
// object_id: ObjectId::from_str("jacob_s_lamp_side").unwrap(),
|
||||||
// };
|
// };
|
||||||
|
|
||||||
let ip = [10, 0, 3, 71];
|
// let ip = [10, 0, 3, 71];
|
||||||
let port = 9999;
|
// let port = 9999;
|
||||||
|
|
||||||
let some_light = LB130USHandle::new(
|
// let some_light = LB130USHandle::new(
|
||||||
(ip, port).into(),
|
// (ip, port).into(),
|
||||||
Duration::from_secs(10),
|
// Duration::from_secs(10),
|
||||||
(64).try_into().unwrap(),
|
// (64).try_into().unwrap(),
|
||||||
);
|
// );
|
||||||
|
|
||||||
let mut interval = interval(Duration::from_secs(20));
|
let mut int = interval(Duration::from_secs(170));
|
||||||
interval.tick().await;
|
let mut value = 0;
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_secs(15)).await;
|
||||||
|
|
||||||
|
let services = Python::attach(|py| home_assistant.services(py)).unwrap();
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
int.tick().await;
|
||||||
|
|
||||||
tracing::info!("about to call get_sysinfo");
|
tracing::debug!(?value);
|
||||||
let sysinfo_res = some_light.get_sysinfo().await;
|
// let service_result: Result<Py<PyAny>, _> = services
|
||||||
tracing::info!(?sysinfo_res, "got sys info");
|
// .call_service(
|
||||||
|
// StandardNotification::builder()
|
||||||
|
// .object_id(ObjectId("galaxy_s21_ultra_1".parse().unwrap()))
|
||||||
|
// .message(format!("The counter is now {value:?}").parse().unwrap())
|
||||||
|
// .title("New value of the counter".into())
|
||||||
|
// .build(),
|
||||||
|
// Option::<Context<()>>::None,
|
||||||
|
// Option::<()>::None,
|
||||||
|
// false,
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
// tracing::debug!(?service_result);
|
||||||
|
|
||||||
let is_on = some_light.is_on().await;
|
value += 1;
|
||||||
tracing::info!(?is_on);
|
|
||||||
let is_off = some_light.is_off().await;
|
// tracing::info!("about to call get_sysinfo");
|
||||||
tracing::info!(?is_off);
|
// let sysinfo_res = some_light.get_sysinfo().await;
|
||||||
|
// tracing::info!(?sysinfo_res, "got sys info");
|
||||||
|
|
||||||
|
// let is_on = some_light.is_on().await;
|
||||||
|
// tracing::info!(?is_on);
|
||||||
|
// let is_off = some_light.is_off().await;
|
||||||
|
// tracing::info!(?is_off);
|
||||||
|
|
||||||
// let is_on = lamp.is_on().await;
|
// let is_on = lamp.is_on().await;
|
||||||
// tracing::info!(?is_on);
|
// tracing::info!(?is_on);
|
||||||
@@ -131,13 +154,98 @@ async fn real_main(
|
|||||||
// let something = lamp.turn_on().await;
|
// let something = lamp.turn_on().await;
|
||||||
// tracing::info!(?something);
|
// tracing::info!(?something);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(persistence_directory) = persistence_directory {
|
||||||
|
let config = Config::new(persistence_directory);
|
||||||
|
let keyspace = Keyspace::open(config).unwrap(); // TODO: just debugging and experiencing it
|
||||||
|
let partition_name = "trying_this_out_partition";
|
||||||
|
let create_options = PartitionCreateOptions::default();
|
||||||
|
let partition =
|
||||||
|
spawn_blocking(move || keyspace.open_partition(partition_name, create_options))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
// TODO: just debugging and experiencing it
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let identifier = "0a7wmg09awgmagw97nawg7awg90a8wgn982".into();
|
||||||
|
let buffer = NonZeroUsize::new(128).unwrap();
|
||||||
|
let (setter, signal, task) = persisted(partition, identifier, buffer).await;
|
||||||
|
|
||||||
|
let consumer = tokio::spawn({
|
||||||
|
let mut subscription = signal.subscribe().unwrap();
|
||||||
|
|
||||||
|
let services = Python::attach(|py| home_assistant.services(py)).unwrap();
|
||||||
|
async move {
|
||||||
|
let initial = subscription.get();
|
||||||
|
tracing::debug!(?initial);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tracing::info!("waiting for changed");
|
||||||
|
subscription.changed().await.unwrap();
|
||||||
|
|
||||||
|
let value = subscription.get();
|
||||||
|
tracing::debug!(?value);
|
||||||
|
|
||||||
|
// TODO: WIP: DEBUGGING
|
||||||
|
let service_result: Result<Py<PyAny>, _> = services
|
||||||
|
.call_service(
|
||||||
|
StandardNotification::builder()
|
||||||
|
.object_id(ObjectId("galaxy_s21_ultra_1".parse().unwrap()))
|
||||||
|
.message(format!("The counter is now {value:?}").parse().unwrap())
|
||||||
|
.title("New value of the counter".into())
|
||||||
|
.build(),
|
||||||
|
Option::<Context<()>>::None,
|
||||||
|
Option::<()>::None,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tracing::error!(?service_result);
|
||||||
|
let service_result_introspection = Python::attach(|py| {
|
||||||
|
service_result
|
||||||
|
.map(|ret| ret.bind(py).get_type().name().unwrap().to_string())
|
||||||
|
});
|
||||||
|
tracing::error!(?service_result_introspection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let producer = tokio::spawn({
|
||||||
|
let mut subscription = signal.subscribe().unwrap();
|
||||||
|
async move {
|
||||||
|
let mut int = interval(Duration::from_secs(190));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
int.tick().await;
|
||||||
|
|
||||||
|
let value = subscription.get();
|
||||||
|
tracing::debug!(?value);
|
||||||
|
|
||||||
|
// let mut counter = value.unwrap_or(14u16);
|
||||||
|
// tracing::info!(?counter);
|
||||||
|
|
||||||
|
if let Ok(counter) = value {
|
||||||
|
let counter: u16 = counter + 1;
|
||||||
|
setter.set(counter).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
producer.await.unwrap();
|
||||||
|
consumer.await.unwrap();
|
||||||
|
task.await.unwrap();
|
||||||
|
|
||||||
|
unreachable!();
|
||||||
|
} else {
|
||||||
|
panic!("please set PERSISTENCE_DIRECTORY while debugging and trying persisted out")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, PyAny>> {
|
fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, PyAny>> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async move {
|
||||||
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async {
|
|
||||||
real_main(args, home_assistant).await;
|
real_main(args, home_assistant).await;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ impl<S: Subscriber> Layer<S> for TracingToHomeAssistant {
|
|||||||
|
|
||||||
let log_data: Option<LogData<()>> = None;
|
let log_data: Option<LogData<()>> = None;
|
||||||
|
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let Ok(hass_logger) = HassLogger::new(py, target) else {
|
let Ok(hass_logger) = HassLogger::new(py, target) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,14 +19,17 @@ derive_more = { workspace = true, features = [
|
|||||||
"try_into",
|
"try_into",
|
||||||
] }
|
] }
|
||||||
emitter-and-signal = { path = "../emitter-and-signal" }
|
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||||
|
mitsein = { workspace = true }
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
protocol = { path = "../protocol" }
|
protocol = { path = "../protocol" }
|
||||||
pyo3 = { workspace = true }
|
pyo3 = { workspace = true }
|
||||||
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
|
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
|
||||||
python-utils = { path = "../python-utils" }
|
python-utils = { path = "../python-utils" }
|
||||||
smol_str = "0.3.2"
|
|
||||||
snafu = { workspace = true }
|
snafu = { workspace = true }
|
||||||
strum = { workspace = true, features = ["derive"] }
|
strum = { workspace = true, features = ["derive"] }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { optional = true, workspace = true }
|
tracing = { optional = true, workspace = true }
|
||||||
|
typed-builder = { workspace = true }
|
||||||
ulid = "1.2.0"
|
ulid = "1.2.0"
|
||||||
|
uom = "0.37.0"
|
||||||
|
url = { workspace = true }
|
||||||
|
|||||||
0
home-assistant/src/binary_sensor/mod.rs
Normal file
0
home-assistant/src/binary_sensor/mod.rs
Normal file
@@ -1,6 +1,10 @@
|
|||||||
use std::{convert::Infallible, fmt::Display, str::FromStr};
|
use std::{convert::Infallible, fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use pyo3::{exceptions::PyValueError, prelude::*, types::PyString};
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
types::PyString,
|
||||||
|
};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -44,21 +48,39 @@ impl Display for EntityId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<EntityIdParsingError> for PyErr {
|
#[derive(Debug, Snafu)]
|
||||||
fn from(error: EntityIdParsingError) -> Self {
|
pub enum ExtractEntityIdError {
|
||||||
PyValueError::new_err(error.to_string())
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
|
||||||
|
/// couldn't parse the string as an [`EntityId`]
|
||||||
|
ParseError { source: EntityIdParsingError },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractEntityIdError> for PyErr {
|
||||||
|
fn from(error: ExtractEntityIdError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractEntityIdError::ExtractStringError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
ExtractEntityIdError::ParseError { .. } => PyValueError::new_err(error.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'py> FromPyObject<'py> for EntityId {
|
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
impl<'a, 'py> FromPyObject<'a, 'py> for EntityId {
|
||||||
let s = ob.extract()?;
|
type Error = ExtractEntityIdError;
|
||||||
let entity_id = EntityId::from_str(s)?;
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = ob.extract().context(ExtractStringSnafu)?;
|
||||||
|
let entity_id = EntityId::from_str(s).context(ParseSnafu)?;
|
||||||
|
|
||||||
Ok(entity_id)
|
Ok(entity_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
||||||
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>;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl<'py, Event: IntoPyObject<'py>> IntoPyObject<'py> for Context<Event> {
|
|||||||
.bind(py);
|
.bind(py);
|
||||||
|
|
||||||
let context_class = homeassistant_core.getattr("Context")?;
|
let context_class = homeassistant_core.getattr("Context")?;
|
||||||
let context_class = context_class.downcast_into::<PyType>()?;
|
let context_class = context_class.cast_into::<PyType>()?;
|
||||||
|
|
||||||
let context_instance = context_class.call1((self.user_id, self.parent_id, self.id))?;
|
let context_instance = context_class.call1((self.user_id, self.parent_id, self.id))?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
use std::convert::Infallible;
|
use std::{convert::Infallible, sync::Arc};
|
||||||
|
|
||||||
use pyo3::{prelude::*, types::PyString};
|
use pyo3::{exceptions::PyTypeError, prelude::*, types::PyString};
|
||||||
use smol_str::SmolStr;
|
use snafu::{ResultExt, Snafu};
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Id {
|
pub enum Id {
|
||||||
Ulid(Ulid),
|
Ulid(Ulid),
|
||||||
Other(SmolStr),
|
Other(Arc<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'py> FromPyObject<'py> for Id {
|
#[derive(Debug, Snafu)]
|
||||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
pub enum ExtractIdError {
|
||||||
let s = ob.extract::<String>()?;
|
/// couldn't extract the given object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractIdError> for PyErr {
|
||||||
|
fn from(error: ExtractIdError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractIdError::ExtractStringError { .. } => PyTypeError::new_err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for Id {
|
||||||
|
type Error = ExtractIdError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
|
||||||
|
|
||||||
if let Ok(ulid) = s.parse() {
|
if let Ok(ulid) = s.parse() {
|
||||||
Ok(Id::Ulid(ulid))
|
Ok(Id::Ulid(ulid))
|
||||||
@@ -22,6 +39,7 @@ impl<'py> FromPyObject<'py> for Id {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
||||||
impl<'py> IntoPyObject<'py> for Id {
|
impl<'py> IntoPyObject<'py> for Id {
|
||||||
type Target = PyString;
|
type Target = PyString;
|
||||||
|
|
||||||
@@ -32,7 +50,7 @@ impl<'py> IntoPyObject<'py> for Id {
|
|||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||||
match self {
|
match self {
|
||||||
Id::Ulid(ulid) => ulid.to_string().into_pyobject(py),
|
Id::Ulid(ulid) => ulid.to_string().into_pyobject(py),
|
||||||
Id::Other(id) => id.as_str().into_pyobject(py),
|
Id::Other(id) => id.into_pyobject(py),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use pyo3::{exceptions::PyValueError, prelude::*};
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyTypeError, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
#[derive(Debug, Clone, strum::EnumString, strum::Display)]
|
#[derive(Debug, Clone, strum::EnumString, strum::Display)]
|
||||||
#[strum(serialize_all = "UPPERCASE")]
|
#[strum(serialize_all = "UPPERCASE")]
|
||||||
@@ -9,12 +13,41 @@ pub enum EventOrigin {
|
|||||||
Remote,
|
Remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'py> FromPyObject<'py> for EventOrigin {
|
#[derive(Debug, Snafu)]
|
||||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
pub enum ExtractEventOriginError {
|
||||||
let s = ob.str()?;
|
/// couldn't turn the object into a string with `str` (Python function)
|
||||||
let s = s.extract()?;
|
ToStrError { source: PyErr },
|
||||||
let event_origin =
|
|
||||||
EventOrigin::from_str(s).map_err(|err| PyValueError::new_err(err.to_string()))?;
|
/// after calling `str` on the object, it's somehow not extractable as a string?!
|
||||||
|
NotString { source: PyErr },
|
||||||
|
|
||||||
|
/// this is not an expected value for [`EventOrigin`]
|
||||||
|
UnexpectedValue {
|
||||||
|
source: <EventOrigin as FromStr>::Err,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractEventOriginError> for PyErr {
|
||||||
|
fn from(error: ExtractEventOriginError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractEventOriginError::ToStrError { .. } => PyException::new_err(error.to_string()),
|
||||||
|
ExtractEventOriginError::NotString { .. } => PyTypeError::new_err(error.to_string()),
|
||||||
|
ExtractEventOriginError::UnexpectedValue { .. } => {
|
||||||
|
PyValueError::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for EventOrigin {
|
||||||
|
type Error = ExtractEventOriginError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = ob.str().context(ToStrSnafu)?;
|
||||||
|
// TODO: could I go straight to trying to extract an &str without calling .str() first? if so then I could
|
||||||
|
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
|
let s = s.extract().context(NotStringSnafu)?;
|
||||||
|
let event_origin = EventOrigin::from_str(s).context(UnexpectedValueSnafu)?;
|
||||||
|
|
||||||
Ok(event_origin)
|
Ok(event_origin)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,41 @@
|
|||||||
use pyo3::exceptions::PyValueError;
|
use pyo3::exceptions::{PyTypeError, PyValueError};
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
use crate::{entity_id::EntityId, state_object::StateObject};
|
use crate::{entity_id::EntityId, state_object::StateObject};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Type;
|
pub struct Type;
|
||||||
|
|
||||||
impl<'py> FromPyObject<'py> for Type {
|
#[derive(Debug, Snafu)]
|
||||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
pub enum ExtractTypeError {
|
||||||
let s = ob.extract::<&str>()?;
|
/// couldn't extract this object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
|
||||||
|
/// expected a string of value "state_changed", but got {actual}
|
||||||
|
UnexpectedValue { actual: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractTypeError> for PyErr {
|
||||||
|
fn from(error: ExtractTypeError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractTypeError::ExtractStringError { .. } => PyTypeError::new_err(error.to_string()),
|
||||||
|
ExtractTypeError::UnexpectedValue { .. } => PyValueError::new_err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStrLiteral) / #[literal = "state_changed"] once I learn how to make something like that and see about serde or strum integration or inspiration
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for Type {
|
||||||
|
type Error = ExtractTypeError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
|
||||||
|
|
||||||
if s == "state_changed" {
|
if s == "state_changed" {
|
||||||
Ok(Type)
|
Ok(Type)
|
||||||
} else {
|
} else {
|
||||||
Err(PyValueError::new_err(format!(
|
Err(ExtractTypeError::UnexpectedValue { actual: s.into() })
|
||||||
"expected a string of value 'state_changed', but got {s}"
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,20 @@ use std::convert::Infallible;
|
|||||||
|
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
use python_utils::{detach, validate_type_by_name};
|
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
use super::{service_registry::ServiceRegistry, 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>);
|
||||||
|
|
||||||
impl<'source> FromPyObject<'source> for HomeAssistant {
|
impl<'a, 'py> FromPyObject<'a, 'py> for HomeAssistant {
|
||||||
fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult<Self> {
|
type Error = TypeByNameValidationError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
// region: Validation
|
// region: Validation
|
||||||
validate_type_by_name(ob, "HomeAssistant")?;
|
validate_type_by_name(&ob, "HomeAssistant")?;
|
||||||
// endregion: Validation
|
// endregion: Validation
|
||||||
|
|
||||||
Ok(Self(detach(ob)))
|
Ok(Self(detach(ob)))
|
||||||
@@ -29,6 +32,24 @@ impl<'py> IntoPyObject<'py> for &HomeAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum GetStatesError {
|
||||||
|
/// couldn't get the `states` attribute on the Home Assistant object
|
||||||
|
GetStatesAttributeError { source: PyErr },
|
||||||
|
|
||||||
|
/// couldn't extract the `states` as a [`StateMachine`]
|
||||||
|
ExtractStateMachineError { source: TypeByNameValidationError },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum GetServicesError {
|
||||||
|
/// couldn't get the `services` attribute on the Home Assistant object
|
||||||
|
GetServicesAttributeError { source: PyErr },
|
||||||
|
|
||||||
|
/// couldn't extract the `states` as a [`ServiceRegistry`]
|
||||||
|
ExtractServiceRegistryError { source: TypeByNameValidationError },
|
||||||
|
}
|
||||||
|
|
||||||
impl HomeAssistant {
|
impl HomeAssistant {
|
||||||
/// Return the representation
|
/// Return the representation
|
||||||
pub fn repr(&self, py: Python<'_>) -> Result<String, PyErr> {
|
pub fn repr(&self, py: Python<'_>) -> Result<String, PyErr> {
|
||||||
@@ -48,13 +69,19 @@ impl HomeAssistant {
|
|||||||
is_stopping.extract(py)
|
is_stopping.extract(py)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn states(&self, py: Python<'_>) -> Result<StateMachine, PyErr> {
|
pub fn states(&self, py: Python<'_>) -> Result<StateMachine, GetStatesError> {
|
||||||
let states = self.0.getattr(py, "states")?;
|
let states = self
|
||||||
states.extract(py)
|
.0
|
||||||
|
.getattr(py, "states")
|
||||||
|
.context(GetStatesAttributeSnafu)?;
|
||||||
|
states.extract(py).context(ExtractStateMachineSnafu)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, PyErr> {
|
pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, GetServicesError> {
|
||||||
let services = self.0.getattr(py, "services")?;
|
let services = self
|
||||||
services.extract(py)
|
.0
|
||||||
|
.getattr(py, "services")
|
||||||
|
.context(GetServicesAttributeSnafu)?;
|
||||||
|
services.extract(py).context(ExtractServiceRegistrySnafu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
home-assistant/src/input_boolean/mod.rs
Normal file
0
home-assistant/src/input_boolean/mod.rs
Normal file
0
home-assistant/src/input_number/mod.rs
Normal file
0
home-assistant/src/input_number/mod.rs
Normal file
0
home-assistant/src/input_text/mod.rs
Normal file
0
home-assistant/src/input_text/mod.rs
Normal file
@@ -1,13 +1,21 @@
|
|||||||
|
pub mod binary_sensor;
|
||||||
pub mod domain;
|
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 input_boolean;
|
||||||
|
pub mod input_number;
|
||||||
|
pub mod input_text;
|
||||||
pub mod light;
|
pub mod light;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
|
pub mod notify;
|
||||||
pub mod object_id;
|
pub mod object_id;
|
||||||
|
pub mod sensor;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod service_registry;
|
pub mod service_registry;
|
||||||
pub mod slug;
|
pub mod slug;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod state_machine;
|
pub mod state_machine;
|
||||||
pub mod state_object;
|
pub mod state_object;
|
||||||
|
pub mod switch;
|
||||||
|
pub mod unit_of_measurement;
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ use pyo3::prelude::*;
|
|||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
use state::LightState;
|
use state::LightState;
|
||||||
|
|
||||||
use crate::state::HomeAssistantState;
|
use crate::{
|
||||||
|
home_assistant::GetStatesError, state::HomeAssistantState, state_machine::GetStateError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
domain::Domain, entity_id::EntityId, home_assistant::HomeAssistant, object_id::ObjectId,
|
domain::Domain, entity_id::EntityId, home_assistant::HomeAssistant, object_id::ObjectId,
|
||||||
@@ -29,7 +31,15 @@ impl HomeAssistantLight {
|
|||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
pub enum GetStateObjectError {
|
pub enum GetStateObjectError {
|
||||||
PythonError { source: PyErr },
|
/// couldn't get the state machine registry
|
||||||
|
GetStatesError { source: GetStatesError },
|
||||||
|
|
||||||
|
/// this state object exists in the state machine registry, but it couldn't be extracted as a light state object
|
||||||
|
GetStateError { source: GetStateError<
|
||||||
|
<StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>> as FromPyObject<'static, 'static>>::Error
|
||||||
|
> },
|
||||||
|
|
||||||
|
/// this entity does not have a state object in the registry
|
||||||
EntityMissing,
|
EntityMissing,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,12 +50,12 @@ impl HomeAssistantLight {
|
|||||||
StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>>,
|
StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>>,
|
||||||
GetStateObjectError,
|
GetStateObjectError,
|
||||||
> {
|
> {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let states = self.home_assistant.states(py).context(PythonSnafu)?;
|
let states = self.home_assistant.states(py).context(GetStatesSnafu)?;
|
||||||
let entity_id = self.entity_id();
|
let entity_id = self.entity_id();
|
||||||
let state_object = states
|
let state_object = states
|
||||||
.get(py, entity_id)
|
.get(py, entity_id)
|
||||||
.context(PythonSnafu)?
|
.context(GetStateSnafu)?
|
||||||
.ok_or(GetStateObjectError::EntityMissing)?;
|
.ok_or(GetStateObjectError::EntityMissing)?;
|
||||||
|
|
||||||
Ok(state_object)
|
Ok(state_object)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::service::{turn_off::TurnOff, turn_on::TurnOn};
|
use super::service::{turn_off::TurnOff, turn_on::TurnOn};
|
||||||
use super::{state::LightState, GetStateObjectError, HomeAssistantLight};
|
use super::{GetStateObjectError, HomeAssistantLight};
|
||||||
|
use crate::home_assistant::GetServicesError;
|
||||||
|
use crate::service_registry::CallServiceError;
|
||||||
use crate::{
|
use crate::{
|
||||||
event::context::context::Context,
|
event::context::context::Context,
|
||||||
state::{ErrorState, HomeAssistantState, UnexpectedState},
|
state::{ErrorState, HomeAssistantState, UnexpectedState},
|
||||||
@@ -35,21 +37,31 @@ impl GetState for HomeAssistantLight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum SetStateError {
|
||||||
|
/// couldn't get the service registry
|
||||||
|
GetServicesError { source: GetServicesError },
|
||||||
|
|
||||||
|
/// couldn't call the service
|
||||||
|
CallServiceError { source: CallServiceError },
|
||||||
|
}
|
||||||
|
|
||||||
impl SetState for HomeAssistantLight {
|
impl SetState for HomeAssistantLight {
|
||||||
type Error = PyErr;
|
type Error = SetStateError;
|
||||||
|
|
||||||
async fn set_state(&mut self, state: protocol::light::State) -> Result<(), Self::Error> {
|
async fn set_state(&mut self, state: protocol::light::State) -> Result<(), Self::Error> {
|
||||||
let context: Option<Context<()>> = None;
|
let context: Option<Context<()>> = None;
|
||||||
let target: Option<()> = None;
|
let target: Option<()> = None;
|
||||||
|
|
||||||
let services = Python::with_gil(|py| self.home_assistant.services(py))?;
|
let services =
|
||||||
|
Python::attach(|py| self.home_assistant.services(py)).context(GetServicesSnafu)?;
|
||||||
|
|
||||||
let _: IsNone = match state {
|
let _: IsNone = match state {
|
||||||
protocol::light::State::Off => {
|
protocol::light::State::Off => {
|
||||||
services
|
services
|
||||||
.call_service(
|
.call_service(
|
||||||
TurnOff {
|
TurnOff {
|
||||||
entity_id: self.entity_id(),
|
object_id: self.object_id.clone(),
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
target,
|
target,
|
||||||
@@ -61,7 +73,7 @@ impl SetState for HomeAssistantLight {
|
|||||||
services
|
services
|
||||||
.call_service(
|
.call_service(
|
||||||
TurnOn {
|
TurnOn {
|
||||||
entity_id: self.entity_id(),
|
object_id: self.object_id.clone(),
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
target,
|
target,
|
||||||
@@ -69,7 +81,8 @@ impl SetState for HomeAssistantLight {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}?;
|
}
|
||||||
|
.context(CallServiceSnafu)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ use std::str::FromStr;
|
|||||||
use pyo3::IntoPyObject;
|
use pyo3::IntoPyObject;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
entity_id::EntityId,
|
domain::Domain, entity_id::EntityId, object_id::ObjectId, service::{IntoServiceCall, service_domain::ServiceDomain, service_id::ServiceId}
|
||||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TurnOff {
|
pub struct TurnOff {
|
||||||
pub entity_id: EntityId,
|
pub object_id: ObjectId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, IntoPyObject)]
|
#[derive(Debug, Clone, IntoPyObject)]
|
||||||
@@ -24,7 +23,8 @@ impl IntoServiceCall for TurnOff {
|
|||||||
let service_domain = ServiceDomain::from_str("light").expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
|
let service_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 service_id = ServiceId::from_str("turn_off").expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
|
||||||
|
|
||||||
let Self { entity_id } = self;
|
let Self { object_id } = self;
|
||||||
|
let entity_id = EntityId(Domain::Light, object_id);
|
||||||
|
|
||||||
let service_data = TurnOffServiceData { entity_id };
|
let service_data = TurnOffServiceData { entity_id };
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ use std::str::FromStr;
|
|||||||
use pyo3::IntoPyObject;
|
use pyo3::IntoPyObject;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
entity_id::EntityId,
|
domain::Domain, entity_id::EntityId, object_id::ObjectId, service::{IntoServiceCall, service_domain::ServiceDomain, service_id::ServiceId}
|
||||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TurnOn {
|
pub struct TurnOn {
|
||||||
pub entity_id: EntityId,
|
pub object_id: ObjectId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, IntoPyObject)]
|
#[derive(Debug, Clone, IntoPyObject)]
|
||||||
@@ -24,7 +23,9 @@ impl IntoServiceCall for TurnOn {
|
|||||||
let service_domain = ServiceDomain::from_str("light").expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
|
let service_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 service_id = ServiceId::from_str("turn_on").expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
|
||||||
|
|
||||||
let Self { entity_id } = self;
|
let Self { object_id } = self;
|
||||||
|
let entity_id = EntityId(Domain::Light, object_id);
|
||||||
|
|
||||||
let service_data = TurnOnServiceData { entity_id };
|
let service_data = TurnOnServiceData { entity_id };
|
||||||
|
|
||||||
(service_domain, service_id, service_data)
|
(service_domain, service_id, service_data)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use pyo3::{exceptions::PyValueError, prelude::*};
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
#[derive(Debug, Clone, EnumString, strum::Display)]
|
#[derive(Debug, Clone, EnumString, strum::Display)]
|
||||||
@@ -10,12 +14,36 @@ pub enum LightState {
|
|||||||
Off,
|
Off,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'py> FromPyObject<'py> for LightState {
|
#[derive(Debug, Snafu)]
|
||||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
pub enum ExtractLightStateError {
|
||||||
let s = ob.extract::<String>()?;
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
|
||||||
let state =
|
/// couldn't parse the string as a [`LightState`]
|
||||||
LightState::from_str(&s).map_err(|err| PyValueError::new_err(err.to_string()))?;
|
ParseError {
|
||||||
|
source: <LightState as FromStr>::Err,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractLightStateError> for PyErr {
|
||||||
|
fn from(error: ExtractLightStateError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractLightStateError::ExtractStringError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
ExtractLightStateError::ParseError { .. } => PyValueError::new_err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for LightState {
|
||||||
|
type Error = ExtractLightStateError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
|
||||||
|
|
||||||
|
let state = LightState::from_str(&s).context(ParseSnafu)?;
|
||||||
|
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
use arbitrary_value::{arbitrary::Arbitrary, map::Map};
|
use arbitrary_value::{arbitrary::Arbitrary, map::Map};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use pyo3::{prelude::*, types::PyTuple};
|
use pyo3::{prelude::*, types::PyTuple};
|
||||||
use python_utils::{detach, validate_type_by_name};
|
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HassLogger(Py<PyAny>);
|
pub struct HassLogger(Py<PyAny>);
|
||||||
|
|
||||||
impl<'source> FromPyObject<'source> for HassLogger {
|
impl<'a, 'py> FromPyObject<'a, 'py> for HassLogger {
|
||||||
fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult<Self> {
|
type Error = TypeByNameValidationError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
// region: Validation
|
// region: Validation
|
||||||
validate_type_by_name(ob, "HassLogger")?;
|
validate_type_by_name(&ob, "HassLogger")?;
|
||||||
// endregion: Validation
|
// endregion: Validation
|
||||||
|
|
||||||
Ok(Self(detach(ob)))
|
Ok(Self(detach(ob)))
|
||||||
|
|||||||
1
home-assistant/src/notify/mod.rs
Normal file
1
home-assistant/src/notify/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod service;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod request_location_update;
|
||||||
|
|
||||||
|
pub use request_location_update::RequestLocationUpdate;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
notify::service::mobile_app::SpecialMessage,
|
||||||
|
object_id::ObjectId,
|
||||||
|
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::NotifyMobileAppServiceData;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
||||||
|
pub struct RequestLocationUpdate {
|
||||||
|
pub object_id: ObjectId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoServiceCall for RequestLocationUpdate {
|
||||||
|
type ServiceData = NotifyMobileAppServiceData;
|
||||||
|
|
||||||
|
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||||
|
let RequestLocationUpdate { object_id } = self;
|
||||||
|
|
||||||
|
let service_domain = ServiceDomain::from_str("notify").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(&format!("mobile_app_{object_id}")).expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
|
||||||
|
|
||||||
|
let service_data = NotifyMobileAppServiceData::builder()
|
||||||
|
.message(SpecialMessage::RequestLocationUpdate.to_string())
|
||||||
|
// .data(NotifyMobileAppServiceDataData::builder().build()) // TODO
|
||||||
|
.build();
|
||||||
|
|
||||||
|
(service_domain, service_id, service_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
223
home-assistant/src/notify/service/mobile_app/mod.rs
Normal file
223
home-assistant/src/notify/service/mobile_app/mod.rs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
use std::{convert::Infallible, str::FromStr};
|
||||||
|
|
||||||
|
use pyo3::{types::PyString, Bound, IntoPyObject, Python};
|
||||||
|
use python_utils::IntoPyObjectViaDisplay;
|
||||||
|
use snafu::Snafu;
|
||||||
|
use strum::EnumString;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub mod command;
|
||||||
|
pub mod standard;
|
||||||
|
pub mod text_to_speech;
|
||||||
|
|
||||||
|
pub use command::*;
|
||||||
|
pub use standard::StandardNotification;
|
||||||
|
pub use text_to_speech::TextToSpeech;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::Display)]
|
||||||
|
pub struct NonSpecialMessage(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, EnumString, strum::Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum SpecialMessage {
|
||||||
|
ClearBadge,
|
||||||
|
ClearNotification,
|
||||||
|
CommandActivity,
|
||||||
|
CommandAppLock,
|
||||||
|
CommandAutoScreenBrightness,
|
||||||
|
CommandBluetooth,
|
||||||
|
CommandBleTransmitter,
|
||||||
|
CommandBeaconMonitor,
|
||||||
|
CommandBroadcastIntent,
|
||||||
|
CommandDnd,
|
||||||
|
CommandFlashlight,
|
||||||
|
CommandHighAccuracyMode,
|
||||||
|
CommandLaunchApp,
|
||||||
|
CommandMedia,
|
||||||
|
CommandRingerMode,
|
||||||
|
CommandScreenBrightnessLevel,
|
||||||
|
CommandScreenOffTimeout,
|
||||||
|
CommandScreenOn,
|
||||||
|
CommandStopTts,
|
||||||
|
CommandPersistentConnection,
|
||||||
|
CommandUpdateSensors,
|
||||||
|
CommandVolumeLevel,
|
||||||
|
CommandWebivew,
|
||||||
|
RemoveChannel,
|
||||||
|
RequestLocationUpdate,
|
||||||
|
// #[strum(serialize = "TTS")] TODO: WIP: TESTING
|
||||||
|
Tts,
|
||||||
|
UpdateComplications,
|
||||||
|
UpdateWidgets,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// wasn't supposed to get a specially-behaving message here, but got {got}
|
||||||
|
#[derive(Debug, Clone, Snafu)]
|
||||||
|
pub struct WasSpecialMessage {
|
||||||
|
pub got: SpecialMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NonSpecialMessage {
|
||||||
|
type Err = WasSpecialMessage;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match SpecialMessage::from_str(s) {
|
||||||
|
Ok(special_message) => Err(WasSpecialMessage {
|
||||||
|
got: special_message,
|
||||||
|
}),
|
||||||
|
Err(_e) => Ok(NonSpecialMessage(s.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much of a notification is visible on the lock screen
|
||||||
|
#[derive(Debug, Clone, Default, EnumString, strum::Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum Visibility {
|
||||||
|
/// always show all notification content
|
||||||
|
Public,
|
||||||
|
/// visibility depends on your setting in the system Settings app > Notifications;
|
||||||
|
/// if the option to show sensitive notifications when locked is enabled all notification content will be shown,
|
||||||
|
/// otherwise only basic information such as the icon and app name are visible
|
||||||
|
#[default]
|
||||||
|
Private,
|
||||||
|
/// always hide notification from lock screen
|
||||||
|
Secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
||||||
|
impl<'py> IntoPyObject<'py> for Visibility {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, EnumString, strum::Display)]
|
||||||
|
pub enum Behavior {
|
||||||
|
/// prompt for text to return with the event
|
||||||
|
#[strum(serialize = "textInput")]
|
||||||
|
TextInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
||||||
|
impl<'py> IntoPyObject<'py> for Behavior {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, EnumString, strum::Display)]
|
||||||
|
#[strum(serialize_all = "camelCase")]
|
||||||
|
pub enum ActivationMode {
|
||||||
|
/// launch the app when tapped
|
||||||
|
Foreground,
|
||||||
|
/// just fires the event
|
||||||
|
#[default]
|
||||||
|
Background,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
||||||
|
impl<'py> IntoPyObject<'py> for ActivationMode {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: better typed versions like `CallNumber` or `OpenWebpage` where Action: From<CallNumber> and Action: From<OpenWebPage>
|
||||||
|
#[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)]
|
||||||
|
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
|
||||||
|
pub struct Action {
|
||||||
|
// TODO: proper type
|
||||||
|
// TODO: I wish I could call this identifier or something instead
|
||||||
|
#[builder(!default, setter(!strip_option))]
|
||||||
|
pub action: String,
|
||||||
|
#[builder(!default, setter(!strip_option))]
|
||||||
|
pub title: String,
|
||||||
|
|
||||||
|
pub uri: Option<IntoPyObjectViaDisplay<Url>>,
|
||||||
|
pub behavior: Option<Behavior>,
|
||||||
|
|
||||||
|
// TODO: make this written as activationMode
|
||||||
|
/// (iOS only) decide whether to open the app (to the foreground) when tapped
|
||||||
|
/// or merely fire an event (in the background)
|
||||||
|
pub activation_mode: Option<ActivationMode>,
|
||||||
|
|
||||||
|
// TODO: make this written as authenticationRequired
|
||||||
|
/// (iOS only) require entering a passcode to use the action
|
||||||
|
pub authentication_required: Option<bool>,
|
||||||
|
|
||||||
|
/// (iOS only) color the action's title red, indicating a destructive action
|
||||||
|
pub destructive: Option<bool>,
|
||||||
|
|
||||||
|
// TODO: make this written as textInputButtonTitle
|
||||||
|
/// (iOS only) Title to use for text input for actions that prompt
|
||||||
|
pub text_input_button_title: Option<String>,
|
||||||
|
|
||||||
|
// TODO: make this written as textInputPlaceholder
|
||||||
|
/// (iOS only) Placeholder to use for text input for actions that prompt
|
||||||
|
pub text_input_placeholder: Option<String>,
|
||||||
|
|
||||||
|
// TODO: proper type
|
||||||
|
/// (iOS only) icon to use for the notification
|
||||||
|
pub icon: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, EnumString, strum::Display)]
|
||||||
|
#[strum(serialize_all = "snake_case", suffix = "_stream")]
|
||||||
|
pub enum MediaStream {
|
||||||
|
Alarm,
|
||||||
|
Call,
|
||||||
|
Dtmf,
|
||||||
|
#[default]
|
||||||
|
Music,
|
||||||
|
Notification,
|
||||||
|
Ring,
|
||||||
|
System,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
||||||
|
impl<'py> IntoPyObject<'py> for MediaStream {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, IntoPyObject, typed_builder::TypedBuilder)]
|
||||||
|
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
|
||||||
|
pub struct NotifyMobileAppServiceDataData {
|
||||||
|
actions: Option<Vec<Action>>,
|
||||||
|
media_stream: Option<MediaStream>,
|
||||||
|
tts_text: Option<String>,
|
||||||
|
visibility: Option<Visibility>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)]
|
||||||
|
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
|
||||||
|
pub struct NotifyMobileAppServiceData {
|
||||||
|
#[builder(!default, setter(!strip_option))]
|
||||||
|
message: String,
|
||||||
|
|
||||||
|
title: Option<String>,
|
||||||
|
#[builder(setter(!strip_option))]
|
||||||
|
data: NotifyMobileAppServiceDataData,
|
||||||
|
}
|
||||||
60
home-assistant/src/notify/service/mobile_app/standard.rs
Normal file
60
home-assistant/src/notify/service/mobile_app/standard.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use mitsein::vec1::Vec1;
|
||||||
|
use pyo3::{types::PyAnyMethods, IntoPyObject, Python};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
object_id::ObjectId,
|
||||||
|
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
Action, NonSpecialMessage, NotifyMobileAppServiceData, NotifyMobileAppServiceDataData,
|
||||||
|
Visibility,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
||||||
|
pub struct StandardNotification {
|
||||||
|
pub object_id: ObjectId,
|
||||||
|
|
||||||
|
#[builder(default, setter(strip_option))]
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub message: NonSpecialMessage,
|
||||||
|
|
||||||
|
#[builder(default, setter(strip_option))]
|
||||||
|
pub actions: Option<Vec1<Action>>,
|
||||||
|
|
||||||
|
#[builder(default, setter(strip_option))]
|
||||||
|
pub visibility: Option<Visibility>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoServiceCall for StandardNotification {
|
||||||
|
type ServiceData = NotifyMobileAppServiceData;
|
||||||
|
|
||||||
|
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||||
|
let StandardNotification {
|
||||||
|
object_id,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
actions,
|
||||||
|
visibility,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let service_domain = ServiceDomain::from_str("notify").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(&format!("mobile_app_{object_id}")).expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
|
||||||
|
|
||||||
|
let service_data = NotifyMobileAppServiceData::builder()
|
||||||
|
.title_option(title)
|
||||||
|
.message(message.to_string())
|
||||||
|
.data(
|
||||||
|
NotifyMobileAppServiceDataData::builder()
|
||||||
|
.actions_option(actions.map(Into::into))
|
||||||
|
.visibility_option(visibility)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
(service_domain, service_id, service_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
notify::service::mobile_app::SpecialMessage,
|
||||||
|
object_id::ObjectId,
|
||||||
|
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{MediaStream, NotifyMobileAppServiceData, NotifyMobileAppServiceDataData};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
||||||
|
pub struct TextToSpeech {
|
||||||
|
pub object_id: ObjectId,
|
||||||
|
|
||||||
|
pub message: String,
|
||||||
|
pub media_stream: Option<MediaStream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoServiceCall for TextToSpeech {
|
||||||
|
type ServiceData = NotifyMobileAppServiceData;
|
||||||
|
|
||||||
|
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||||
|
let TextToSpeech {
|
||||||
|
object_id,
|
||||||
|
message,
|
||||||
|
media_stream,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let service_domain = ServiceDomain::from_str("notify").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(&format!("mobile_app_{object_id}")).expect("statically written and known to be a valid slug; hoping to get compiler checks instead in the future");
|
||||||
|
|
||||||
|
let service_data = NotifyMobileAppServiceData::builder()
|
||||||
|
.message(SpecialMessage::Tts.to_string())
|
||||||
|
.data(
|
||||||
|
NotifyMobileAppServiceDataData::builder()
|
||||||
|
.tts_text(message)
|
||||||
|
.media_stream_option(media_stream)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
(service_domain, service_id, service_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
home-assistant/src/notify/service/mod.rs
Normal file
1
home-assistant/src/notify/service/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod mobile_app;
|
||||||
1
home-assistant/src/sensor/device_classes/mod.rs
Normal file
1
home-assistant/src/sensor/device_classes/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
mod power;
|
||||||
125
home-assistant/src/sensor/device_classes/power.rs
Normal file
125
home-assistant/src/sensor/device_classes/power.rs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
use std::{future::Future, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use emitter_and_signal::{Signal, SignalExt};
|
||||||
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use python_utils::FromPyObjectViaParse;
|
||||||
|
use snafu::{ensure, ResultExt, Snafu};
|
||||||
|
|
||||||
|
use super::super::state_classes::measurement::Measurement;
|
||||||
|
use crate::{
|
||||||
|
domain::Domain,
|
||||||
|
entity_id::EntityId,
|
||||||
|
home_assistant::HomeAssistant,
|
||||||
|
object_id::ObjectId,
|
||||||
|
state_object::{self, StateObject, StateObjectSignalError},
|
||||||
|
unit_of_measurement::power::UnitOfMeasurement,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Power;
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum ExtractPowerError {
|
||||||
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
|
||||||
|
/// the string {actual:?} is not "power" like it's supposed to be
|
||||||
|
NotPower { actual: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractPowerError> for PyErr {
|
||||||
|
fn from(error: ExtractPowerError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractPowerError::ExtractStringError { .. } => PyException::new_err(error.to_string()),
|
||||||
|
ExtractPowerError::NotPower { .. } => PyValueError::new_err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStrLiteral) / #[literal = "state_changed"] once I learn how to make something like that and see about serde or strum integration or inspiration
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for Power {
|
||||||
|
type Error = ExtractPowerError;
|
||||||
|
|
||||||
|
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let string: &str = obj.extract().context(ExtractStringSnafu)?;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
string == "power",
|
||||||
|
NotPowerSnafu {
|
||||||
|
actual: string.to_owned()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromPyObject)]
|
||||||
|
#[pyo3(from_item_all)]
|
||||||
|
pub struct PowerSensorAttributes {
|
||||||
|
state_class: Measurement,
|
||||||
|
device_class: Power,
|
||||||
|
unit_of_measurement: UnitOfMeasurement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum CreateSignalError {
|
||||||
|
/// couldn't get the underlying state object signal
|
||||||
|
StateObjectSignalError {
|
||||||
|
source: state_object::CreateSignalError,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// couldn't map the state object to a power value
|
||||||
|
MappedSignalError {
|
||||||
|
source: emitter_and_signal::signal_ext::ProducerAlreadyExited,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signal<'py>(
|
||||||
|
py: Python<'py>,
|
||||||
|
home_assistant: &'py HomeAssistant,
|
||||||
|
object_id: ObjectId,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
Signal<Option<Arc<Result<uom::si::f64::Power, StateObjectSignalError<PyErr>>>>>,
|
||||||
|
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
|
||||||
|
),
|
||||||
|
CreateSignalError,
|
||||||
|
> {
|
||||||
|
let entity_id = EntityId(Domain::Light, object_id);
|
||||||
|
|
||||||
|
let (signal, task1) =
|
||||||
|
StateObject::<FromPyObjectViaParse<f64>, PowerSensorAttributes, Py<PyAny>>::signal(
|
||||||
|
py,
|
||||||
|
home_assistant,
|
||||||
|
entity_id,
|
||||||
|
)
|
||||||
|
.context(StateObjectSignalSnafu)?;
|
||||||
|
|
||||||
|
let (signal, task2) = signal
|
||||||
|
.map(|state_object_result_option| {
|
||||||
|
state_object_result_option.map(|state_object_result| {
|
||||||
|
Arc::new(
|
||||||
|
Result::as_ref(&state_object_result)
|
||||||
|
.map(|state_object| {
|
||||||
|
let amount = state_object.state.0;
|
||||||
|
let unit_of_measurement = state_object.attributes.unit_of_measurement;
|
||||||
|
|
||||||
|
let power = unit_of_measurement.into_uom(amount);
|
||||||
|
|
||||||
|
power
|
||||||
|
})
|
||||||
|
.map_err(|e| todo!()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.context(MappedSignalSnafu)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
signal,
|
||||||
|
async move { tokio::try_join!(task1, task2).map(|_| ()) },
|
||||||
|
))
|
||||||
|
}
|
||||||
2
home-assistant/src/sensor/mod.rs
Normal file
2
home-assistant/src/sensor/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod device_classes;
|
||||||
|
pub mod state_classes;
|
||||||
48
home-assistant/src/sensor/state_classes/measurement.rs
Normal file
48
home-assistant/src/sensor/state_classes/measurement.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use snafu::{ensure, ResultExt, Snafu};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Measurement;
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum ExtractMeasurementError {
|
||||||
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
|
||||||
|
/// the string {actual:?} is not "measurement" like it's supposed to be
|
||||||
|
NotMeasurement { actual: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractMeasurementError> for PyErr {
|
||||||
|
fn from(error: ExtractMeasurementError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractMeasurementError::ExtractStringError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
ExtractMeasurementError::NotMeasurement { .. } => {
|
||||||
|
PyValueError::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStrLiteral) / #[literal = "state_changed"] once I learn how to make something like that and see about serde or strum integration or inspiration
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for Measurement {
|
||||||
|
type Error = ExtractMeasurementError;
|
||||||
|
|
||||||
|
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let string: &str = obj.extract().context(ExtractStringSnafu)?;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
string == "measurement",
|
||||||
|
NotMeasurementSnafu {
|
||||||
|
actual: string.to_owned()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
home-assistant/src/sensor/state_classes/mod.rs
Normal file
1
home-assistant/src/sensor/state_classes/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod measurement;
|
||||||
@@ -1,33 +1,64 @@
|
|||||||
use super::{event::context::context::Context, service::IntoServiceCall};
|
use super::{event::context::context::Context, service::IntoServiceCall};
|
||||||
use pyo3::prelude::*;
|
use pyo3::{
|
||||||
use python_utils::{detach, validate_type_by_name};
|
exceptions::{PyException, PyTypeError},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ServiceRegistry(Py<PyAny>);
|
pub struct ServiceRegistry(Py<PyAny>);
|
||||||
|
|
||||||
impl<'py> FromPyObject<'py> for ServiceRegistry {
|
impl<'a, 'py> FromPyObject<'a, 'py> for ServiceRegistry {
|
||||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
type Error = TypeByNameValidationError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
// region: Validation
|
// region: Validation
|
||||||
validate_type_by_name(ob, "ServiceRegistry")?;
|
validate_type_by_name(&ob, "ServiceRegistry")?;
|
||||||
// endregion: Validation
|
// endregion: Validation
|
||||||
|
|
||||||
Ok(Self(detach(ob)))
|
Ok(Self(detach(ob)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum CallServiceError {
|
||||||
|
/// couldn't successfully call `async_call` and turn it into a `Future`
|
||||||
|
CallIntoFutureError { source: PyErr },
|
||||||
|
|
||||||
|
/// couldn't await the `Future` from the `async_call`
|
||||||
|
AwaitFutureError { source: PyErr },
|
||||||
|
|
||||||
|
/// couldn't extract the service response as the requested type
|
||||||
|
ExtractServiceResponseError { source: PyErr },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CallServiceError> for PyErr {
|
||||||
|
fn from(error: CallServiceError) -> Self {
|
||||||
|
match &error {
|
||||||
|
CallServiceError::CallIntoFutureError { .. } => PyException::new_err(error.to_string()),
|
||||||
|
CallServiceError::AwaitFutureError { .. } => PyException::new_err(error.to_string()),
|
||||||
|
CallServiceError::ExtractServiceResponseError { .. } => {
|
||||||
|
PyTypeError::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ServiceRegistry {
|
impl ServiceRegistry {
|
||||||
pub async fn call_service<
|
pub async fn call_service<
|
||||||
|
'a,
|
||||||
ServiceData: for<'py> IntoPyObject<'py>,
|
ServiceData: for<'py> IntoPyObject<'py>,
|
||||||
Target: for<'py> IntoPyObject<'py>,
|
Target: for<'py> IntoPyObject<'py>,
|
||||||
Event: for<'py> IntoPyObject<'py>,
|
Event: for<'py> IntoPyObject<'py>,
|
||||||
ServiceResponse: for<'py> FromPyObject<'py>,
|
ServiceResponse: 'static + for<'py> FromPyObjectOwned<'py>,
|
||||||
>(
|
>(
|
||||||
&self,
|
&'a self,
|
||||||
service_call: impl IntoServiceCall<ServiceData = ServiceData>,
|
service_call: impl IntoServiceCall<ServiceData = ServiceData>,
|
||||||
context: Option<Context<Event>>,
|
context: Option<Context<Event>>,
|
||||||
target: Option<Target>,
|
target: Option<Target>,
|
||||||
return_response: bool,
|
return_response: bool,
|
||||||
) -> PyResult<ServiceResponse> {
|
) -> Result<ServiceResponse, CallServiceError> {
|
||||||
let (domain, service, service_data) = service_call.into_service_call();
|
let (domain, service, service_data) = service_call.into_service_call();
|
||||||
|
|
||||||
let blocking = true;
|
let blocking = true;
|
||||||
@@ -42,13 +73,21 @@ impl ServiceRegistry {
|
|||||||
return_response,
|
return_response,
|
||||||
);
|
);
|
||||||
|
|
||||||
let future = Python::with_gil::<_, PyResult<_>>(|py| {
|
let future = Python::attach::<_, PyResult<_>>(|py| {
|
||||||
let service_registry = self.0.bind(py);
|
let service_registry = self.0.bind(py);
|
||||||
let awaitable = service_registry.call_method("async_call", args, None)?;
|
let awaitable = service_registry.call_method("async_call", args, None)?;
|
||||||
pyo3_async_runtimes::tokio::into_future(awaitable)
|
pyo3_async_runtimes::tokio::into_future(awaitable)
|
||||||
|
})
|
||||||
|
.context(CallIntoFutureSnafu)?;
|
||||||
|
|
||||||
|
let service_response = future.await.context(AwaitFutureSnafu)?;
|
||||||
|
let service_response = Python::attach(move |py| {
|
||||||
|
service_response
|
||||||
|
.extract(py)
|
||||||
|
.map_err(Into::into)
|
||||||
|
.context(ExtractServiceResponseSnafu)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let service_response = future.await?;
|
Ok(service_response)
|
||||||
Python::with_gil(|py| service_response.extract(py))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::str::FromStr;
|
use std::{str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use pyo3::{exceptions::PyValueError, PyErr};
|
use pyo3::{exceptions::PyValueError, PyErr};
|
||||||
use smol_str::SmolStr;
|
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
|
// TODO: derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
#[derive(Debug, Clone, derive_more::Display)]
|
#[derive(Debug, Clone, derive_more::Display)]
|
||||||
pub struct Slug(SmolStr);
|
pub struct Slug(Arc<str>);
|
||||||
|
|
||||||
#[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}"))]
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
55
home-assistant/src/state/error_state.rs
Normal file
55
home-assistant/src/state/error_state.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum ExtractErrorStateError {
|
||||||
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
|
||||||
|
/// the string had an unexpected value
|
||||||
|
UnexpectedValue {
|
||||||
|
source: <ErrorState as FromStr>::Err,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractErrorStateError> for PyErr {
|
||||||
|
fn from(error: ExtractErrorStateError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractErrorStateError::ExtractStringError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
ExtractErrorStateError::UnexpectedValue { .. } => {
|
||||||
|
PyValueError::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for ErrorState {
|
||||||
|
type Error = ExtractErrorStateError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = ob.extract::<String>().context(ExtractStringSnafu)?;
|
||||||
|
|
||||||
|
let state = ErrorState::from_str(&s).context(UnexpectedValueSnafu)?;
|
||||||
|
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
64
home-assistant/src/state/mod.rs
Normal file
64
home-assistant/src/state/mod.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use std::{convert::Infallible, str::FromStr};
|
||||||
|
|
||||||
|
use pyo3::{exceptions::PyException, prelude::*};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
|
pub mod error_state;
|
||||||
|
pub mod unexpected_state;
|
||||||
|
|
||||||
|
pub use error_state::ErrorState;
|
||||||
|
pub use unexpected_state::UnexpectedState;
|
||||||
|
|
||||||
|
#[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())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum ExtractHomeAssistantStateError {
|
||||||
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractHomeAssistantStateError> for PyErr {
|
||||||
|
fn from(error: ExtractHomeAssistantStateError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractHomeAssistantStateError::ExtractStringError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
|
impl<'a, 'py, State: FromStr + FromPyObject<'a, 'py>> FromPyObject<'a, 'py>
|
||||||
|
for HomeAssistantState<State>
|
||||||
|
{
|
||||||
|
type Error = ExtractHomeAssistantStateError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
|
||||||
|
|
||||||
|
let Ok(state) = s.parse();
|
||||||
|
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
home-assistant/src/state/unexpected_state.rs
Normal file
35
home-assistant/src/state/unexpected_state.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use pyo3::{exceptions::PyException, prelude::*};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::Display)]
|
||||||
|
pub struct UnexpectedState(pub Arc<str>);
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum ExtractUnexpectedStateError {
|
||||||
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractUnexpectedStateError> for PyErr {
|
||||||
|
fn from(error: ExtractUnexpectedStateError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractUnexpectedStateError::ExtractStringError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for UnexpectedState {
|
||||||
|
type Error = ExtractUnexpectedStateError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = ob.extract::<String>().context(ExtractStringSnafu)?;
|
||||||
|
let s = s.into();
|
||||||
|
|
||||||
|
Ok(UnexpectedState(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,58 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::entity_id::EntityId;
|
use super::entity_id::EntityId;
|
||||||
use super::state_object::StateObject;
|
use super::state_object::StateObject;
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use python_utils::{detach, validate_type_by_name};
|
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StateMachine(Py<PyAny>);
|
pub struct StateMachine(Py<PyAny>);
|
||||||
|
|
||||||
impl<'py> FromPyObject<'py> for StateMachine {
|
impl<'a, 'py> FromPyObject<'a, 'py> for StateMachine {
|
||||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
type Error = TypeByNameValidationError;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
// region: Validation
|
// region: Validation
|
||||||
validate_type_by_name(ob, "StateMachine")?;
|
validate_type_by_name(&ob, "StateMachine")?;
|
||||||
// endregion: Validation
|
// endregion: Validation
|
||||||
|
|
||||||
Ok(Self(detach(ob)))
|
Ok(Self(detach(ob)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Snafu)]
|
||||||
|
pub enum GetStateError<ExtractStateObjectError: 'static + snafu::Error> {
|
||||||
|
/// couldn't get this state object from the state machine
|
||||||
|
GetStateObjectError { source: Arc<PyErr> },
|
||||||
|
|
||||||
|
/// couldn't extract the state as a [`StateObject`]
|
||||||
|
ExtractStateObjectError { source: ExtractStateObjectError },
|
||||||
|
}
|
||||||
|
|
||||||
impl StateMachine {
|
impl StateMachine {
|
||||||
pub fn get<
|
pub fn get<
|
||||||
|
'a,
|
||||||
'py,
|
'py,
|
||||||
State: FromPyObject<'py>,
|
State: FromPyObjectOwned<'py>,
|
||||||
Attributes: FromPyObject<'py>,
|
Attributes: FromPyObjectOwned<'py>,
|
||||||
ContextEvent: FromPyObject<'py>,
|
ContextEvent: FromPyObjectOwned<'py>,
|
||||||
>(
|
>(
|
||||||
&self,
|
&'a self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
entity_id: EntityId,
|
entity_id: EntityId,
|
||||||
) -> PyResult<Option<StateObject<State, Attributes, ContextEvent>>> {
|
) -> Result<
|
||||||
|
Option<StateObject<State, Attributes, ContextEvent>>,
|
||||||
|
GetStateError<
|
||||||
|
<Option<StateObject<State, Attributes, ContextEvent>> as FromPyObject<'a, 'py>>::Error,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
let args = (entity_id.to_string(),);
|
let args = (entity_id.to_string(),);
|
||||||
let state = self.0.call_method1(py, "get", args)?;
|
let state = self
|
||||||
state.extract(py)
|
.0
|
||||||
|
.call_method1(py, "get", args)
|
||||||
|
.map_err(Arc::new)
|
||||||
|
.context(GetStateObjectSnafu)?;
|
||||||
|
Ok(state.extract(py).context(ExtractStateObjectSnafu)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::{
|
|||||||
event::{context::context::Context, specific::state_changed},
|
event::{context::context::Context, specific::state_changed},
|
||||||
home_assistant::HomeAssistant,
|
home_assistant::HomeAssistant,
|
||||||
};
|
};
|
||||||
use crate::entity_id::EntityId;
|
use crate::{entity_id::EntityId, home_assistant::GetStatesError, state_machine::GetStateError};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use emitter_and_signal::signal::Signal;
|
use emitter_and_signal::signal::Signal;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@@ -10,6 +10,7 @@ use pyo3::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
types::{PyCFunction, PyDict, PyTuple},
|
types::{PyCFunction, PyDict, PyTuple},
|
||||||
};
|
};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
use std::{future::Future, sync::Arc};
|
use std::{future::Future, sync::Arc};
|
||||||
use tokio::{select, sync::mpsc};
|
use tokio::{select, sync::mpsc};
|
||||||
|
|
||||||
@@ -24,48 +25,86 @@ pub struct StateObject<State, Attributes, ContextEvent> {
|
|||||||
pub context: Context<ContextEvent>,
|
pub context: Context<ContextEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type ExtractStateObjectError<'a, 'py, State, Attributes, ContextEvent> =
|
||||||
|
<StateObject<State, Attributes, ContextEvent> as FromPyObject<'a, 'py>>::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum CreateSignalError {
|
||||||
|
/// couldn't get the state machine from the Home Assistant object
|
||||||
|
GetStatesError { source: GetStatesError },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Snafu)]
|
||||||
|
pub enum StateObjectSignalError<ExtractStateObjectError: 'static + snafu::Error> {
|
||||||
|
/// couldn't get the state object directly from the state machine
|
||||||
|
GetFromStateMachine {
|
||||||
|
source: GetStateError<ExtractStateObjectError>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// couldn't get the state object from the new state event
|
||||||
|
GetFromNewStateEvent { source: Arc<PyErr> },
|
||||||
|
}
|
||||||
|
|
||||||
impl<
|
impl<
|
||||||
State: Send + Sync + 'static + for<'py> FromPyObject<'py>,
|
State: Send + Sync + 'static + for<'a, 'py> FromPyObject<'a, 'py>,
|
||||||
Attributes: Send + Sync + 'static + for<'py> FromPyObject<'py>,
|
Attributes: Send + Sync + 'static + for<'a, 'py> FromPyObject<'a, 'py>,
|
||||||
ContextEvent: Send + Sync + 'static + for<'py> FromPyObject<'py>,
|
ContextEvent: Send + Sync + 'static + for<'a, 'py> FromPyObject<'a, 'py>,
|
||||||
> StateObject<State, Attributes, ContextEvent>
|
> StateObject<State, Attributes, ContextEvent>
|
||||||
{
|
{
|
||||||
pub fn store(
|
pub fn signal<'a, 'py>(
|
||||||
py: Python<'_>,
|
py: Python<'py>,
|
||||||
home_assistant: &HomeAssistant,
|
home_assistant: &'py HomeAssistant,
|
||||||
entity_id: EntityId,
|
entity_id: EntityId,
|
||||||
) -> PyResult<(
|
) -> Result<
|
||||||
Signal<Option<Arc<Self>>>,
|
(
|
||||||
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
|
Signal<
|
||||||
)> {
|
Option<
|
||||||
let state_machine = home_assistant.states(py)?;
|
Arc<
|
||||||
let current = state_machine.get(py, entity_id.clone())?;
|
Result<
|
||||||
|
Self,
|
||||||
|
StateObjectSignalError<<Self as FromPyObject<'a, 'py>>::Error>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
|
||||||
|
),
|
||||||
|
CreateSignalError,
|
||||||
|
> {
|
||||||
|
let state_machine = home_assistant.states(py).context(GetStatesSnafu)?;
|
||||||
|
let current = state_machine
|
||||||
|
.get(py, entity_id.clone())
|
||||||
|
.context(GetFromStateMachineSnafu)
|
||||||
|
.transpose();
|
||||||
|
|
||||||
let py_home_assistant = home_assistant.into_pyobject(py)?.unbind();
|
let Ok(py_home_assistant) = home_assistant.into_pyobject(py);
|
||||||
|
let py_home_assistant = py_home_assistant.unbind();
|
||||||
|
|
||||||
let (store, task) = Signal::new(current.map(Arc::new), |mut publisher_stream| async move {
|
let (signal, task) = Signal::new(
|
||||||
while let Some(publisher) = publisher_stream.wait().await {
|
current.map(Arc::new),
|
||||||
let (new_state_sender, mut new_state_receiver) = mpsc::channel(8);
|
|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| {
|
let untrack = Python::attach::<_, PyResult<_>>(|py| {
|
||||||
static EVENT_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
static EVENT_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||||
|
|
||||||
let event_module = EVENT_MODULE
|
let event_module = EVENT_MODULE
|
||||||
.get_or_try_init(|| {
|
.get_or_try_init(|| {
|
||||||
Result::<_, PyErr>::Ok(
|
Result::<_, PyErr>::Ok(
|
||||||
py.import("homeassistant.helpers.event")?.unbind(),
|
py.import("homeassistant.helpers.event")?.unbind(),
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.bind(py);
|
.bind(py);
|
||||||
|
|
||||||
let untrack = {
|
let untrack = {
|
||||||
let callback =
|
let callback =
|
||||||
move |args: &Bound<'_, PyTuple>,
|
move |args: &Bound<'_, PyTuple>,
|
||||||
_kwargs: Option<&Bound<'_, PyDict>>| {
|
_kwargs: Option<&Bound<'_, PyDict>>| {
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("calling the closure");
|
tracing::debug!("calling the closure");
|
||||||
|
|
||||||
if let Ok((event,)) = args.extract::<(
|
let new_state_res = args.extract::<(
|
||||||
state_changed::Event<
|
state_changed::Event<
|
||||||
State,
|
State,
|
||||||
Attributes,
|
Attributes,
|
||||||
@@ -75,77 +114,65 @@ impl<
|
|||||||
ContextEvent,
|
ContextEvent,
|
||||||
Py<PyAny>,
|
Py<PyAny>,
|
||||||
>,
|
>,
|
||||||
)>() {
|
)>().map(|event| event.0.data.new_state).map_err(Arc::new).context(GetFromNewStateEventSnafu);
|
||||||
let new_state = event.data.new_state;
|
|
||||||
|
new_state_sender.try_send(new_state_res).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)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let untrack = untrack.unbind();
|
||||||
|
|
||||||
|
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::attach(|py| untrack.call0(py));
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("sending a new state"); // TODO: remove
|
tracing::debug!(?res);
|
||||||
new_state_sender.try_send(new_state).unwrap();
|
break;
|
||||||
}
|
}
|
||||||
};
|
new_state_res_option = new_state_receiver.recv() => {
|
||||||
let callback = PyCFunction::new_closure(py, None, None, callback)?;
|
match new_state_res_option {
|
||||||
let args = (
|
Some(new_state_res) => {
|
||||||
py_home_assistant.clone_ref(py),
|
#[cfg(feature = "tracing")]
|
||||||
vec![entity_id.clone()],
|
tracing::debug!("publishing new state");
|
||||||
callback,
|
publisher.publish(new_state_res.transpose().map(Arc::new));
|
||||||
);
|
},
|
||||||
event_module.call_method1("async_track_state_change_event", args)?
|
None => {
|
||||||
};
|
#[cfg(feature = "tracing")]
|
||||||
#[cfg(feature = "tracing")]
|
tracing::debug!("channel dropped");
|
||||||
tracing::debug!(?untrack, "as any");
|
break
|
||||||
|
},
|
||||||
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");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
#[cfg(feature = "tracing")]
|
|
||||||
tracing::debug!("untrack is err");
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
Ok((store, task))
|
Ok((signal, task))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
home-assistant/src/switch/mod.rs
Normal file
1
home-assistant/src/switch/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
home-assistant/src/unit_of_measurement/mod.rs
Normal file
1
home-assistant/src/unit_of_measurement/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod power;
|
||||||
102
home-assistant/src/unit_of_measurement/power.rs
Normal file
102
home-assistant/src/unit_of_measurement/power.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
use strum::EnumString;
|
||||||
|
use uom::{
|
||||||
|
si::{
|
||||||
|
energy::btu,
|
||||||
|
power::{gigawatt, kilowatt, megawatt, milliwatt, terawatt, watt},
|
||||||
|
quantities::{Energy, Power, Time},
|
||||||
|
time::hour,
|
||||||
|
Units, SI,
|
||||||
|
},
|
||||||
|
Conversion,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Power units
|
||||||
|
#[derive(Debug, Clone, Copy, EnumString, strum::Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum UnitOfMeasurement {
|
||||||
|
#[strum(serialize = "mW")]
|
||||||
|
MilliWatt,
|
||||||
|
#[strum(serialize = "W")]
|
||||||
|
Watt,
|
||||||
|
#[strum(serialize = "kW")]
|
||||||
|
KiloWatt,
|
||||||
|
#[strum(serialize = "MW")]
|
||||||
|
MegaWatt,
|
||||||
|
#[strum(serialize = "GW")]
|
||||||
|
GigaWatt,
|
||||||
|
#[strum(serialize = "TW")]
|
||||||
|
TeraWatt,
|
||||||
|
#[strum(serialize = "BTU/h")]
|
||||||
|
BtuPerhour,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum ExtractUnitOfMeasurementError {
|
||||||
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: PyErr },
|
||||||
|
|
||||||
|
/// couldn't parse the string as a [`UnitOfMeasurement`]
|
||||||
|
ParseError {
|
||||||
|
source: <UnitOfMeasurement as FromStr>::Err,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtractUnitOfMeasurementError> for PyErr {
|
||||||
|
fn from(error: ExtractUnitOfMeasurementError) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractUnitOfMeasurementError::ExtractStringError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
ExtractUnitOfMeasurementError::ParseError { .. } => {
|
||||||
|
PyValueError::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for UnitOfMeasurement {
|
||||||
|
type Error = ExtractUnitOfMeasurementError;
|
||||||
|
|
||||||
|
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = obj.extract().context(ExtractStringSnafu)?;
|
||||||
|
let unit_of_measurement = UnitOfMeasurement::from_str(s).context(ParseSnafu)?;
|
||||||
|
|
||||||
|
Ok(unit_of_measurement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnitOfMeasurement {
|
||||||
|
pub fn into_uom<V>(&self, amount: V) -> Power<V>
|
||||||
|
where
|
||||||
|
V: uom::num::Num + uom::Conversion<V, T = V>,
|
||||||
|
milliwatt: Conversion<V, T = V>,
|
||||||
|
watt: Conversion<V, T = V>,
|
||||||
|
kilowatt: Conversion<V, T = V>,
|
||||||
|
megawatt: Conversion<V, T = V>,
|
||||||
|
gigawatt: Conversion<V, T = V>,
|
||||||
|
terawatt: Conversion<V, T = V>,
|
||||||
|
btu: Conversion<V, T = V>,
|
||||||
|
hour: Conversion<V, T = V>,
|
||||||
|
SI<V>: Units<V>,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
UnitOfMeasurement::MilliWatt => Power::new::<milliwatt>(amount),
|
||||||
|
UnitOfMeasurement::Watt => Power::new::<watt>(amount),
|
||||||
|
UnitOfMeasurement::KiloWatt => Power::new::<kilowatt>(amount),
|
||||||
|
UnitOfMeasurement::MegaWatt => Power::new::<megawatt>(amount),
|
||||||
|
UnitOfMeasurement::GigaWatt => Power::new::<gigawatt>(amount),
|
||||||
|
UnitOfMeasurement::TeraWatt => Power::new::<terawatt>(amount),
|
||||||
|
UnitOfMeasurement::BtuPerhour => {
|
||||||
|
Energy::new::<btu>(amount) / Time::new::<hour>(V::one())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
persisted/Cargo.toml
Normal file
14
persisted/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "persisted"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bytes = { workspace = true }
|
||||||
|
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||||
|
fjall = "2.11"
|
||||||
|
postcard = { version = "1.1", features = ["use-std"] }
|
||||||
|
serde = { workspace = true }
|
||||||
|
snafu = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["rt", "sync"] }
|
||||||
111
persisted/src/lib.rs
Normal file
111
persisted/src/lib.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
pub use bytes::Bytes;
|
||||||
|
use emitter_and_signal::signal::{JoinError, Signal};
|
||||||
|
pub use fjall::{Config, Keyspace, Partition, PartitionCreateOptions};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use snafu::{OptionExt, ResultExt, Snafu};
|
||||||
|
use std::{fmt::Debug, future::Future, num::NonZeroUsize, ops::Deref, sync::Arc};
|
||||||
|
use tokio::{
|
||||||
|
select,
|
||||||
|
sync::{mpsc, oneshot},
|
||||||
|
task::spawn_blocking,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Snafu)]
|
||||||
|
pub enum PersistedError {
|
||||||
|
Missing,
|
||||||
|
CreationError {
|
||||||
|
// Wrapped in [`Arc`] to make this [`Clone`]
|
||||||
|
source: Arc<fjall::Error>,
|
||||||
|
},
|
||||||
|
DeserializationError {
|
||||||
|
source: postcard::Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn persisted<T: Debug + Send + Sync + 'static + Serialize + for<'a> Deserialize<'a>>(
|
||||||
|
partition: Partition,
|
||||||
|
identifier: Bytes,
|
||||||
|
buffer: NonZeroUsize,
|
||||||
|
) -> (
|
||||||
|
Setter<T>,
|
||||||
|
Signal<Result<T, PersistedError>>,
|
||||||
|
impl Future<Output = Result<(), JoinError>>,
|
||||||
|
) {
|
||||||
|
let initial = spawn_blocking({
|
||||||
|
let partition = partition.clone();
|
||||||
|
let identifier = identifier.clone();
|
||||||
|
move || partition.get(identifier.deref())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.map_err(Arc::new)
|
||||||
|
.context(CreationSnafu)
|
||||||
|
.and_then(|op| op.context(MissingSnafu))
|
||||||
|
.and_then(|slice| postcard::from_bytes(&slice).context(DeserializationSnafu));
|
||||||
|
|
||||||
|
let (set_tx, mut set_rx) = mpsc::channel(buffer.get());
|
||||||
|
let setter = Setter { sender: set_tx };
|
||||||
|
|
||||||
|
let (signal, task) = Signal::new(initial, move |mut publisher_stream| async move {
|
||||||
|
while let Some(publisher) = publisher_stream.wait().await {
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
biased;
|
||||||
|
_ = publisher.all_unsubscribed() => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
new_value_opt = set_rx.recv() => {
|
||||||
|
let Some((new_value, callback)) = new_value_opt else { return };
|
||||||
|
let serialized_res = postcard::to_stdvec(&new_value).context(SerializationSnafu);
|
||||||
|
// Stand-in for Option::async_and_then
|
||||||
|
let persisted_res = match serialized_res {
|
||||||
|
Ok(serialized) => spawn_blocking({
|
||||||
|
let partition = partition.clone();
|
||||||
|
let identifier = identifier.clone();
|
||||||
|
move || partition.insert(identifier.deref(), serialized)
|
||||||
|
}).await.unwrap().context(SavingSnafu),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
};
|
||||||
|
|
||||||
|
if persisted_res.is_ok() {
|
||||||
|
publisher.publish(Ok(new_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = callback.send(persisted_res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(setter, signal, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Setter<T: 'static> {
|
||||||
|
sender: mpsc::Sender<(T, oneshot::Sender<Result<(), SetError<T>>>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add doc comments functioning as error Display
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum SetError<T: 'static> {
|
||||||
|
Closed { source: mpsc::error::SendError<T> },
|
||||||
|
NoFeedback { source: oneshot::error::RecvError },
|
||||||
|
SerializationError { source: postcard::Error },
|
||||||
|
SavingError { source: fjall::Error },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> Setter<T> {
|
||||||
|
pub async fn set(&self, value: T) -> Result<(), SetError<T>> {
|
||||||
|
let (callback_tx, callback_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
self.sender
|
||||||
|
.send((value, callback_tx))
|
||||||
|
.await
|
||||||
|
.map_err(|send_error| mpsc::error::SendError(send_error.0 .0))
|
||||||
|
.context(ClosedSnafu)?;
|
||||||
|
|
||||||
|
let set_res = callback_rx.await.context(NoFeedbackSnafu)?;
|
||||||
|
set_res
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,4 +5,6 @@ edition = "2021"
|
|||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
derive_more = { workspace = true }
|
||||||
pyo3 = { workspace = true }
|
pyo3 = { workspace = true }
|
||||||
|
snafu = { workspace = true }
|
||||||
|
|||||||
@@ -1,45 +1,150 @@
|
|||||||
use std::convert::Infallible;
|
use std::{convert::Infallible, fmt::Display, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use pyo3::{exceptions::PyTypeError, prelude::*, types::PyNone};
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyTypeError, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
types::PyString,
|
||||||
|
};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
pub mod none;
|
||||||
pub struct IsNone;
|
pub use none::IsNone;
|
||||||
|
|
||||||
impl<'py> FromPyObject<'py> for IsNone {
|
|
||||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
|
||||||
ob.downcast::<PyNone>()?;
|
|
||||||
Ok(IsNone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'py> IntoPyObject<'py> for IsNone {
|
|
||||||
type Target = PyNone;
|
|
||||||
|
|
||||||
type Output = Borrowed<'py, 'py, Self::Target>;
|
|
||||||
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
|
||||||
Ok(PyNone::get(py))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a GIL-independent reference
|
/// Create a GIL-independent reference
|
||||||
pub fn detach<T>(bound: &Bound<T>) -> Py<T> {
|
pub fn detach<T>(borrowed: Borrowed<'_, '_, T>) -> Py<T> {
|
||||||
let py = bound.py();
|
let py = borrowed.py();
|
||||||
bound.as_unbound().clone_ref(py)
|
borrowed.as_unbound().clone_ref(py)
|
||||||
|
}
|
||||||
|
/// Create a GIL-independent reference
|
||||||
|
pub fn detach_bound<T>(bound: &Bound<T>) -> Py<T> {
|
||||||
|
detach(bound.as_borrowed())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_type_by_name(bound: &Bound<PyAny>, expected_type_name: &str) -> PyResult<()> {
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum TypeByNameValidationError {
|
||||||
|
/// error getting the type name of this object
|
||||||
|
GetTypeNameError { source: PyErr },
|
||||||
|
/// error extracting the (successfully retrieved) type name as an [`&str`]
|
||||||
|
ExtractTypeNameError { source: PyErr },
|
||||||
|
|
||||||
|
/// error getting the fully qualified type name of this object
|
||||||
|
GetFullyQualifiedTypeNameError { source: PyErr },
|
||||||
|
/// error extracting the (successfully retrieved) fully qualified type name as an [`&str`]
|
||||||
|
ExtractFullyQualifiedTypeNameError { source: PyErr },
|
||||||
|
|
||||||
|
/// expected an instance of {expected} but got an instance of {actual}
|
||||||
|
UnexpectedType { expected: String, actual: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TypeByNameValidationError> for PyErr {
|
||||||
|
fn from(error: TypeByNameValidationError) -> Self {
|
||||||
|
match &error {
|
||||||
|
TypeByNameValidationError::GetTypeNameError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
TypeByNameValidationError::ExtractTypeNameError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
TypeByNameValidationError::GetFullyQualifiedTypeNameError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
TypeByNameValidationError::ExtractFullyQualifiedTypeNameError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
TypeByNameValidationError::UnexpectedType { .. } => {
|
||||||
|
PyTypeError::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_type_by_name(
|
||||||
|
bound: &Bound<PyAny>,
|
||||||
|
expected_type_name: &str,
|
||||||
|
) -> Result<(), TypeByNameValidationError> {
|
||||||
let py_type = bound.get_type();
|
let py_type = bound.get_type();
|
||||||
let type_name = py_type.name()?;
|
let type_name = py_type.name().context(GetTypeNameSnafu)?;
|
||||||
let type_name = type_name.to_str()?;
|
let type_name = type_name.to_str().context(ExtractTypeNameSnafu)?;
|
||||||
|
|
||||||
if type_name != expected_type_name {
|
if type_name != expected_type_name {
|
||||||
let fully_qualified_type_name = py_type.fully_qualified_name()?;
|
let fully_qualified_type_name = py_type
|
||||||
let fully_qualified_type_name = fully_qualified_type_name.to_str()?;
|
.fully_qualified_name()
|
||||||
return Err(PyTypeError::new_err(format!("expected an instance of {expected_type_name} but got an instance of {fully_qualified_type_name}")));
|
.context(GetFullyQualifiedTypeNameSnafu)?;
|
||||||
|
let fully_qualified_type_name = fully_qualified_type_name
|
||||||
|
.to_str()
|
||||||
|
.context(ExtractFullyQualifiedTypeNameSnafu)?;
|
||||||
|
return Err(TypeByNameValidationError::UnexpectedType {
|
||||||
|
expected: expected_type_name.to_owned(),
|
||||||
|
actual: fully_qualified_type_name.to_owned(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::Display)]
|
||||||
|
pub struct IntoPyObjectViaDisplay<T>(pub T);
|
||||||
|
|
||||||
|
impl<'py, T> IntoPyObject<'py> for IntoPyObjectViaDisplay<T>
|
||||||
|
where
|
||||||
|
T: Display,
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::FromStr)]
|
||||||
|
pub struct FromPyObjectViaParse<T>(pub T);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Snafu)]
|
||||||
|
pub enum ExtractPyObjectViaParseError<ParseError>
|
||||||
|
where
|
||||||
|
ParseError: 'static + snafu::Error,
|
||||||
|
{
|
||||||
|
/// couldn't extract the object as a string
|
||||||
|
ExtractStringError { source: Arc<PyErr> },
|
||||||
|
|
||||||
|
/// couldn't parse the string as an instance of this Rust type
|
||||||
|
ParseError { source: ParseError },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<ExtractPyObjectViaParseError<E>> for PyErr
|
||||||
|
where
|
||||||
|
E: 'static + snafu::Error,
|
||||||
|
{
|
||||||
|
fn from(error: ExtractPyObjectViaParseError<E>) -> Self {
|
||||||
|
match &error {
|
||||||
|
ExtractPyObjectViaParseError::ExtractStringError { .. } => {
|
||||||
|
PyException::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
ExtractPyObjectViaParseError::ParseError { .. } => {
|
||||||
|
PyValueError::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'py, T> FromPyObject<'a, 'py> for FromPyObjectViaParse<T>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
<T as FromStr>::Err: 'static + snafu::Error,
|
||||||
|
PyErr: From<ExtractPyObjectViaParseError<<T as FromStr>::Err>>,
|
||||||
|
{
|
||||||
|
type Error = ExtractPyObjectViaParseError<<T as FromStr>::Err>;
|
||||||
|
|
||||||
|
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
let s = obj
|
||||||
|
.extract::<&str>()
|
||||||
|
.map_err(Arc::new)
|
||||||
|
.context(ExtractStringSnafu)?;
|
||||||
|
let t = T::from_str(s).context(ParseSnafu)?;
|
||||||
|
|
||||||
|
Ok(FromPyObjectViaParse(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
27
python-utils/src/none.rs
Normal file
27
python-utils/src/none.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use pyo3::{types::PyNone, Borrowed, FromPyObject, IntoPyObject, PyAny, PyErr, Python};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct IsNone;
|
||||||
|
|
||||||
|
impl<'a, 'py> FromPyObject<'a, 'py> for IsNone {
|
||||||
|
type Error = PyErr;
|
||||||
|
|
||||||
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||||
|
ob.cast::<PyNone>()?;
|
||||||
|
Ok(IsNone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'py> IntoPyObject<'py> for IsNone {
|
||||||
|
type Target = PyNone;
|
||||||
|
|
||||||
|
type Output = Borrowed<'py, 'py, Self::Target>;
|
||||||
|
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||||
|
Ok(PyNone::get(py))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user