Compare commits
34 Commits
89404a56e0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4786b1e6ba | ||
|
|
a09a97d06d | ||
| 4eb8a752cc | |||
|
|
8d3cde3c43 | ||
|
|
cb216d0a0a | ||
|
|
8b966c1210 | ||
|
|
a2d6d9f4c2 | ||
|
|
8ab8dd3441 | ||
|
|
224ee7732f | ||
|
|
83b35908f7 | ||
| e3c54a996e | |||
| 38c30d87d0 | |||
| 17fabbf178 | |||
| 6abe278aa3 | |||
| 120ad97c53 | |||
| 338a8c571c | |||
| 34d0266c1f | |||
| ac0f574d2b | |||
| 9863029eb9 | |||
|
|
41cf9d0d09 | ||
|
|
d6418b5fb0 | ||
|
|
c0737c4420 | ||
|
|
53a2fc2a35 | ||
|
|
044243ae3c | ||
|
|
7f3c2ac30c | ||
|
|
3f94015b4f | ||
|
|
3e063901be | ||
| 9d8b2140dd | |||
|
|
0565624599 | ||
|
|
d6ad9972c7 | ||
|
|
9a9cefee37 | ||
| c637ad2d76 | |||
| 8e43ff75a5 | |||
| cebf7dd93b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
|
.history
|
||||||
/target
|
/target
|
||||||
|
|||||||
1253
Cargo.lock
generated
1253
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
25
Cargo.toml
@@ -8,8 +8,13 @@ members = [
|
|||||||
"persisted",
|
"persisted",
|
||||||
"protocol",
|
"protocol",
|
||||||
"python-utils",
|
"python-utils",
|
||||||
|
"python-utils-macros",
|
||||||
|
"python-utils-macros-impl",
|
||||||
|
"string-literal",
|
||||||
|
"string-literal-macros",
|
||||||
|
"string-literal-macros-impl",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
license = "Unlicense"
|
license = "Unlicense"
|
||||||
@@ -22,15 +27,19 @@ chrono-tz = "0.10.4"
|
|||||||
deranged = "0.5"
|
deranged = "0.5"
|
||||||
derive_more = "2.1.0"
|
derive_more = "2.1.0"
|
||||||
ext-trait = "2.0.1"
|
ext-trait = "2.0.1"
|
||||||
mitsein = "0.8"
|
futures = "0.3.32"
|
||||||
|
mitsein = "0.9"
|
||||||
palette = "0.7"
|
palette = "0.7"
|
||||||
pyo3 = "0.27"
|
proc-macro2 = "1.0.106"
|
||||||
pyo3-async-runtimes = "0.27"
|
pyo3 = "0.29"
|
||||||
|
pyo3-async-runtimes = "0.29"
|
||||||
|
quote = "1.0.46"
|
||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
snafu = "0.8.9"
|
snafu = "0.9"
|
||||||
strum = "0.27.2"
|
strum = "0.28"
|
||||||
|
syn = "2.0.118"
|
||||||
tokio = "1.48.0"
|
tokio = "1.48.0"
|
||||||
tracing = "0.1.43"
|
tracing = "0.1.43"
|
||||||
typed-builder = "0.22"
|
typed-builder = "0.23"
|
||||||
typed-builder-macro = "0.22"
|
typed-builder-macro = "0.23"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ chrono = { workspace = true }
|
|||||||
chrono-tz = { workspace = true }
|
chrono-tz = { workspace = true }
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
ijson = "0.1.4"
|
ijson = "0.1.4"
|
||||||
itertools = "0.14.0"
|
itertools = "0.15"
|
||||||
pyo3 = { workspace = true, optional = true, features = ["chrono", "chrono-tz"] }
|
pyo3 = { workspace = true, optional = true, features = ["chrono", "chrono-tz"] }
|
||||||
snafu = { workspace = true }
|
snafu = { workspace = true }
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use ijson::{IArray, INumber, IObject, IString, IValue};
|
|||||||
#[cfg(feature = "pyo3")]
|
#[cfg(feature = "pyo3")]
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
exceptions::{PyException, PyTypeError, PyValueError},
|
exceptions::{PyException, PyTypeError, PyValueError},
|
||||||
prelude::*,
|
types::{PyAnyMethods as _, PyList, PyNone, PyTypeMethods as _},
|
||||||
types::{PyList, PyNone},
|
Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, Python,
|
||||||
};
|
};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
@@ -41,9 +41,10 @@ impl From<MapKey> for Arbitrary {
|
|||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
pub enum MapKeyFromArbitraryError {
|
pub enum MapKeyFromArbitraryError {
|
||||||
#[snafu(display("floats aren't supported as map keys yet. got {value:?}"))]
|
/// floats aren't supported as map keys yet. got {value:?}
|
||||||
FloatNotSupported { value: FiniteF64 },
|
FloatNotSupported { value: FiniteF64 },
|
||||||
#[snafu(display("a map cannot be a map key. got {value:?}"))]
|
|
||||||
|
/// a map cannot be a map key. got {value:?}
|
||||||
MapCannotBeAMapKey { value: Map },
|
MapCannotBeAMapKey { value: Map },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use snafu::Snafu;
|
|||||||
#[derive(Debug, Clone, derive_more::Into)]
|
#[derive(Debug, Clone, derive_more::Into)]
|
||||||
pub struct FiniteF64(f64);
|
pub struct FiniteF64(f64);
|
||||||
|
|
||||||
|
/// {value:?} is not finite
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
#[snafu(display("{value:?} is not finite"))]
|
|
||||||
pub struct NotFinite {
|
pub struct NotFinite {
|
||||||
value: f64,
|
value: f64,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use itertools::Itertools;
|
|||||||
#[cfg(feature = "pyo3")]
|
#[cfg(feature = "pyo3")]
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
exceptions::PyTypeError,
|
exceptions::PyTypeError,
|
||||||
prelude::*,
|
types::{PyAnyMethods as _, PyNone, PyTuple, PyTypeMethods as _},
|
||||||
types::{PyNone, PyTuple},
|
Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, Python,
|
||||||
};
|
};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::messages::{
|
use crate::messages::{
|
||||||
GetSysInfo, GetSysInfoResponse, LB130USSys, LightState, Off, On, SetLightLastOn, SetLightOff,
|
GetSysInfo, GetSysInfoResponse, LB130USSys, SetLightState, SetLightStateArgs,
|
||||||
SetLightState, SetLightStateArgs, SetLightStateResponse, SetLightTo, SysInfo,
|
SetLightStateResponse, SysInfo,
|
||||||
};
|
};
|
||||||
use backon::{FibonacciBuilder, Retryable};
|
use backon::{FibonacciBuilder, Retryable};
|
||||||
|
|
||||||
@@ -186,8 +186,7 @@ async fn send_request<
|
|||||||
let incoming_length = reader.read_u32().await.context(ReadSnafu)?;
|
let incoming_length = reader.read_u32().await.context(ReadSnafu)?;
|
||||||
tracing::info!(?incoming_length);
|
tracing::info!(?incoming_length);
|
||||||
|
|
||||||
let mut incoming_message = Vec::new();
|
let mut incoming_message = vec![0; incoming_length as usize];
|
||||||
incoming_message.resize(incoming_length as usize, 0);
|
|
||||||
reader
|
reader
|
||||||
.read_exact(&mut incoming_message)
|
.read_exact(&mut incoming_message)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod connection;
|
pub mod connection;
|
||||||
mod impl_protocol;
|
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
mod protocol;
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ impl<'de> Deserialize<'de> for MaybeKelvin {
|
|||||||
match u16::deserialize(deserializer)? {
|
match u16::deserialize(deserializer)? {
|
||||||
0 => Ok(MaybeKelvin(None)),
|
0 => Ok(MaybeKelvin(None)),
|
||||||
value => {
|
value => {
|
||||||
let kelvin = Kelvin::try_from(value).map_err(|e| {
|
let kelvin = Kelvin::try_from(value).map_err(|_e| {
|
||||||
serde::de::Error::custom(format!(
|
serde::de::Error::custom(format!(
|
||||||
"{value} is not in the range {}..{}",
|
"{value} is not in the range {}..{}",
|
||||||
Kelvin::MIN,
|
Kelvin::MIN,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::convert::Infallible;
|
|
||||||
|
|
||||||
use palette::{encoding::Srgb, Hsv, IntoColor};
|
use palette::{encoding::Srgb, Hsv, IntoColor};
|
||||||
use protocol::light::{GetState, SetState, TurnToColor, TurnToTemperature};
|
use protocol::light::{GetState, SetState, TurnToColor, TurnToTemperature};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
@@ -7,8 +5,8 @@ use snafu::{ResultExt, Snafu};
|
|||||||
use crate::{
|
use crate::{
|
||||||
connection::{HandleError, LB130USHandle},
|
connection::{HandleError, LB130USHandle},
|
||||||
messages::{
|
messages::{
|
||||||
Angle, Hsb, LightState, Off, On, Percentage, SetLightHsv, SetLightKelvin, SetLightLastOn,
|
LightState, Off, On, SetLightHsv, SetLightKelvin, SetLightLastOn, SetLightOff,
|
||||||
SetLightOff, SetLightStateArgs, SetLightTo,
|
SetLightStateArgs, SetLightTo,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
pub mod emitter;
|
pub mod emitter;
|
||||||
pub mod emitter_ext;
|
mod emitter_ext;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
pub mod signal_ext;
|
mod signal_ext;
|
||||||
|
|
||||||
pub use emitter::Emitter;
|
pub use emitter::Emitter;
|
||||||
pub use emitter_ext::EmitterExt;
|
pub use emitter_ext::EmitterExt;
|
||||||
|
|||||||
@@ -1,30 +1,24 @@
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
use ext_trait::extension;
|
use ext_trait::extension;
|
||||||
use snafu::{ResultExt, Snafu};
|
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
|
|
||||||
use crate::ProducerExited;
|
use crate::ProducerExited;
|
||||||
|
|
||||||
use super::signal::{JoinError, Signal};
|
use super::signal::{JoinError, Signal};
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
pub struct ProducerAlreadyExited {
|
|
||||||
source: ProducerExited,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[extension(pub trait SignalExt)]
|
#[extension(pub trait SignalExt)]
|
||||||
impl<T> Signal<T> {
|
impl<T> Signal<T> {
|
||||||
fn map<M, F>(
|
fn map<M, F>(
|
||||||
self,
|
self,
|
||||||
mut func: F,
|
mut func: F,
|
||||||
) -> Result<(Signal<M>, impl Future<Output = Result<(), JoinError>>), ProducerAlreadyExited>
|
) -> Result<(Signal<M>, impl Future<Output = Result<(), JoinError>>), ProducerExited>
|
||||||
where
|
where
|
||||||
T: 'static + Sync + Send + Clone,
|
T: 'static + Sync + Send + Clone,
|
||||||
M: 'static + Sync + Send + Clone,
|
M: 'static + Sync + Send + Clone,
|
||||||
F: 'static + Send + FnMut(T) -> M,
|
F: 'static + Send + FnMut(T) -> M,
|
||||||
{
|
{
|
||||||
let initial = func(self.subscribe().context(ProducerAlreadyExitedSnafu)?.get());
|
let initial = func(self.subscribe()?.get());
|
||||||
|
|
||||||
Ok(Signal::new(initial, |mut publisher_stream| async move {
|
Ok(Signal::new(initial, |mut publisher_stream| async move {
|
||||||
while let Some(publisher) = publisher_stream.wait().await {
|
while let Some(publisher) = publisher_stream.wait().await {
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ pyo3 = { workspace = true, features = [
|
|||||||
"extension-module",
|
"extension-module",
|
||||||
] }
|
] }
|
||||||
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
|
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
|
||||||
shadow-rs = { version = "1.0.1", default-features = false }
|
shadow-rs = { version = "2", default-features = false }
|
||||||
snafu = { workspace = true }
|
snafu = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["time"] }
|
tokio = { workspace = true, features = ["time"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-appender = "0.2.3"
|
tracing-appender = "0.2.3"
|
||||||
tracing-subscriber = "0.3.17"
|
tracing-subscriber = "0.3.17"
|
||||||
uom = "0.36.0"
|
uom = "0.38.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
shadow-rs = "1.0.1"
|
shadow-rs = "2"
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
use std::{num::NonZeroUsize, path::PathBuf, str::FromStr, time::Duration};
|
use std::{path::PathBuf, str::FromStr, time::Duration};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use driver_kasa::connection::LB130USHandle;
|
use driver_kasa::connection::LB130USHandle;
|
||||||
use futures::stream::{FuturesUnordered, StreamExt};
|
use futures::{TryFutureExt, stream::{FuturesUnordered, StreamExt}};
|
||||||
use home_assistant::{
|
use home_assistant::{home_assistant::HomeAssistant, object_id::ObjectId};
|
||||||
event::context::context::Context, home_assistant::HomeAssistant, light::HomeAssistantLight,
|
use protocol::light::{Kelvin, TurnToTemperature};
|
||||||
notify::service::mobile_app::StandardNotification, object_id::ObjectId,
|
use pyo3::{
|
||||||
|
pyfunction, pymodule,
|
||||||
|
types::{PyModule, PyModuleMethods as _},
|
||||||
|
wrap_pyfunction, Bound, PyAny, PyResult, Python,
|
||||||
};
|
};
|
||||||
use persisted::{persisted, Config, Keyspace, Partition, PartitionCreateOptions};
|
|
||||||
use protocol::light::{IsOff, IsOn, Kelvin, TurnToTemperature};
|
|
||||||
use pyo3::prelude::*;
|
|
||||||
use shadow_rs::shadow;
|
use shadow_rs::shadow;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
task::{spawn_blocking, JoinSet},
|
join, task::JoinSet, time::{MissedTickBehavior, interval}
|
||||||
time::{interval, MissedTickBehavior},
|
|
||||||
};
|
};
|
||||||
use tracing::{level_filters::LevelFilter, Level};
|
use tracing::{level_filters::LevelFilter, Level};
|
||||||
use tracing_appender::rolling::{self, RollingFileAppender};
|
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
fmt::{self, fmt, format::FmtSpan},
|
fmt::{self, format::FmtSpan},
|
||||||
layer::SubscriberExt,
|
layer::SubscriberExt,
|
||||||
registry,
|
registry,
|
||||||
util::SubscriberInitExt,
|
util::SubscriberInitExt,
|
||||||
@@ -191,8 +189,61 @@ async fn real_main(
|
|||||||
// }, context, target, false).await;
|
// }, context, target, false).await;
|
||||||
// dbg!(total_silence_result);
|
// dbg!(total_silence_result);
|
||||||
// }
|
// }
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
let plug_awp04l_1_power_object_id = "plug_awp04l_1_power";
|
||||||
|
let plug_awp04l_2_power_object_id = "plug_awp04l_2_power";
|
||||||
|
|
||||||
|
let plug_awp04l_1_power_object_id = ObjectId::from_str(plug_awp04l_1_power_object_id)
|
||||||
|
.expect("statically written and known to be correct");
|
||||||
|
let plug_awp04l_2_power_object_id = ObjectId::from_str(plug_awp04l_2_power_object_id)
|
||||||
|
.expect("statically written and known to be correct");
|
||||||
|
|
||||||
|
let plug_awp04l_1_power_signal_result = Python::attach(|py| {
|
||||||
|
home_assistant::sensor::device_classes::power::signal::<f64>(
|
||||||
|
py,
|
||||||
|
&home_assistant,
|
||||||
|
plug_awp04l_1_power_object_id,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let plug_awp04l_2_power_signal_result = Python::attach(|py| {
|
||||||
|
home_assistant::sensor::device_classes::power::signal::<f64>(
|
||||||
|
py,
|
||||||
|
&home_assistant,
|
||||||
|
plug_awp04l_2_power_object_id,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut tasks = JoinSet::new();
|
||||||
|
if let Ok((plug_awp04l_1_power_signal, task)) = plug_awp04l_1_power_signal_result {
|
||||||
|
tracing::error!("listening to the plug_awp04l_1_power_signal");
|
||||||
|
tasks.spawn(task.unwrap_or_else(|e| panic!("TODO: {e}")));
|
||||||
|
tasks.spawn(
|
||||||
|
plug_awp04l_1_power_signal
|
||||||
|
.subscribe()
|
||||||
|
.expect("TODO")
|
||||||
|
.for_each(|plug_awp04l_1_power| async move {
|
||||||
|
tracing::warn!(?plug_awp04l_1_power)
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
).0
|
if let Ok((plug_awp04l_2_power_signal, task)) = plug_awp04l_2_power_signal_result {
|
||||||
|
tracing::error!("listening to the plug_awp04l_2_power_signal");
|
||||||
|
tasks.spawn(task.unwrap_or_else(|e| panic!("TODO: {e}")));
|
||||||
|
tasks.spawn(
|
||||||
|
plug_awp04l_2_power_signal
|
||||||
|
.subscribe()
|
||||||
|
.expect("TODO")
|
||||||
|
.for_each(|plug_awp04l_2_power| async move {
|
||||||
|
tracing::warn!(?plug_awp04l_2_power)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.join_all().await;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
|
|||||||
@@ -24,12 +24,13 @@ 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", features = ["macros"] }
|
||||||
snafu = { workspace = true }
|
snafu = { workspace = true }
|
||||||
|
string-literal = { path = "../string-literal", features = ["macros"] }
|
||||||
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 }
|
typed-builder = { workspace = true }
|
||||||
ulid = "1.2.0"
|
ulid = "2"
|
||||||
uom = "0.37.0"
|
uom = "0.38"
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
use std::{convert::Infallible, fmt::Display, str::FromStr};
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
use pyo3::{
|
|
||||||
exceptions::{PyException, PyValueError},
|
|
||||||
prelude::*,
|
|
||||||
types::PyString,
|
|
||||||
};
|
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
domain::Domain,
|
domain::Domain,
|
||||||
object_id::{ObjectId, ObjectIdParsingError},
|
object_id::{ObjectId, ObjectIdParsingError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, FromPyFromStr, ToStrToPy)]
|
||||||
pub struct EntityId(pub Domain, pub ObjectId);
|
pub struct EntityId(pub Domain, pub ObjectId);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Snafu)]
|
#[derive(Debug, Clone, Snafu)]
|
||||||
pub enum EntityIdParsingError {
|
pub enum EntityIdParsingError {
|
||||||
#[snafu(display("entity IDs have a dot / period in them, e.g. light.kitchen_lamp"))]
|
/// entity IDs have a dot / period in them, e.g. light.kitchen_lamp
|
||||||
MissingDot,
|
MissingDot,
|
||||||
|
|
||||||
#[snafu(display("could not parse the domain part of the entity ID"))]
|
/// could not parse the domain part of the entity ID
|
||||||
ParsingDomain { source: <Domain as FromStr>::Err },
|
ParsingDomain { source: <Domain as FromStr>::Err },
|
||||||
|
|
||||||
#[snafu(display("could not parse the object ID part of the entity ID"))]
|
/// could not parse the object ID part of the entity ID
|
||||||
ParsingObjectId { source: ObjectIdParsingError },
|
ParsingObjectId { source: ObjectIdParsingError },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,47 +42,3 @@ impl Display for EntityId {
|
|||||||
write!(f, "{domain}.{object_id}")
|
write!(f, "{domain}.{object_id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
pub enum ExtractEntityIdError {
|
|
||||||
/// 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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
|
||||||
impl<'a, 'py> FromPyObject<'a, 'py> for EntityId {
|
|
||||||
type Error = ExtractEntityIdError;
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
|
||||||
impl<'py> IntoPyObject<'py> for EntityId {
|
|
||||||
type Target = PyString;
|
|
||||||
type Output = Bound<'py, Self::Target>;
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
|
||||||
let s = self.to_string();
|
|
||||||
s.into_pyobject(py)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
use super::id::Id;
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use pyo3::{prelude::*, types::PyType};
|
|
||||||
|
|
||||||
/// The context that triggered something.
|
|
||||||
#[derive(Debug, FromPyObject)]
|
|
||||||
pub struct Context<Event> {
|
|
||||||
pub id: Id,
|
|
||||||
pub user_id: Option<String>,
|
|
||||||
pub parent_id: Option<String>,
|
|
||||||
/// In order to prevent cycles, the user must decide to pass [`Py<PyAny>`] for the `Event` type here
|
|
||||||
/// or for the `Context` type in [`Event`]
|
|
||||||
pub origin_event: Event,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'py, Event: IntoPyObject<'py>> IntoPyObject<'py> for Context<Event> {
|
|
||||||
type Target = PyAny;
|
|
||||||
|
|
||||||
type Output = Bound<'py, Self::Target>;
|
|
||||||
|
|
||||||
type Error = PyErr;
|
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
|
||||||
static HOMEASSISTANT_CORE: OnceCell<Py<PyModule>> = OnceCell::new();
|
|
||||||
|
|
||||||
let homeassistant_core = HOMEASSISTANT_CORE
|
|
||||||
.get_or_try_init(|| Result::<_, PyErr>::Ok(py.import("homeassistant.core")?.unbind()))?
|
|
||||||
.bind(py);
|
|
||||||
|
|
||||||
let context_class = homeassistant_core.getattr("Context")?;
|
|
||||||
let context_class = context_class.cast_into::<PyType>()?;
|
|
||||||
|
|
||||||
let context_instance = context_class.call1((self.user_id, self.parent_id, self.id))?;
|
|
||||||
|
|
||||||
context_instance.setattr("origin_event", self.origin_event)?;
|
|
||||||
|
|
||||||
Ok(context_instance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
home-assistant/src/event/context/context_id.rs
Normal file
41
home-assistant/src/event/context/context_id.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use std::{convert::Infallible, fmt::Display, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, FromPyFromStr, ToStrToPy)]
|
||||||
|
pub enum ContextId {
|
||||||
|
Ulid(Ulid),
|
||||||
|
Other(Arc<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ContextId {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
if let Ok(ulid) = s.parse() {
|
||||||
|
ContextId::Ulid(ulid)
|
||||||
|
} else {
|
||||||
|
ContextId::Other(s.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ContextId {
|
||||||
|
type Err = Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(ulid) = s.parse() {
|
||||||
|
Ok(ContextId::Ulid(ulid))
|
||||||
|
} else {
|
||||||
|
Ok(ContextId::Other(s.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ContextId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ContextId::Ulid(ulid) => write!(f, "{ulid}"),
|
||||||
|
ContextId::Other(other) => write!(f, "{other}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
use std::{convert::Infallible, sync::Arc};
|
|
||||||
|
|
||||||
use pyo3::{exceptions::PyTypeError, prelude::*, types::PyString};
|
|
||||||
use snafu::{ResultExt, Snafu};
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Id {
|
|
||||||
Ulid(Ulid),
|
|
||||||
Other(Arc<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
pub enum ExtractIdError {
|
|
||||||
/// 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() {
|
|
||||||
Ok(Id::Ulid(ulid))
|
|
||||||
} else {
|
|
||||||
Ok(Id::Other(s.into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
|
||||||
impl<'py> IntoPyObject<'py> for Id {
|
|
||||||
type Target = PyString;
|
|
||||||
|
|
||||||
type Output = Bound<'py, Self::Target>;
|
|
||||||
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
|
||||||
match self {
|
|
||||||
Id::Ulid(ulid) => ulid.to_string().into_pyobject(py),
|
|
||||||
Id::Other(id) => id.into_pyobject(py),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,44 @@
|
|||||||
pub mod context;
|
use once_cell::sync::OnceCell;
|
||||||
pub mod id;
|
use pyo3::{
|
||||||
|
types::{PyAnyMethods, PyModule, PyType},
|
||||||
|
Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, Python,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod context_id;
|
||||||
|
pub use context_id::ContextId;
|
||||||
|
|
||||||
|
/// The context that triggered something.
|
||||||
|
#[derive(Debug, FromPyObject)]
|
||||||
|
pub struct Context<Event> {
|
||||||
|
pub id: ContextId,
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
pub parent_id: Option<String>,
|
||||||
|
/// In order to prevent cycles, the user must decide to pass [`Py<PyAny>`] for the `Event` type here
|
||||||
|
/// or for the `Context` type in [`Event`]
|
||||||
|
pub origin_event: Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'py, Event: IntoPyObject<'py>> IntoPyObject<'py> for Context<Event> {
|
||||||
|
type Target = PyAny;
|
||||||
|
|
||||||
|
type Output = Bound<'py, Self::Target>;
|
||||||
|
|
||||||
|
type Error = PyErr;
|
||||||
|
|
||||||
|
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||||
|
static HOMEASSISTANT_CORE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||||
|
|
||||||
|
let homeassistant_core = HOMEASSISTANT_CORE
|
||||||
|
.get_or_try_init(|| Result::<_, PyErr>::Ok(py.import("homeassistant.core")?.unbind()))?
|
||||||
|
.bind(py);
|
||||||
|
|
||||||
|
let context_class = homeassistant_core.getattr("Context")?;
|
||||||
|
let context_class = context_class.cast_into::<PyType>()?;
|
||||||
|
|
||||||
|
let context_instance = context_class.call1((self.user_id, self.parent_id, self.id))?;
|
||||||
|
|
||||||
|
context_instance.setattr("origin_event", self.origin_event)?;
|
||||||
|
|
||||||
|
Ok(context_instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
use chrono::{DateTime, Utc};
|
|
||||||
use pyo3::prelude::*;
|
|
||||||
|
|
||||||
use super::event_origin::EventOrigin;
|
|
||||||
|
|
||||||
/// Representation of an event within the bus.
|
|
||||||
#[derive(Debug, FromPyObject)]
|
|
||||||
pub struct Event<Type, Data, Context> {
|
|
||||||
pub event_type: Type,
|
|
||||||
pub data: Data,
|
|
||||||
pub origin: EventOrigin,
|
|
||||||
/// In order to prevent cycles, the user must decide to pass [`Py<PyAny>`] for the `Context` type here
|
|
||||||
/// or for the `Event` type in [`Context`]
|
|
||||||
pub context: Context,
|
|
||||||
time_fired_timestamp: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Type, Data, Context> Event<Type, Data, Context> {
|
|
||||||
pub fn time_fired(&self) -> Option<DateTime<Utc>> {
|
|
||||||
const NANOS_PER_SEC: i32 = 1_000_000_000;
|
|
||||||
|
|
||||||
let secs = self.time_fired_timestamp as i64;
|
|
||||||
let nsecs = (self.time_fired_timestamp.fract() * (NANOS_PER_SEC as f64)) as u32;
|
|
||||||
|
|
||||||
DateTime::from_timestamp(secs, nsecs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,8 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
exceptions::{PyException, PyTypeError, PyValueError},
|
exceptions::{PyException, PyTypeError, PyValueError},
|
||||||
prelude::*,
|
types::PyAnyMethods,
|
||||||
|
Borrowed, FromPyObject, PyAny, PyErr,
|
||||||
};
|
};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod event;
|
|
||||||
pub mod event_origin;
|
pub mod event_origin;
|
||||||
pub mod specific;
|
pub mod specific;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use pyo3::FromPyObject;
|
||||||
|
|
||||||
|
pub use event_origin::{EventOrigin, ExtractEventOriginError};
|
||||||
|
|
||||||
|
/// Representation of an event within the bus.
|
||||||
|
#[derive(Debug, FromPyObject)]
|
||||||
|
pub struct Event<Type, Data, Context> {
|
||||||
|
pub event_type: Type,
|
||||||
|
pub data: Data,
|
||||||
|
pub origin: EventOrigin,
|
||||||
|
/// In order to prevent cycles, the user must decide to pass [`Py<PyAny>`] for the `Context` type here
|
||||||
|
/// or for the `Event` type in [`Context`]
|
||||||
|
pub context: Context,
|
||||||
|
time_fired_timestamp: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Type, Data, Context> Event<Type, Data, Context> {
|
||||||
|
pub fn time_fired(&self) -> Option<DateTime<Utc>> {
|
||||||
|
const NANOS_PER_SEC: i32 = 1_000_000_000;
|
||||||
|
|
||||||
|
let secs = self.time_fired_timestamp as i64;
|
||||||
|
let nsecs = (self.time_fired_timestamp.fract() * (NANOS_PER_SEC as f64)) as u32;
|
||||||
|
|
||||||
|
DateTime::from_timestamp(secs, nsecs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,45 +1,13 @@
|
|||||||
use pyo3::exceptions::{PyTypeError, PyValueError};
|
use pyo3::FromPyObject;
|
||||||
use pyo3::prelude::*;
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
use snafu::{ResultExt, Snafu};
|
use string_literal::StringLiteral;
|
||||||
|
|
||||||
use crate::{entity_id::EntityId, state_object::StateObject};
|
use crate::{entity_id::EntityId, state_object::StateObject};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, StringLiteral, FromPyFromStr, ToStrToPy)]
|
||||||
|
#[string_literal(value = "state_changed")]
|
||||||
pub struct Type;
|
pub struct Type;
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
pub enum ExtractTypeError {
|
|
||||||
/// 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" {
|
|
||||||
Ok(Type)
|
|
||||||
} else {
|
|
||||||
Err(ExtractTypeError::UnexpectedValue { actual: s.into() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, FromPyObject)]
|
#[derive(Debug, FromPyObject)]
|
||||||
#[pyo3(from_item_all)]
|
#[pyo3(from_item_all)]
|
||||||
pub struct Data<
|
pub struct Data<
|
||||||
@@ -64,7 +32,7 @@ pub type Event<
|
|||||||
NewAttributes,
|
NewAttributes,
|
||||||
NewStateContextEvent,
|
NewStateContextEvent,
|
||||||
Context,
|
Context,
|
||||||
> = super::super::event::Event<
|
> = super::super::Event<
|
||||||
Type,
|
Type,
|
||||||
Data<
|
Data<
|
||||||
OldState,
|
OldState,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::convert::Infallible;
|
use std::{convert::Infallible, sync::Arc};
|
||||||
|
|
||||||
use pyo3::prelude::*;
|
|
||||||
|
|
||||||
|
use pyo3::{
|
||||||
|
types::PyAnyMethods as _, Borrowed, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, Python,
|
||||||
|
};
|
||||||
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
@@ -32,22 +33,26 @@ impl<'py> IntoPyObject<'py> for &HomeAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Clone, Snafu)]
|
||||||
pub enum GetStatesError {
|
pub enum GetStatesError {
|
||||||
/// couldn't get the `states` attribute on the Home Assistant object
|
/// couldn't get the `states` attribute on the Home Assistant object
|
||||||
GetStatesAttributeError { source: PyErr },
|
GetStatesAttributeError { source: Arc<PyErr> },
|
||||||
|
|
||||||
/// couldn't extract the `states` as a [`StateMachine`]
|
/// couldn't extract the `states` as a [`StateMachine`]
|
||||||
ExtractStateMachineError { source: TypeByNameValidationError },
|
ExtractStateMachineError {
|
||||||
|
source: Arc<TypeByNameValidationError>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Clone, Snafu)]
|
||||||
pub enum GetServicesError {
|
pub enum GetServicesError {
|
||||||
/// couldn't get the `services` attribute on the Home Assistant object
|
/// couldn't get the `services` attribute on the Home Assistant object
|
||||||
GetServicesAttributeError { source: PyErr },
|
GetServicesAttributeError { source: Arc<PyErr> },
|
||||||
|
|
||||||
/// couldn't extract the `states` as a [`ServiceRegistry`]
|
/// couldn't extract the `states` as a [`ServiceRegistry`]
|
||||||
ExtractServiceRegistryError { source: TypeByNameValidationError },
|
ExtractServiceRegistryError {
|
||||||
|
source: Arc<TypeByNameValidationError>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HomeAssistant {
|
impl HomeAssistant {
|
||||||
@@ -73,15 +78,23 @@ impl HomeAssistant {
|
|||||||
let states = self
|
let states = self
|
||||||
.0
|
.0
|
||||||
.getattr(py, "states")
|
.getattr(py, "states")
|
||||||
|
.map_err(Arc::new)
|
||||||
.context(GetStatesAttributeSnafu)?;
|
.context(GetStatesAttributeSnafu)?;
|
||||||
states.extract(py).context(ExtractStateMachineSnafu)
|
states
|
||||||
|
.extract(py)
|
||||||
|
.map_err(Arc::new)
|
||||||
|
.context(ExtractStateMachineSnafu)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, GetServicesError> {
|
pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, GetServicesError> {
|
||||||
let services = self
|
let services = self
|
||||||
.0
|
.0
|
||||||
.getattr(py, "services")
|
.getattr(py, "services")
|
||||||
|
.map_err(Arc::new)
|
||||||
.context(GetServicesAttributeSnafu)?;
|
.context(GetServicesAttributeSnafu)?;
|
||||||
services.extract(py).context(ExtractServiceRegistrySnafu)
|
services
|
||||||
|
.extract(py)
|
||||||
|
.map_err(Arc::new)
|
||||||
|
.context(ExtractServiceRegistrySnafu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
home-assistant/src/input_number/attributes.rs
Normal file
16
home-assistant/src/input_number/attributes.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use pyo3::FromPyObject;
|
||||||
|
|
||||||
|
use super::InputNumberMode;
|
||||||
|
|
||||||
|
#[derive(Debug, FromPyObject)]
|
||||||
|
#[pyo3(from_item_all)]
|
||||||
|
pub struct InputNumberAttributes<Number> {
|
||||||
|
initial: Option<Number>,
|
||||||
|
editable: bool,
|
||||||
|
min: Number,
|
||||||
|
max: Number,
|
||||||
|
step: Number,
|
||||||
|
mode: InputNumberMode,
|
||||||
|
// todo: CustomUnitOfMeasurement type? probably not?
|
||||||
|
unit_of_measurement: Option<String>,
|
||||||
|
}
|
||||||
@@ -1 +1,75 @@
|
|||||||
|
use std::{future::Future, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use emitter_and_signal::{Signal, SignalExt};
|
||||||
|
use pyo3::{FromPyObject, Py, PyAny, PyErr, Python};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::Domain,
|
||||||
|
entity_id::EntityId,
|
||||||
|
home_assistant::HomeAssistant,
|
||||||
|
object_id::ObjectId,
|
||||||
|
state::HomeAssistantState,
|
||||||
|
state_object::{self, StateObject, StateObjectSignalError},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod attributes;
|
||||||
|
mod mode;
|
||||||
|
|
||||||
|
pub use attributes::InputNumberAttributes;
|
||||||
|
pub use mode::InputNumberMode;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, 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::ProducerExited,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signal<
|
||||||
|
'py,
|
||||||
|
Number: 'static + uom::num::Num + Clone + Send + Sync + FromStr + for<'a, 'py2> FromPyObject<'a, 'py2>,
|
||||||
|
>(
|
||||||
|
py: Python<'py>,
|
||||||
|
home_assistant: &'py HomeAssistant,
|
||||||
|
object_id: ObjectId,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
Signal<Option<Arc<Result<HomeAssistantState<Number>, StateObjectSignalError<Arc<PyErr>>>>>>,
|
||||||
|
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
|
||||||
|
),
|
||||||
|
CreateSignalError,
|
||||||
|
> {
|
||||||
|
let entity_id = EntityId(Domain::InputNumber, object_id);
|
||||||
|
|
||||||
|
let (signal, task1) = StateObject::<
|
||||||
|
HomeAssistantState<Number>,
|
||||||
|
InputNumberAttributes<Number>,
|
||||||
|
Py<PyAny>,
|
||||||
|
>::signal(py, home_assistant, entity_id)
|
||||||
|
.context(StateObjectSignalSnafu)?;
|
||||||
|
|
||||||
|
let (signal, task2) = signal
|
||||||
|
.map(|state_object_arc_result_option| {
|
||||||
|
state_object_arc_result_option.map(|state_object_arc_result| {
|
||||||
|
Arc::new(
|
||||||
|
(*state_object_arc_result)
|
||||||
|
.as_ref()
|
||||||
|
.map(|state_object| state_object.state.clone())
|
||||||
|
.map_err(|e| e.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.context(MappedSignalSnafu)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
signal,
|
||||||
|
async move { tokio::try_join!(task1, task2).map(|_| ()) },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
10
home-assistant/src/input_number/mode.rs
Normal file
10
home-assistant/src/input_number/mode.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum InputNumberMode {
|
||||||
|
Box,
|
||||||
|
#[default]
|
||||||
|
Slider,
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use pyo3::prelude::*;
|
use pyo3::FromPyObject;
|
||||||
|
|
||||||
#[derive(Debug, FromPyObject)]
|
#[derive(Debug, FromPyObject)]
|
||||||
#[pyo3(from_item_all)]
|
#[pyo3(from_item_all)]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use attributes::LightAttributes;
|
use attributes::LightAttributes;
|
||||||
use pyo3::prelude::*;
|
use pyo3::{FromPyObject, Py, PyAny, Python};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
use state::LightState;
|
use state::LightState;
|
||||||
|
|
||||||
@@ -29,15 +31,15 @@ impl HomeAssistantLight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Clone, Snafu)]
|
||||||
pub enum GetStateObjectError {
|
pub enum GetStateObjectError {
|
||||||
/// couldn't get the state machine registry
|
/// couldn't get the state machine registry
|
||||||
GetStatesError { source: GetStatesError },
|
GetStatesError { source: GetStatesError },
|
||||||
|
|
||||||
/// this state object exists in the state machine registry, but it couldn't be extracted as a light state object
|
/// this state object exists in the state machine registry, but it couldn't be extracted as a light state object
|
||||||
GetStateError { source: GetStateError<
|
GetStateError { source: Arc<GetStateError<
|
||||||
<StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>> as FromPyObject<'static, 'static>>::Error
|
<StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>> as FromPyObject<'static, 'static>>::Error
|
||||||
> },
|
>> },
|
||||||
|
|
||||||
/// this entity does not have a state object in the registry
|
/// this entity does not have a state object in the registry
|
||||||
EntityMissing,
|
EntityMissing,
|
||||||
@@ -55,6 +57,7 @@ impl HomeAssistantLight {
|
|||||||
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)
|
||||||
|
.map_err(Arc::new)
|
||||||
.context(GetStateSnafu)?
|
.context(GetStateSnafu)?
|
||||||
.ok_or(GetStateObjectError::EntityMissing)?;
|
.ok_or(GetStateObjectError::EntityMissing)?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ use super::{GetStateObjectError, HomeAssistantLight};
|
|||||||
use crate::home_assistant::GetServicesError;
|
use crate::home_assistant::GetServicesError;
|
||||||
use crate::service_registry::CallServiceError;
|
use crate::service_registry::CallServiceError;
|
||||||
use crate::{
|
use crate::{
|
||||||
event::context::context::Context,
|
event::context::Context,
|
||||||
state::{ErrorState, HomeAssistantState, UnexpectedState},
|
state::{ErrorState, HomeAssistantState, UnexpectedState},
|
||||||
};
|
};
|
||||||
use protocol::light::{GetState, SetState};
|
use protocol::light::{GetState, SetState};
|
||||||
use pyo3::prelude::*;
|
use pyo3::Python;
|
||||||
use python_utils::IsNone;
|
use python_utils::IsNone;
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
@@ -30,9 +30,7 @@ impl GetState for HomeAssistantLight {
|
|||||||
HomeAssistantState::Err(error_state) => {
|
HomeAssistantState::Err(error_state) => {
|
||||||
Err(GetStateError::Error { state: error_state })
|
Err(GetStateError::Error { state: error_state })
|
||||||
}
|
}
|
||||||
HomeAssistantState::UnexpectedErr(state) => {
|
HomeAssistantState::Unexpected(state) => Err(GetStateError::UnexpectedError { state }),
|
||||||
Err(GetStateError::UnexpectedError { state })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,13 @@
|
|||||||
use std::str::FromStr;
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
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, FromPyFromStr, ToStrToPy)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum LightState {
|
pub enum LightState {
|
||||||
On,
|
On,
|
||||||
Off,
|
Off,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
pub enum ExtractLightStateError {
|
|
||||||
/// couldn't extract the object as a string
|
|
||||||
ExtractStringError { source: PyErr },
|
|
||||||
|
|
||||||
/// couldn't parse the string as a [`LightState`]
|
|
||||||
ParseError {
|
|
||||||
source: <LightState as FromStr>::Err,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ExtractLightStateError> for PyErr {
|
|
||||||
fn from(error: ExtractLightStateError) -> Self {
|
|
||||||
match &error {
|
|
||||||
ExtractLightStateError::ExtractStringError { .. } => {
|
|
||||||
PyException::new_err(error.to_string())
|
|
||||||
}
|
|
||||||
ExtractLightStateError::ParseError { .. } => PyValueError::new_err(error.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
|
||||||
impl<'a, 'py> FromPyObject<'a, 'py> for LightState {
|
|
||||||
type Error = ExtractLightStateError;
|
|
||||||
|
|
||||||
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
|
||||||
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
|
|
||||||
|
|
||||||
let state = LightState::from_str(&s).context(ParseSnafu)?;
|
|
||||||
|
|
||||||
Ok(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LightState> for protocol::light::State {
|
impl From<LightState> for protocol::light::State {
|
||||||
fn from(light_state: LightState) -> Self {
|
fn from(light_state: LightState) -> Self {
|
||||||
match light_state {
|
match light_state {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
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::{
|
||||||
|
types::{PyAnyMethods as _, PyModule, PyTuple},
|
||||||
|
Borrowed, FromPyObject, IntoPyObject, Py, PyAny, PyErr, Python,
|
||||||
|
};
|
||||||
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -54,7 +57,7 @@ pub struct LogData<ExcInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HassLogger {
|
impl HassLogger {
|
||||||
pub fn new(py: Python<'_>, name: &str) -> PyResult<Self> {
|
pub fn new(py: Python<'_>, name: &str) -> Result<Self, PyErr> {
|
||||||
static LOGGING_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
static LOGGING_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||||
|
|
||||||
let logging_module = LOGGING_MODULE
|
let logging_module = LOGGING_MODULE
|
||||||
@@ -71,7 +74,7 @@ impl HassLogger {
|
|||||||
msg: &str,
|
msg: &str,
|
||||||
args: Vec<Arbitrary>,
|
args: Vec<Arbitrary>,
|
||||||
log_data: Option<LogData<ExcInfo>>,
|
log_data: Option<LogData<ExcInfo>>,
|
||||||
) -> PyResult<()> {
|
) -> Result<(), PyErr> {
|
||||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||||
for arg in args {
|
for arg in args {
|
||||||
let arg = arg.into_pyobject(py)?;
|
let arg = arg.into_pyobject(py)?;
|
||||||
@@ -94,7 +97,7 @@ impl HassLogger {
|
|||||||
msg: &str,
|
msg: &str,
|
||||||
args: Vec<Arbitrary>,
|
args: Vec<Arbitrary>,
|
||||||
log_data: Option<LogData<ExcInfo>>,
|
log_data: Option<LogData<ExcInfo>>,
|
||||||
) -> PyResult<()> {
|
) -> Result<(), PyErr> {
|
||||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||||
for arg in args {
|
for arg in args {
|
||||||
let arg = arg.into_pyobject(py)?;
|
let arg = arg.into_pyobject(py)?;
|
||||||
@@ -117,7 +120,7 @@ impl HassLogger {
|
|||||||
msg: &str,
|
msg: &str,
|
||||||
args: Vec<Arbitrary>,
|
args: Vec<Arbitrary>,
|
||||||
log_data: Option<LogData<ExcInfo>>,
|
log_data: Option<LogData<ExcInfo>>,
|
||||||
) -> PyResult<()> {
|
) -> Result<(), PyErr> {
|
||||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||||
for arg in args {
|
for arg in args {
|
||||||
let arg = arg.into_pyobject(py)?;
|
let arg = arg.into_pyobject(py)?;
|
||||||
@@ -141,7 +144,7 @@ impl HassLogger {
|
|||||||
msg: &str,
|
msg: &str,
|
||||||
args: Vec<Arbitrary>,
|
args: Vec<Arbitrary>,
|
||||||
log_data: Option<LogData<ExcInfo>>,
|
log_data: Option<LogData<ExcInfo>>,
|
||||||
) -> PyResult<()> {
|
) -> Result<(), PyErr> {
|
||||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||||
for arg in args {
|
for arg in args {
|
||||||
let arg = arg.into_pyobject(py)?;
|
let arg = arg.into_pyobject(py)?;
|
||||||
@@ -164,7 +167,7 @@ impl HassLogger {
|
|||||||
msg: &str,
|
msg: &str,
|
||||||
args: Vec<Arbitrary>,
|
args: Vec<Arbitrary>,
|
||||||
log_data: Option<LogData<ExcInfo>>,
|
log_data: Option<LogData<ExcInfo>>,
|
||||||
) -> PyResult<()> {
|
) -> Result<(), PyErr> {
|
||||||
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
let mut all_args = vec![msg.into_pyobject(py)?.into_any()];
|
||||||
for arg in args {
|
for arg in args {
|
||||||
let arg = arg.into_pyobject(py)?;
|
let arg = arg.into_pyobject(py)?;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
use crate::{
|
use crate::service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall};
|
||||||
notify::service::mobile_app::SpecialMessage,
|
|
||||||
object_id::ObjectId,
|
|
||||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::super::{NotifyMobileAppServiceData, NotifyMobileAppServiceDataData};
|
use super::super::{
|
||||||
|
MobileAppDeviceId, NotifyMobileAppServiceData, NotifyMobileAppServiceDataData, SpecialMessage,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, EnumString, strum::Display)]
|
#[derive(Debug, Clone, EnumString, strum::Display)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
@@ -33,7 +31,7 @@ pub enum Filter {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
||||||
pub struct DoNotDisturb {
|
pub struct DoNotDisturb {
|
||||||
pub object_id: ObjectId,
|
pub device_id: MobileAppDeviceId,
|
||||||
pub filter: Filter,
|
pub filter: Filter,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,11 +39,11 @@ impl IntoServiceCall for DoNotDisturb {
|
|||||||
type ServiceData = NotifyMobileAppServiceData;
|
type ServiceData = NotifyMobileAppServiceData;
|
||||||
|
|
||||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||||
let DoNotDisturb { object_id, filter } = self;
|
let DoNotDisturb { device_id, filter } = 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_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_id = ServiceId::from_str(&format!("mobile_app_{device_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()
|
let service_data = NotifyMobileAppServiceData::builder()
|
||||||
.message(SpecialMessage::CommandDnd.to_string())
|
.message(SpecialMessage::CommandDnd.to_string())
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall};
|
||||||
notify::service::mobile_app::SpecialMessage,
|
|
||||||
object_id::ObjectId,
|
|
||||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::super::NotifyMobileAppServiceData;
|
use super::super::{MobileAppDeviceId, NotifyMobileAppServiceData, SpecialMessage};
|
||||||
|
|
||||||
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
||||||
pub struct RequestLocationUpdate {
|
pub struct RequestLocationUpdate {
|
||||||
pub object_id: ObjectId,
|
pub device_id: MobileAppDeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoServiceCall for RequestLocationUpdate {
|
impl IntoServiceCall for RequestLocationUpdate {
|
||||||
type ServiceData = NotifyMobileAppServiceData;
|
type ServiceData = NotifyMobileAppServiceData;
|
||||||
|
|
||||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||||
let RequestLocationUpdate { object_id } = self;
|
let RequestLocationUpdate { device_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_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_id = ServiceId::from_str(&format!("mobile_app_{device_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()
|
let service_data = NotifyMobileAppServiceData::builder()
|
||||||
.message(SpecialMessage::RequestLocationUpdate.to_string())
|
.message(SpecialMessage::RequestLocationUpdate.to_string())
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
|
use crate::slug::Slug;
|
||||||
|
|
||||||
|
pub use crate::slug::SlugParsingError as ParseMobileAppDeviceIdError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::FromStr, derive_more::Display, FromPyFromStr, ToStrToPy)]
|
||||||
|
pub struct MobileAppDeviceId(pub Slug);
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
use std::{convert::Infallible, str::FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
use pyo3::{types::{PyDict, PyString, PyDictMethods}, Bound, IntoPyObject, Python, PyErr};
|
use pyo3::{
|
||||||
use python_utils::IntoPyObjectViaDisplay;
|
types::{PyDict, PyDictMethods},
|
||||||
|
Bound, IntoPyObject, PyErr, Python,
|
||||||
|
};
|
||||||
|
use python_utils::{FromPyFromStr, IntoPyObjectViaDisplay, ToStrToPy};
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub mod command;
|
pub mod command;
|
||||||
|
pub mod device_id;
|
||||||
pub mod standard;
|
pub mod standard;
|
||||||
pub mod text_to_speech;
|
pub mod text_to_speech;
|
||||||
|
|
||||||
pub use command::*;
|
pub use command::*;
|
||||||
|
pub use device_id::MobileAppDeviceId;
|
||||||
pub use standard::StandardNotification;
|
pub use standard::StandardNotification;
|
||||||
pub use text_to_speech::TextToSpeech;
|
pub use text_to_speech::TextToSpeech;
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@ impl TryFrom<String> for NonSpecialMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// How much of a notification is visible on the lock screen
|
/// How much of a notification is visible on the lock screen
|
||||||
#[derive(Debug, Clone, Default, EnumString, strum::Display)]
|
#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum Visibility {
|
pub enum Visibility {
|
||||||
/// always show all notification content
|
/// always show all notification content
|
||||||
@@ -98,38 +103,14 @@ pub enum Visibility {
|
|||||||
Secret,
|
Secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
#[derive(Debug, Clone, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
|
||||||
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 {
|
pub enum Behavior {
|
||||||
/// prompt for text to return with the event
|
/// prompt for text to return with the event
|
||||||
#[strum(serialize = "textInput")]
|
#[strum(serialize = "textInput")]
|
||||||
TextInput,
|
TextInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one
|
#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
|
||||||
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")]
|
#[strum(serialize_all = "camelCase")]
|
||||||
pub enum ActivationMode {
|
pub enum ActivationMode {
|
||||||
/// launch the app when tapped
|
/// launch the app when tapped
|
||||||
@@ -139,18 +120,6 @@ pub enum ActivationMode {
|
|||||||
Background,
|
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>
|
// TODO: better typed versions like `CallNumber` or `OpenWebpage` where Action: From<CallNumber> and Action: From<OpenWebPage>
|
||||||
#[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)]
|
#[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)]
|
||||||
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
|
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
|
||||||
@@ -190,7 +159,7 @@ pub struct Action {
|
|||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, EnumString, strum::Display)]
|
#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
|
||||||
#[strum(serialize_all = "snake_case", suffix = "_stream")]
|
#[strum(serialize_all = "snake_case", suffix = "_stream")]
|
||||||
pub enum MediaStream {
|
pub enum MediaStream {
|
||||||
Alarm,
|
Alarm,
|
||||||
@@ -203,18 +172,6 @@ pub enum MediaStream {
|
|||||||
System,
|
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, typed_builder::TypedBuilder)]
|
#[derive(Debug, Default, Clone, typed_builder::TypedBuilder)]
|
||||||
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
|
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
|
||||||
pub struct NotifyMobileAppServiceDataData {
|
pub struct NotifyMobileAppServiceDataData {
|
||||||
@@ -232,7 +189,13 @@ impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceDataData {
|
|||||||
type Error = PyErr;
|
type Error = PyErr;
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||||
let NotifyMobileAppServiceDataData { actions, command, media_stream, tts_text, visibility } = self;
|
let NotifyMobileAppServiceDataData {
|
||||||
|
actions,
|
||||||
|
command,
|
||||||
|
media_stream,
|
||||||
|
tts_text,
|
||||||
|
visibility,
|
||||||
|
} = self;
|
||||||
|
|
||||||
let dict = PyDict::new(py);
|
let dict = PyDict::new(py);
|
||||||
|
|
||||||
@@ -279,17 +242,21 @@ impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceData {
|
|||||||
type Error = PyErr;
|
type Error = PyErr;
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||||
let NotifyMobileAppServiceData { message, title, data } = self;
|
let NotifyMobileAppServiceData {
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
data,
|
||||||
|
} = self;
|
||||||
|
|
||||||
let dict = PyDict::new(py);
|
let dict = PyDict::new(py);
|
||||||
|
|
||||||
dict.set_item("message", message)?;
|
dict.set_item("message", message)?;
|
||||||
|
|
||||||
if let Some(title) = title {
|
if let Some(title) = title {
|
||||||
dict.set_item("title", title);
|
dict.set_item("title", title)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
dict.set_item("data", data);
|
dict.set_item("data", data)?;
|
||||||
|
|
||||||
Ok(dict)
|
Ok(dict)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use mitsein::vec1::Vec1;
|
use mitsein::vec1::Vec1;
|
||||||
use pyo3::{types::PyAnyMethods, IntoPyObject, Python};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall};
|
||||||
object_id::ObjectId,
|
|
||||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Action, NonSpecialMessage, NotifyMobileAppServiceData, NotifyMobileAppServiceDataData,
|
Action, MobileAppDeviceId, NonSpecialMessage, NotifyMobileAppServiceData,
|
||||||
Visibility,
|
NotifyMobileAppServiceDataData, Visibility,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
||||||
pub struct StandardNotification {
|
pub struct StandardNotification {
|
||||||
pub object_id: ObjectId,
|
pub device_id: MobileAppDeviceId,
|
||||||
|
|
||||||
#[builder(default, setter(strip_option))]
|
#[builder(default, setter(strip_option))]
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
@@ -33,7 +29,7 @@ impl IntoServiceCall for StandardNotification {
|
|||||||
|
|
||||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||||
let StandardNotification {
|
let StandardNotification {
|
||||||
object_id,
|
device_id,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
actions,
|
actions,
|
||||||
@@ -42,7 +38,7 @@ impl IntoServiceCall for StandardNotification {
|
|||||||
|
|
||||||
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_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_id = ServiceId::from_str(&format!("mobile_app_{device_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()
|
let service_data = NotifyMobileAppServiceData::builder()
|
||||||
.title_option(title)
|
.title_option(title)
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall};
|
||||||
notify::service::mobile_app::SpecialMessage,
|
|
||||||
object_id::ObjectId,
|
|
||||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{MediaStream, NotifyMobileAppServiceData, NotifyMobileAppServiceDataData};
|
use super::{
|
||||||
|
MediaStream, MobileAppDeviceId, NotifyMobileAppServiceData, NotifyMobileAppServiceDataData,
|
||||||
|
SpecialMessage,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
|
||||||
pub struct TextToSpeech {
|
pub struct TextToSpeech {
|
||||||
pub object_id: ObjectId,
|
pub device_id: MobileAppDeviceId,
|
||||||
|
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub media_stream: Option<MediaStream>,
|
pub media_stream: Option<MediaStream>,
|
||||||
@@ -21,14 +20,14 @@ impl IntoServiceCall for TextToSpeech {
|
|||||||
|
|
||||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||||
let TextToSpeech {
|
let TextToSpeech {
|
||||||
object_id,
|
device_id,
|
||||||
message,
|
message,
|
||||||
media_stream,
|
media_stream,
|
||||||
} = self;
|
} = 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_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_id = ServiceId::from_str(&format!("mobile_app_{device_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()
|
let service_data = NotifyMobileAppServiceData::builder()
|
||||||
.message(SpecialMessage::Tts.to_string())
|
.message(SpecialMessage::Tts.to_string())
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
use std::convert::Infallible;
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
use pyo3::{prelude::*, types::PyString};
|
|
||||||
|
|
||||||
use super::slug::Slug;
|
use super::slug::Slug;
|
||||||
|
|
||||||
pub use super::slug::SlugParsingError as ObjectIdParsingError;
|
pub use super::slug::SlugParsingError as ObjectIdParsingError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
#[derive(Debug, Clone, derive_more::FromStr, derive_more::Display, FromPyFromStr, ToStrToPy)]
|
||||||
pub struct ObjectId(pub Slug);
|
pub struct ObjectId(pub Slug);
|
||||||
|
|
||||||
impl<'py> IntoPyObject<'py> for ObjectId {
|
|
||||||
type Target = PyString;
|
|
||||||
type Output = Bound<'py, Self::Target>;
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
|
||||||
let s = self.to_string();
|
|
||||||
s.into_pyobject(py)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
mod power;
|
pub mod power;
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
use std::{future::Future, str::FromStr, sync::Arc};
|
use std::{future::Future, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use emitter_and_signal::{Signal, SignalExt};
|
use emitter_and_signal::{Signal, SignalExt};
|
||||||
use pyo3::{
|
use pyo3::{FromPyObject, Py, PyAny, PyErr, Python};
|
||||||
exceptions::{PyException, PyValueError},
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
prelude::*,
|
use snafu::{ResultExt, Snafu};
|
||||||
|
use string_literal::StringLiteral;
|
||||||
|
use uom::{
|
||||||
|
si::{
|
||||||
|
energy::btu,
|
||||||
|
power::{gigawatt, kilowatt, megawatt, milliwatt, terawatt, watt},
|
||||||
|
time::hour,
|
||||||
|
Units, SI,
|
||||||
|
},
|
||||||
|
Conversion,
|
||||||
};
|
};
|
||||||
use python_utils::FromPyObjectViaParse;
|
|
||||||
use snafu::{ensure, ResultExt, Snafu};
|
|
||||||
|
|
||||||
use super::super::state_classes::measurement::Measurement;
|
use super::super::state_classes::measurement::Measurement;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -14,49 +21,15 @@ use crate::{
|
|||||||
entity_id::EntityId,
|
entity_id::EntityId,
|
||||||
home_assistant::HomeAssistant,
|
home_assistant::HomeAssistant,
|
||||||
object_id::ObjectId,
|
object_id::ObjectId,
|
||||||
|
state::HomeAssistantState,
|
||||||
state_object::{self, StateObject, StateObjectSignalError},
|
state_object::{self, StateObject, StateObjectSignalError},
|
||||||
unit_of_measurement::power::UnitOfMeasurement,
|
unit_of_measurement::power::UnitOfMeasurement,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy, StringLiteral, FromPyFromStr, ToStrToPy)]
|
||||||
|
#[string_literal(value = "power")]
|
||||||
struct Power;
|
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)]
|
#[derive(Debug, FromPyObject)]
|
||||||
#[pyo3(from_item_all)]
|
#[pyo3(from_item_all)]
|
||||||
pub struct PowerSensorAttributes {
|
pub struct PowerSensorAttributes {
|
||||||
@@ -74,25 +47,45 @@ pub enum CreateSignalError {
|
|||||||
|
|
||||||
/// couldn't map the state object to a power value
|
/// couldn't map the state object to a power value
|
||||||
MappedSignalError {
|
MappedSignalError {
|
||||||
source: emitter_and_signal::signal_ext::ProducerAlreadyExited,
|
source: emitter_and_signal::ProducerExited,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn signal<'py>(
|
pub fn signal<'py, Number>(
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
home_assistant: &'py HomeAssistant,
|
home_assistant: &'py HomeAssistant,
|
||||||
object_id: ObjectId,
|
object_id: ObjectId,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
Signal<Option<Arc<Result<uom::si::f64::Power, StateObjectSignalError<PyErr>>>>>,
|
Signal<
|
||||||
|
Option<
|
||||||
|
Result<
|
||||||
|
HomeAssistantState<uom::si::quantities::Power<Number>>,
|
||||||
|
StateObjectSignalError<Arc<PyErr>>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
|
impl Future<Output = Result<(), emitter_and_signal::signal::JoinError>>,
|
||||||
),
|
),
|
||||||
CreateSignalError,
|
CreateSignalError,
|
||||||
> {
|
>
|
||||||
let entity_id = EntityId(Domain::Light, object_id);
|
where
|
||||||
|
Number: 'static + uom::num::Num + Clone + Send + Sync + FromStr,
|
||||||
|
Number: uom::num::Num + uom::Conversion<Number, T = Number>,
|
||||||
|
milliwatt: Conversion<Number, T = Number>,
|
||||||
|
watt: Conversion<Number, T = Number>,
|
||||||
|
kilowatt: Conversion<Number, T = Number>,
|
||||||
|
megawatt: Conversion<Number, T = Number>,
|
||||||
|
gigawatt: Conversion<Number, T = Number>,
|
||||||
|
terawatt: Conversion<Number, T = Number>,
|
||||||
|
btu: Conversion<Number, T = Number>,
|
||||||
|
hour: Conversion<Number, T = Number>,
|
||||||
|
SI<Number>: Units<Number>,
|
||||||
|
{
|
||||||
|
let entity_id = EntityId(Domain::Sensor, object_id);
|
||||||
|
|
||||||
let (signal, task1) =
|
let (signal, task1) =
|
||||||
StateObject::<FromPyObjectViaParse<f64>, PowerSensorAttributes, Py<PyAny>>::signal(
|
StateObject::<HomeAssistantState<Number>, PowerSensorAttributes, Py<PyAny>>::signal(
|
||||||
py,
|
py,
|
||||||
home_assistant,
|
home_assistant,
|
||||||
entity_id,
|
entity_id,
|
||||||
@@ -102,18 +95,17 @@ pub fn signal<'py>(
|
|||||||
let (signal, task2) = signal
|
let (signal, task2) = signal
|
||||||
.map(|state_object_result_option| {
|
.map(|state_object_result_option| {
|
||||||
state_object_result_option.map(|state_object_result| {
|
state_object_result_option.map(|state_object_result| {
|
||||||
Arc::new(
|
|
||||||
Result::as_ref(&state_object_result)
|
Result::as_ref(&state_object_result)
|
||||||
.map(|state_object| {
|
.map(
|
||||||
let amount = state_object.state.0;
|
|StateObject {
|
||||||
let unit_of_measurement = state_object.attributes.unit_of_measurement;
|
state, attributes, ..
|
||||||
|
}| {
|
||||||
let power = unit_of_measurement.into_uom(amount);
|
state.as_ref().map(|amount| {
|
||||||
|
attributes.unit_of_measurement.into_uom(amount.clone())
|
||||||
power
|
|
||||||
})
|
})
|
||||||
.map_err(|e| todo!()),
|
},
|
||||||
)
|
)
|
||||||
|
.map_err(Clone::clone)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.context(MappedSignalSnafu)?;
|
.context(MappedSignalSnafu)?;
|
||||||
|
|||||||
@@ -1,48 +1,6 @@
|
|||||||
use pyo3::{
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
exceptions::{PyException, PyValueError},
|
use string_literal::StringLiteral;
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use snafu::{ensure, ResultExt, Snafu};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy, StringLiteral, FromPyFromStr, ToStrToPy)]
|
||||||
|
#[string_literal(value = "measurement")]
|
||||||
pub struct Measurement;
|
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,21 +1,8 @@
|
|||||||
use std::convert::Infallible;
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
use pyo3::{prelude::*, types::PyString};
|
|
||||||
|
|
||||||
use super::super::slug::Slug;
|
use super::super::slug::Slug;
|
||||||
|
|
||||||
pub use super::super::slug::SlugParsingError as ServiceDomainParsingError;
|
pub use super::super::slug::SlugParsingError as ServiceDomainParsingError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
#[derive(Debug, Clone, derive_more::FromStr, derive_more::Display, FromPyFromStr, ToStrToPy)]
|
||||||
pub struct ServiceDomain(pub Slug);
|
pub struct ServiceDomain(pub Slug);
|
||||||
|
|
||||||
impl<'py> IntoPyObject<'py> for ServiceDomain {
|
|
||||||
type Target = PyString;
|
|
||||||
type Output = Bound<'py, Self::Target>;
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
|
||||||
let s = self.to_string();
|
|
||||||
s.into_pyobject(py)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
use std::convert::Infallible;
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
use pyo3::{prelude::*, types::PyString};
|
|
||||||
|
|
||||||
use super::super::slug::Slug;
|
use super::super::slug::Slug;
|
||||||
|
|
||||||
pub use super::super::slug::SlugParsingError as ServiceIdParsingError;
|
pub use super::super::slug::SlugParsingError as ServiceIdParsingError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
#[derive(Debug, Clone, derive_more::FromStr, derive_more::Display, FromPyFromStr, ToStrToPy)]
|
||||||
pub struct ServiceId(pub Slug);
|
pub struct ServiceId(pub Slug);
|
||||||
|
|
||||||
impl<'py> IntoPyObject<'py> for ServiceId {
|
|
||||||
type Target = PyString;
|
|
||||||
type Output = Bound<'py, Self::Target>;
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
|
||||||
let s = self.to_string();
|
|
||||||
s.into_pyobject(py)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use super::{event::context::context::Context, service::IntoServiceCall};
|
use super::{event::context::Context, service::IntoServiceCall};
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
|
conversion::FromPyObjectOwned,
|
||||||
exceptions::{PyException, PyTypeError},
|
exceptions::{PyException, PyTypeError},
|
||||||
prelude::*,
|
types::PyAnyMethods as _,
|
||||||
|
Borrowed, FromPyObject, IntoPyObject, Py, PyAny, PyErr, Python,
|
||||||
};
|
};
|
||||||
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
@@ -73,7 +75,7 @@ impl ServiceRegistry {
|
|||||||
return_response,
|
return_response,
|
||||||
);
|
);
|
||||||
|
|
||||||
let future = Python::attach::<_, PyResult<_>>(|py| {
|
let future = Python::attach::<_, Result<_, PyErr>>(|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)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use std::{str::FromStr, sync::Arc};
|
use std::{str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use pyo3::{exceptions::PyValueError, PyErr};
|
use pyo3::{exceptions::PyValueError, PyErr};
|
||||||
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
// TODO: derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
#[derive(Debug, Clone, derive_more::Display, FromPyFromStr, ToStrToPy)]
|
||||||
#[derive(Debug, Clone, derive_more::Display)]
|
|
||||||
pub struct Slug(Arc<str>);
|
pub struct Slug(Arc<str>);
|
||||||
|
|
||||||
|
/// 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}
|
||||||
#[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}"))]
|
|
||||||
pub struct SlugParsingError {
|
pub struct SlugParsingError {
|
||||||
encountered: char,
|
encountered: char,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,12 @@
|
|||||||
use std::str::FromStr;
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
use pyo3::{
|
|
||||||
exceptions::{PyException, PyValueError},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use snafu::{ResultExt, Snafu};
|
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
/// A state in Home Assistant that is known to represent an error of some kind:
|
/// 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)
|
/// * `unavailable` (the device is likely offline or unreachable from the Home Assistant instance)
|
||||||
/// * `unknown` (I don't know how to explain this one)
|
/// * `unknown` (I don't know how to explain this one)
|
||||||
#[derive(Debug, Clone, EnumString, strum::Display)]
|
#[derive(Debug, Clone, Copy, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum ErrorState {
|
pub enum ErrorState {
|
||||||
Unavailable,
|
Unavailable,
|
||||||
Unknown,
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::{convert::Infallible, str::FromStr};
|
use std::{convert::Infallible, str::FromStr};
|
||||||
|
|
||||||
use pyo3::{exceptions::PyException, prelude::*};
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
use snafu::{ResultExt, Snafu};
|
|
||||||
|
|
||||||
pub mod error_state;
|
pub mod error_state;
|
||||||
pub mod unexpected_state;
|
pub mod unexpected_state;
|
||||||
@@ -9,11 +8,28 @@ pub mod unexpected_state;
|
|||||||
pub use error_state::ErrorState;
|
pub use error_state::ErrorState;
|
||||||
pub use unexpected_state::UnexpectedState;
|
pub use unexpected_state::UnexpectedState;
|
||||||
|
|
||||||
#[derive(Debug, Clone, derive_more::Display)]
|
#[derive(Debug, Clone, derive_more::Display, FromPyFromStr, ToStrToPy)]
|
||||||
pub enum HomeAssistantState<State> {
|
pub enum HomeAssistantState<State> {
|
||||||
Ok(State),
|
Ok(State),
|
||||||
Err(ErrorState),
|
Err(ErrorState),
|
||||||
UnexpectedErr(UnexpectedState),
|
Unexpected(UnexpectedState),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State> From<String> for HomeAssistantState<State>
|
||||||
|
where
|
||||||
|
State: FromStr,
|
||||||
|
{
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
if let Ok(ok) = State::from_str(&s) {
|
||||||
|
return HomeAssistantState::Ok(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(error) = ErrorState::from_str(&s) {
|
||||||
|
return HomeAssistantState::Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeAssistantState::Unexpected(UnexpectedState(s.into()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State: FromStr> FromStr for HomeAssistantState<State> {
|
impl<State: FromStr> FromStr for HomeAssistantState<State> {
|
||||||
@@ -28,37 +44,45 @@ impl<State: FromStr> FromStr for HomeAssistantState<State> {
|
|||||||
return Ok(HomeAssistantState::Err(error));
|
return Ok(HomeAssistantState::Err(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HomeAssistantState::UnexpectedErr(UnexpectedState(s.into())))
|
Ok(HomeAssistantState::Unexpected(UnexpectedState(s.into())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
impl<State> HomeAssistantState<State> {
|
||||||
pub enum ExtractHomeAssistantStateError {
|
pub fn as_ref(&self) -> HomeAssistantState<&State> {
|
||||||
/// couldn't extract the object as a string
|
match self {
|
||||||
ExtractStringError { source: PyErr },
|
HomeAssistantState::Ok(state) => HomeAssistantState::Ok(state),
|
||||||
}
|
HomeAssistantState::Err(error_state) => HomeAssistantState::Err(*error_state),
|
||||||
|
HomeAssistantState::Unexpected(unexpected_state) => {
|
||||||
|
// TODO: just considered cheap enough to clone implicitly
|
||||||
|
HomeAssistantState::Unexpected(unexpected_state.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<ExtractHomeAssistantStateError> for PyErr {
|
pub fn map<Mapped>(self, f: impl FnOnce(State) -> Mapped) -> HomeAssistantState<Mapped> {
|
||||||
fn from(error: ExtractHomeAssistantStateError) -> Self {
|
match self {
|
||||||
match &error {
|
HomeAssistantState::Ok(state) => HomeAssistantState::Ok(f(state)),
|
||||||
ExtractHomeAssistantStateError::ExtractStringError { .. } => {
|
HomeAssistantState::Err(error_state) => HomeAssistantState::Err(error_state),
|
||||||
PyException::new_err(error.to_string())
|
HomeAssistantState::Unexpected(unexpected_state) => {
|
||||||
|
HomeAssistantState::Unexpected(unexpected_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one
|
impl<State> HomeAssistantState<&State> {
|
||||||
impl<'a, 'py, State: FromStr + FromPyObject<'a, 'py>> FromPyObject<'a, 'py>
|
pub fn cloned(self) -> HomeAssistantState<State>
|
||||||
for HomeAssistantState<State>
|
where
|
||||||
{
|
State: Clone,
|
||||||
type Error = ExtractHomeAssistantStateError;
|
{
|
||||||
|
self.map(Clone::clone)
|
||||||
|
}
|
||||||
|
|
||||||
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
pub fn copied(self) -> HomeAssistantState<State>
|
||||||
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
|
where
|
||||||
|
State: Copy,
|
||||||
let Ok(state) = s.parse();
|
{
|
||||||
|
self.map(|&s| s)
|
||||||
Ok(state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,14 @@
|
|||||||
use std::sync::Arc;
|
use std::{convert::Infallible, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use pyo3::{exceptions::PyException, prelude::*};
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
use snafu::{ResultExt, Snafu};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, derive_more::Display)]
|
#[derive(Debug, Clone, derive_more::Display, FromPyFromStr, ToStrToPy)]
|
||||||
pub struct UnexpectedState(pub Arc<str>);
|
pub struct UnexpectedState(pub Arc<str>);
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
impl FromStr for UnexpectedState {
|
||||||
pub enum ExtractUnexpectedStateError {
|
type Err = Infallible;
|
||||||
/// couldn't extract the object as a string
|
|
||||||
ExtractStringError { source: PyErr },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ExtractUnexpectedStateError> for PyErr {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
fn from(error: ExtractUnexpectedStateError) -> Self {
|
Ok(Self(s.into()))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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::{conversion::FromPyObjectOwned, Borrowed, FromPyObject, Py, PyAny, PyErr, Python};
|
||||||
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
use python_utils::{detach, validate_type_by_name, TypeByNameValidationError};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@ impl StateMachine {
|
|||||||
.call_method1(py, "get", args)
|
.call_method1(py, "get", args)
|
||||||
.map_err(Arc::new)
|
.map_err(Arc::new)
|
||||||
.context(GetStateObjectSnafu)?;
|
.context(GetStateObjectSnafu)?;
|
||||||
Ok(state.extract(py).context(ExtractStateObjectSnafu)?)
|
|
||||||
|
state.extract(py).context(ExtractStateObjectSnafu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::{
|
use super::{
|
||||||
event::{context::context::Context, specific::state_changed},
|
event::{context::Context, specific::state_changed},
|
||||||
home_assistant::HomeAssistant,
|
home_assistant::HomeAssistant,
|
||||||
};
|
};
|
||||||
use crate::{entity_id::EntityId, home_assistant::GetStatesError, state_machine::GetStateError};
|
use crate::{entity_id::EntityId, home_assistant::GetStatesError, state_machine::GetStateError};
|
||||||
@@ -7,8 +7,8 @@ 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;
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
prelude::*,
|
types::{PyAnyMethods as _, PyCFunction, PyDict, PyModule, PyTuple},
|
||||||
types::{PyCFunction, PyDict, PyTuple},
|
Bound, FromPyObject, IntoPyObject as _, Py, PyAny, PyErr, Python,
|
||||||
};
|
};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
use std::{future::Future, sync::Arc};
|
use std::{future::Future, sync::Arc};
|
||||||
@@ -28,7 +28,7 @@ pub struct StateObject<State, Attributes, ContextEvent> {
|
|||||||
pub type ExtractStateObjectError<'a, 'py, State, Attributes, ContextEvent> =
|
pub type ExtractStateObjectError<'a, 'py, State, Attributes, ContextEvent> =
|
||||||
<StateObject<State, Attributes, ContextEvent> as FromPyObject<'a, 'py>>::Error;
|
<StateObject<State, Attributes, ContextEvent> as FromPyObject<'a, 'py>>::Error;
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Clone, Snafu)]
|
||||||
pub enum CreateSignalError {
|
pub enum CreateSignalError {
|
||||||
/// couldn't get the state machine from the Home Assistant object
|
/// couldn't get the state machine from the Home Assistant object
|
||||||
GetStatesError { source: GetStatesError },
|
GetStatesError { source: GetStatesError },
|
||||||
@@ -62,7 +62,7 @@ impl<
|
|||||||
Arc<
|
Arc<
|
||||||
Result<
|
Result<
|
||||||
Self,
|
Self,
|
||||||
StateObjectSignalError<<Self as FromPyObject<'a, 'py>>::Error>,
|
StateObjectSignalError<Arc<<Self as FromPyObject<'a, 'py>>::Error>>,
|
||||||
>,
|
>,
|
||||||
>,
|
>,
|
||||||
>,
|
>,
|
||||||
@@ -74,6 +74,16 @@ impl<
|
|||||||
let state_machine = home_assistant.states(py).context(GetStatesSnafu)?;
|
let state_machine = home_assistant.states(py).context(GetStatesSnafu)?;
|
||||||
let current = state_machine
|
let current = state_machine
|
||||||
.get(py, entity_id.clone())
|
.get(py, entity_id.clone())
|
||||||
|
.map_err(|e| match e {
|
||||||
|
GetStateError::GetStateObjectError { source } => {
|
||||||
|
GetStateError::GetStateObjectError { source }
|
||||||
|
}
|
||||||
|
GetStateError::ExtractStateObjectError { source } => {
|
||||||
|
GetStateError::ExtractStateObjectError {
|
||||||
|
source: Arc::new(source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.context(GetFromStateMachineSnafu)
|
.context(GetFromStateMachineSnafu)
|
||||||
.transpose();
|
.transpose();
|
||||||
|
|
||||||
@@ -86,7 +96,7 @@ impl<
|
|||||||
while let Some(publisher) = publisher_stream.wait().await {
|
while let Some(publisher) = publisher_stream.wait().await {
|
||||||
let (new_state_sender, mut new_state_receiver) = mpsc::channel(8);
|
let (new_state_sender, mut new_state_receiver) = mpsc::channel(8);
|
||||||
|
|
||||||
let untrack = Python::attach::<_, PyResult<_>>(|py| {
|
let untrack = Python::attach::<_, Result<_, PyErr>>(|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
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
use std::str::FromStr;
|
use python_utils::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
use pyo3::{
|
|
||||||
exceptions::{PyException, PyValueError},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use snafu::{ResultExt, Snafu};
|
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
use uom::{
|
use uom::{
|
||||||
si::{
|
si::{
|
||||||
@@ -18,7 +12,7 @@ use uom::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Power units
|
/// Power units
|
||||||
#[derive(Debug, Clone, Copy, EnumString, strum::Display)]
|
#[derive(Debug, Clone, Copy, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum UnitOfMeasurement {
|
pub enum UnitOfMeasurement {
|
||||||
#[strum(serialize = "mW")]
|
#[strum(serialize = "mW")]
|
||||||
@@ -37,46 +31,10 @@ pub enum UnitOfMeasurement {
|
|||||||
BtuPerhour,
|
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 {
|
impl UnitOfMeasurement {
|
||||||
pub fn into_uom<V>(&self, amount: V) -> Power<V>
|
pub fn into_uom<V>(&self, amount: V) -> Power<V>
|
||||||
where
|
where
|
||||||
V: uom::num::Num + uom::Conversion<V, T = V>,
|
V: uom::num::Num + Conversion<V, T = V>,
|
||||||
milliwatt: Conversion<V, T = V>,
|
milliwatt: Conversion<V, T = V>,
|
||||||
watt: Conversion<V, T = V>,
|
watt: Conversion<V, T = V>,
|
||||||
kilowatt: Conversion<V, T = V>,
|
kilowatt: Conversion<V, T = V>,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = { workspace = true }
|
bytes = { workspace = true }
|
||||||
emitter-and-signal = { path = "../emitter-and-signal" }
|
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||||
fjall = "2.11"
|
fjall = "3"
|
||||||
postcard = { version = "1.1", features = ["use-std"] }
|
postcard = { version = "1.1", features = ["use-std"] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
snafu = { workspace = true }
|
snafu = { workspace = true }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pub use bytes::Bytes;
|
pub use bytes::Bytes;
|
||||||
use emitter_and_signal::signal::{JoinError, Signal};
|
use emitter_and_signal::signal::{JoinError, Signal};
|
||||||
pub use fjall::{Config, Keyspace, Partition, PartitionCreateOptions};
|
pub use fjall::{Config, Database, Keyspace, KeyspaceCreateOptions};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use snafu::{OptionExt, ResultExt, Snafu};
|
use snafu::{OptionExt, ResultExt, Snafu};
|
||||||
use std::{fmt::Debug, future::Future, num::NonZeroUsize, ops::Deref, sync::Arc};
|
use std::{fmt::Debug, future::Future, num::NonZeroUsize, ops::Deref, sync::Arc};
|
||||||
@@ -23,7 +23,7 @@ pub enum PersistedError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn persisted<T: Debug + Send + Sync + 'static + Serialize + for<'a> Deserialize<'a>>(
|
pub async fn persisted<T: Debug + Send + Sync + 'static + Serialize + for<'a> Deserialize<'a>>(
|
||||||
partition: Partition,
|
keyspace: Keyspace,
|
||||||
identifier: Bytes,
|
identifier: Bytes,
|
||||||
buffer: NonZeroUsize,
|
buffer: NonZeroUsize,
|
||||||
) -> (
|
) -> (
|
||||||
@@ -32,9 +32,9 @@ pub async fn persisted<T: Debug + Send + Sync + 'static + Serialize + for<'a> De
|
|||||||
impl Future<Output = Result<(), JoinError>>,
|
impl Future<Output = Result<(), JoinError>>,
|
||||||
) {
|
) {
|
||||||
let initial = spawn_blocking({
|
let initial = spawn_blocking({
|
||||||
let partition = partition.clone();
|
let keyspace = keyspace.clone();
|
||||||
let identifier = identifier.clone();
|
let identifier = identifier.clone();
|
||||||
move || partition.get(identifier.deref())
|
move || keyspace.get(identifier.deref())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -60,9 +60,9 @@ pub async fn persisted<T: Debug + Send + Sync + 'static + Serialize + for<'a> De
|
|||||||
// Stand-in for Option::async_and_then
|
// Stand-in for Option::async_and_then
|
||||||
let persisted_res = match serialized_res {
|
let persisted_res = match serialized_res {
|
||||||
Ok(serialized) => spawn_blocking({
|
Ok(serialized) => spawn_blocking({
|
||||||
let partition = partition.clone();
|
let keyspace = keyspace.clone();
|
||||||
let identifier = identifier.clone();
|
let identifier = identifier.clone();
|
||||||
move || partition.insert(identifier.deref(), serialized)
|
move || keyspace.insert(identifier.deref(), serialized)
|
||||||
}).await.unwrap().context(SavingSnafu),
|
}).await.unwrap().context(SavingSnafu),
|
||||||
Err(error) => Err(error),
|
Err(error) => Err(error),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ serde = ["dep:serde"]
|
|||||||
deranged = { workspace = true }
|
deranged = { workspace = true }
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
ext-trait = { workspace = true }
|
ext-trait = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
palette = { workspace = true }
|
palette = { workspace = true }
|
||||||
snafu = { workspace = true }
|
snafu = { workspace = true }
|
||||||
strum = { workspace = true, features = ["derive"] }
|
strum = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::{error::Error, future::Future};
|
use std::{error::Error, future::Future};
|
||||||
|
|
||||||
use deranged::RangedU16;
|
use deranged::RangedU16;
|
||||||
use snafu::{ResultExt, Snafu};
|
use futures::TryFutureExt as _;
|
||||||
|
use snafu::{ResultExt as _, Snafu};
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, strum::Display, strum::EnumIs,
|
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, strum::Display, strum::EnumIs,
|
||||||
@@ -13,7 +14,7 @@ pub enum State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub const fn invert(self) -> Self {
|
pub const fn inverted(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
State::Off => State::On,
|
State::Off => State::On,
|
||||||
State::On => State::Off,
|
State::On => State::Off,
|
||||||
@@ -44,15 +45,15 @@ pub trait GetState {
|
|||||||
|
|
||||||
#[ext_trait::extension(pub trait IsOff)]
|
#[ext_trait::extension(pub trait IsOff)]
|
||||||
impl<T: GetState> T {
|
impl<T: GetState> T {
|
||||||
async fn is_off(&self) -> Result<bool, T::Error> {
|
fn is_off(&self) -> impl Future<Output = Result<bool, T::Error>> + Send {
|
||||||
Ok(self.get_state().await?.is_off())
|
self.get_state().map_ok(|state| state.is_off())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ext_trait::extension(pub trait IsOn)]
|
#[ext_trait::extension(pub trait IsOn)]
|
||||||
impl<T: GetState> T {
|
impl<T: GetState> T {
|
||||||
async fn is_on(&self) -> Result<bool, T::Error> {
|
fn is_on(&self) -> impl Future<Output = Result<bool, T::Error>> + Send {
|
||||||
Ok(self.get_state().await?.is_on())
|
self.get_state().map_ok(|state| state.is_on())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +64,15 @@ pub trait SetState {
|
|||||||
|
|
||||||
#[ext_trait::extension(pub trait TurnOff)]
|
#[ext_trait::extension(pub trait TurnOff)]
|
||||||
impl<T: SetState> T {
|
impl<T: SetState> T {
|
||||||
async fn turn_off(&mut self) -> Result<(), T::Error> {
|
fn turn_off(&mut self) -> impl Future<Output = Result<(), T::Error>> + Send {
|
||||||
self.set_state(State::Off).await
|
self.set_state(State::Off)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ext_trait::extension(pub trait TurnOn)]
|
#[ext_trait::extension(pub trait TurnOn)]
|
||||||
impl<T: SetState> T {
|
impl<T: SetState> T {
|
||||||
async fn turn_on(&mut self) -> Result<(), T::Error> {
|
fn turn_on(&mut self) -> impl Future<Output = Result<(), T::Error>> + Send {
|
||||||
self.set_state(State::On).await
|
self.set_state(State::On)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ where
|
|||||||
/// Toggle the light by setting it to the inverse of its current state
|
/// Toggle the light by setting it to the inverse of its current state
|
||||||
async fn toggle(&mut self) -> Result<(), Self::Error> {
|
async fn toggle(&mut self) -> Result<(), Self::Error> {
|
||||||
let state = self.get_state().await.context(GetStateSnafu)?;
|
let state = self.get_state().await.context(GetStateSnafu)?;
|
||||||
self.set_state(state.invert())
|
self.set_state(state.inverted())
|
||||||
.await
|
.await
|
||||||
.context(SetStateSnafu)?;
|
.context(SetStateSnafu)?;
|
||||||
|
|
||||||
|
|||||||
9
python-utils-macros-impl/Cargo.toml
Normal file
9
python-utils-macros-impl/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "python-utils-macros-impl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = { workspace = true }
|
||||||
|
quote = { workspace = true }
|
||||||
|
syn = { workspace = true }
|
||||||
54
python-utils-macros-impl/src/from_py_from_str.rs
Normal file
54
python-utils-macros-impl/src/from_py_from_str.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{ToTokens, quote};
|
||||||
|
use syn::{
|
||||||
|
DeriveInput, GenericParam,
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
parse_quote,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct FromPyFromStr {
|
||||||
|
derive_input: DeriveInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for FromPyFromStr {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let derive_input = input.parse()?;
|
||||||
|
|
||||||
|
let this = Self { derive_input };
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for FromPyFromStr {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
|
let name = &self.derive_input.ident;
|
||||||
|
|
||||||
|
let (_, ty_generics, where_clause) = self.derive_input.generics.split_for_impl();
|
||||||
|
let mut generics = self.derive_input.generics.clone();
|
||||||
|
|
||||||
|
let a_lifetime: GenericParam = parse_quote!('a);
|
||||||
|
let py_lifetime: GenericParam = parse_quote!('py);
|
||||||
|
|
||||||
|
generics.params.push(a_lifetime.clone());
|
||||||
|
generics.params.push(py_lifetime.clone());
|
||||||
|
|
||||||
|
let (impl_generics, _, _) = generics.split_for_impl();
|
||||||
|
|
||||||
|
let output = quote! {
|
||||||
|
impl #impl_generics ::pyo3::conversion::FromPyObject<#a_lifetime, #py_lifetime> for #name #ty_generics #where_clause
|
||||||
|
where
|
||||||
|
#name #ty_generics: ::std::str::FromStr,
|
||||||
|
<#name #ty_generics as ::std::str::FromStr>::Err: ::std::fmt::Display,
|
||||||
|
{
|
||||||
|
type Error = ::python_utils::ExtractPyObjectViaParseError<<#name #ty_generics as ::std::str::FromStr>::Err>;
|
||||||
|
|
||||||
|
fn extract(obj: ::pyo3::Borrowed<#a_lifetime, #py_lifetime, ::pyo3::types::PyAny>) -> ::core::result::Result<Self, Self::Error> {
|
||||||
|
::python_utils::FromPyObjectViaParse::extract(obj).map(|wrapper| wrapper.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tokens.extend(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
python-utils-macros-impl/src/lib.rs
Normal file
5
python-utils-macros-impl/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod from_py_from_str;
|
||||||
|
mod to_str_to_py;
|
||||||
|
|
||||||
|
pub use from_py_from_str::FromPyFromStr;
|
||||||
|
pub use to_str_to_py::ToStrToPy;
|
||||||
54
python-utils-macros-impl/src/to_str_to_py.rs
Normal file
54
python-utils-macros-impl/src/to_str_to_py.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{ToTokens, quote};
|
||||||
|
use syn::{
|
||||||
|
DeriveInput, GenericParam,
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
parse_quote,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ToStrToPy {
|
||||||
|
derive_input: DeriveInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for ToStrToPy {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let derive_input = input.parse()?;
|
||||||
|
|
||||||
|
let this = Self { derive_input };
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for ToStrToPy {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
|
let name = &self.derive_input.ident;
|
||||||
|
|
||||||
|
let (_, ty_generics, where_clause) = self.derive_input.generics.split_for_impl();
|
||||||
|
let mut generics = self.derive_input.generics.clone();
|
||||||
|
|
||||||
|
let py_lifetime: GenericParam = parse_quote!('py);
|
||||||
|
|
||||||
|
generics.params.push(py_lifetime.clone());
|
||||||
|
|
||||||
|
let (impl_generics, _, _) = generics.split_for_impl();
|
||||||
|
|
||||||
|
let output = quote! {
|
||||||
|
impl #impl_generics ::pyo3::conversion::IntoPyObject<#py_lifetime> for #name #ty_generics #where_clause
|
||||||
|
where
|
||||||
|
#name #ty_generics: ::std::fmt::Display
|
||||||
|
{
|
||||||
|
type Target = ::pyo3::types::PyString;
|
||||||
|
type Output = ::pyo3::Bound<#py_lifetime, Self::Target>;
|
||||||
|
type Error = ::std::convert::Infallible;
|
||||||
|
|
||||||
|
fn into_pyobject(self, py: ::pyo3::Python<#py_lifetime>) -> ::core::result::Result<Self::Output, Self::Error> {
|
||||||
|
let s = ::std::string::ToString::to_string(&self);
|
||||||
|
::pyo3::conversion::IntoPyObject::into_pyobject(s, py)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tokens.extend(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
python-utils-macros/Cargo.toml
Normal file
13
python-utils-macros/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "python-utils-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
python-utils-macros-impl = { path = "../python-utils-macros-impl" }
|
||||||
|
quote = { workspace = true }
|
||||||
|
syn = { workspace = true }
|
||||||
15
python-utils-macros/src/lib.rs
Normal file
15
python-utils-macros/src/lib.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::parse_macro_input;
|
||||||
|
|
||||||
|
#[proc_macro_derive(FromPyFromStr)]
|
||||||
|
pub fn from_py_from_str(input: TokenStream) -> TokenStream {
|
||||||
|
let item: python_utils_macros_impl::FromPyFromStr = parse_macro_input!(input);
|
||||||
|
quote! { #item }.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(ToStrToPy)]
|
||||||
|
pub fn to_str_to_py(input: TokenStream) -> TokenStream {
|
||||||
|
let item: python_utils_macros_impl::ToStrToPy = parse_macro_input!(input);
|
||||||
|
quote! { #item }.into()
|
||||||
|
}
|
||||||
@@ -4,7 +4,11 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
macros = ["dep:python-utils-macros"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
pyo3 = { workspace = true }
|
pyo3 = { workspace = true }
|
||||||
|
python-utils-macros = { optional = true, path = "../python-utils-macros" }
|
||||||
snafu = { workspace = true }
|
snafu = { workspace = true }
|
||||||
|
|||||||
73
python-utils/src/from_pyobject_via_parse.rs
Normal file
73
python-utils/src/from_pyobject_via_parse.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
str::FromStr,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use pyo3::{
|
||||||
|
exceptions::{PyException, PyValueError},
|
||||||
|
Borrowed, FromPyObject, PyAny, PyErr,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::FromStr)]
|
||||||
|
pub struct FromPyObjectViaParse<T>(pub T);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ExtractPyObjectViaParseError<ParseError> {
|
||||||
|
/// 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<ParseError: Display> Display for ExtractPyObjectViaParseError<ParseError> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ExtractPyObjectViaParseError::ExtractStringError { source } => write!(f, "{}", source),
|
||||||
|
ExtractPyObjectViaParseError::ParseError { source } => write!(f, "{}", source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ParseError> std::error::Error for ExtractPyObjectViaParseError<ParseError> where
|
||||||
|
ExtractPyObjectViaParseError<ParseError>: Debug + Display
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ParseError> From<ExtractPyObjectViaParseError<ParseError>> for PyErr
|
||||||
|
where
|
||||||
|
ExtractPyObjectViaParseError<ParseError>: Display,
|
||||||
|
{
|
||||||
|
fn from(error: ExtractPyObjectViaParseError<ParseError>) -> 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,
|
||||||
|
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)
|
||||||
|
.map_err(|e| ExtractPyObjectViaParseError::ExtractStringError { source: e })?;
|
||||||
|
|
||||||
|
let t = s
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| ExtractPyObjectViaParseError::ParseError { source: e })?;
|
||||||
|
|
||||||
|
Ok(FromPyObjectViaParse(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
20
python-utils/src/into_pyobject_via_display.rs
Normal file
20
python-utils/src/into_pyobject_via_display.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use std::{convert::Infallible, fmt::Display};
|
||||||
|
|
||||||
|
use pyo3::{types::PyString, Bound, IntoPyObject, Python};
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
use std::{convert::Infallible, fmt::Display, str::FromStr, sync::Arc};
|
|
||||||
|
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
exceptions::{PyException, PyTypeError, PyValueError},
|
exceptions::{PyException, PyTypeError},
|
||||||
prelude::*,
|
types::{PyAnyMethods as _, PyStringMethods, PyTypeMethods as _},
|
||||||
types::PyString,
|
Borrowed, Bound, Py, PyAny, PyErr,
|
||||||
};
|
};
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
pub use python_utils_macros::{FromPyFromStr, ToStrToPy};
|
||||||
|
|
||||||
|
pub mod from_pyobject_via_parse;
|
||||||
|
pub mod into_pyobject_via_display;
|
||||||
pub mod none;
|
pub mod none;
|
||||||
|
|
||||||
|
pub use from_pyobject_via_parse::{ExtractPyObjectViaParseError, FromPyObjectViaParse};
|
||||||
|
pub use into_pyobject_via_display::IntoPyObjectViaDisplay;
|
||||||
pub use none::IsNone;
|
pub use none::IsNone;
|
||||||
|
|
||||||
/// Create a GIL-independent reference
|
/// Create a GIL-independent reference
|
||||||
@@ -79,72 +85,5 @@ pub fn validate_type_by_name(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(());
|
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
string-literal-macros-impl/Cargo.toml
Normal file
11
string-literal-macros-impl/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "string-literal-macros-impl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
darling = "0.23.0"
|
||||||
|
proc-macro2 = { workspace = true }
|
||||||
|
quote = { workspace = true }
|
||||||
|
syn = { workspace = true }
|
||||||
70
string-literal-macros-impl/src/lib.rs
Normal file
70
string-literal-macros-impl/src/lib.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use darling::FromDeriveInput;
|
||||||
|
use proc_macro2::{Span, TokenStream};
|
||||||
|
use quote::{ToTokens, quote};
|
||||||
|
use syn::{Ident, LitStr, Path, parse_quote, parse2};
|
||||||
|
|
||||||
|
#[derive(FromDeriveInput)]
|
||||||
|
#[darling(supports(struct_unit))]
|
||||||
|
#[darling(attributes(string_literal))]
|
||||||
|
struct StringLiteral {
|
||||||
|
ident: Ident,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for StringLiteral {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
|
let Self { ident, value } = self;
|
||||||
|
|
||||||
|
let display_trait: Path = parse_quote!(::std::fmt::Display);
|
||||||
|
let from_trait: Path = parse_quote!(::core::convert::From);
|
||||||
|
let from_str_trait: Path = parse_quote!(::std::str::FromStr);
|
||||||
|
let result: Path = parse_quote!(::core::result::Result);
|
||||||
|
let wrong_literal_error_generic: Path = parse_quote!(::string_literal::WrongLiteralError);
|
||||||
|
let wrong_literal_error: Path = parse_quote!(#wrong_literal_error_generic<#ident>);
|
||||||
|
let write_macro: Path = parse_quote!(::std::write);
|
||||||
|
|
||||||
|
let value_literal = LitStr::new(value.as_str(), Span::call_site());
|
||||||
|
|
||||||
|
let output = quote! {
|
||||||
|
impl #from_trait<&#ident> for &'static str {
|
||||||
|
fn from(unit: &#ident) -> Self {
|
||||||
|
#value_literal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #from_str_trait for #ident {
|
||||||
|
type Err = #wrong_literal_error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> #result<Self, Self::Err> {
|
||||||
|
if s == #value_literal {
|
||||||
|
Ok(Self)
|
||||||
|
} else {
|
||||||
|
Err(#wrong_literal_error_generic { actual: s.to_owned(), expected: #ident })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #display_trait for #ident {
|
||||||
|
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||||
|
#write_macro!(f, "state_changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tokens.extend(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_literal_impl(input: TokenStream) -> TokenStream {
|
||||||
|
let derive_input = match parse2(input) {
|
||||||
|
Ok(derive_input) => derive_input,
|
||||||
|
Err(error) => return error.into_compile_error(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let string_literal = match StringLiteral::from_derive_input(&derive_input) {
|
||||||
|
Ok(string_literal) => string_literal,
|
||||||
|
Err(error) => return error.write_errors(),
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! { #string_literal }
|
||||||
|
}
|
||||||
11
string-literal-macros/Cargo.toml
Normal file
11
string-literal-macros/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "string-literal-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
string-literal-macros-impl = { path = "../string-literal-macros-impl" }
|
||||||
6
string-literal-macros/src/lib.rs
Normal file
6
string-literal-macros/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
|
||||||
|
#[proc_macro_derive(StringLiteral, attributes(string_literal))]
|
||||||
|
pub fn string_literal(input: TokenStream) -> TokenStream {
|
||||||
|
string_literal_macros_impl::string_literal_impl(input.into()).into()
|
||||||
|
}
|
||||||
11
string-literal/Cargo.toml
Normal file
11
string-literal/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "string-literal"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
macros = ["dep:string-literal-macros"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
string-literal-macros = { optional = true, path = "../string-literal-macros" }
|
||||||
24
string-literal/src/lib.rs
Normal file
24
string-literal/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
pub use string_literal_macros::StringLiteral;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WrongLiteralError<S> {
|
||||||
|
pub actual: String,
|
||||||
|
pub expected: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Display for WrongLiteralError<S>
|
||||||
|
where
|
||||||
|
&'static str: for<'a> From<&'a S>,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Self { actual, expected } = self;
|
||||||
|
let expected_str = <&'static str>::from(expected);
|
||||||
|
|
||||||
|
write!(f, "expected {expected_str:?} but got {actual:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> std::error::Error for WrongLiteralError<S> where WrongLiteralError<S>: Debug + Display {}
|
||||||
Reference in New Issue
Block a user