Compare commits

..

36 Commits

Author SHA1 Message Date
J / Jacob Babich
4786b1e6ba chore: clean up emitter-and-signal library design 2026-07-15 14:33:51 -04:00
J / Jacob Babich
a09a97d06d chore: replace all #[snafu(display(...))] with regular doc comments /// 2026-07-15 13:48:41 -04:00
4eb8a752cc feat(entrypoint): trial the power sensor signal for the smart plugs connected to the washer and dryer currently 2026-07-15 02:41:13 -04:00
J / Jacob Babich
8d3cde3c43 chore(driver-kasa): rename impl_protocol to protocol to match with home-assistant/light 2026-07-15 01:06:34 -04:00
J / Jacob Babich
cb216d0a0a chore: rename Numeric generic parameter to Number so that it's a noun and not mistakeable for a trait 2026-07-15 01:00:50 -04:00
J / Jacob Babich
8b966c1210 feat: make power sensor signal support any numeric type 2026-07-15 00:57:59 -04:00
J / Jacob Babich
a2d6d9f4c2 feat: make the input_number attributes and signal support any numeric type 2026-07-15 00:52:01 -04:00
J / Jacob Babich
8ab8dd3441 chore: address some clippy concerns 2026-07-15 00:32:04 -04:00
J / Jacob Babich
224ee7732f chore: upgrade to fjall 3 2026-07-15 00:13:47 -04:00
J / Jacob Babich
83b35908f7 chore: update lockfile 2026-07-15 00:09:02 -04:00
e3c54a996e feat: input_number signal support, implement Clone for a lot of error types 2026-07-14 22:53:42 -04:00
38c30d87d0 chore(home-assistant): rename HomeAssistantState::UnexpectedErr to Unexpected because I was expecting it to contain an error type returning to the project after so long 2026-07-14 21:29:50 -04:00
17fabbf178 chore: eliminate all usage of pyo3::prelude::* 2026-07-14 20:50:07 -04:00
6abe278aa3 chore(home-assistant): streamline Python conversion for UnexpectedState 2026-07-14 20:29:32 -04:00
120ad97c53 chore(home-assistant): streamline Python conversion for LightState 2026-07-14 20:27:51 -04:00
338a8c571c chore(home-assistant): streamline Python conversion for ServiceDomain and ServiceId 2026-07-14 20:24:27 -04:00
34d0266c1f chore: make MobileAppDeviceId type instead of using ObjectId as that is the wrong abstraction 2026-07-14 20:14:45 -04:00
ac0f574d2b chore(home-assistant): remove unused imports in object_id 2026-07-14 20:06:21 -04:00
9863029eb9 chore(home-assistant): streamline ObjectId's Python definitions 2026-07-14 20:04:15 -04:00
J / Jacob Babich
41cf9d0d09 fix: remove dbg! in StringLiteral ToTokens implementation 2026-07-12 19:39:57 -04:00
J / Jacob Babich
d6418b5fb0 chore(entrypoint): remove unused imports 2026-07-12 19:39:21 -04:00
J / Jacob Babich
c0737c4420 chore: remove unused imports 2026-07-12 19:38:27 -04:00
J / Jacob Babich
53a2fc2a35 chore(protocol): address async fn in extension traits 2026-07-12 19:34:25 -04:00
J / Jacob Babich
044243ae3c chore: update various dependencies (except fjall) to their latest versions 2026-07-12 14:13:06 -04:00
J / Jacob Babich
7f3c2ac30c fix(home-assistant): expose sensor::device_classes::power for use 2026-07-12 14:07:12 -04:00
J / Jacob Babich
3f94015b4f chore: remove AsRef impl from the StringLiteral macro since I don't like that behavior, reorganize it to prevent leaking proc-macro2 / syn / quote 2026-07-12 13:54:57 -04:00
J / Jacob Babich
3e063901be fix: use sensor domain in power sensor signal 2026-07-12 13:43:31 -04:00
9d8b2140dd chore: update lockfile 2026-07-10 17:40:41 -04:00
J / Jacob Babich
0565624599 feat: implement StringLiteral macro 2026-07-09 22:20:24 -04:00
J / Jacob Babich
d6ad9972c7 fix: remove dbg! usages in the proc macros 2026-07-09 01:39:56 -04:00
J / Jacob Babich
9a9cefee37 feat: FromPyFromStr and ToStrToPy macros YAY 2026-07-09 01:35:49 -04:00
c637ad2d76 chore: initial draft of proc macros 2026-07-08 17:30:52 -04:00
8e43ff75a5 fix(home-assistant): propagate set_item errors in NotifyMobileAppServiceData's IntoPyObject implementation 2026-07-08 17:01:28 -04:00
cebf7dd93b chore(protocol): rename invert to inverted 2026-07-08 16:58:03 -04:00
89404a56e0 feat(entrypoint): try using the do not disturb command 2026-07-08 16:49:37 -04:00
9e7e5cdcc8 fix: make some types of mobile app notifications work 2026-07-08 16:49:21 -04:00
75 changed files with 1771 additions and 1503 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.history
/target /target

1253
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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 }

View File

@@ -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 },
} }

View File

@@ -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,
} }

View File

@@ -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};

View File

@@ -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

View File

@@ -1,3 +1,3 @@
pub mod connection; pub mod connection;
mod impl_protocol;
pub mod messages; pub mod messages;
mod protocol;

View File

@@ -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,

View File

@@ -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,
}, },
}; };

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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,
@@ -101,6 +99,8 @@ async fn real_main(
let built_at = build_info::BUILD_TIME; let built_at = build_info::BUILD_TIME;
tracing::info!(built_at); tracing::info!(built_at);
tokio::join!(
async {
let mut bathroom_mirror_lights = [ let mut bathroom_mirror_lights = [
LB130USHandle::new( LB130USHandle::new(
([10, 0, 3, 80], 9999).into(), ([10, 0, 3, 80], 9999).into(),
@@ -126,16 +126,124 @@ async fn real_main(
let tasks = bathroom_mirror_lights let tasks = bathroom_mirror_lights
.iter_mut() .iter_mut()
.map(|bathroom_mirror_light| bathroom_mirror_light.turn_to_temperature(temperature)); .map(|bathroom_mirror_light| {
bathroom_mirror_light.turn_to_temperature(temperature)
});
let mut tasks = FuturesUnordered::from_iter(tasks); let mut tasks = FuturesUnordered::from_iter(tasks);
while let Some(result) = tasks.next().await { while let Some(result) = tasks.next().await {
match result { match result {
Ok(()) => {} Ok(()) => {}
Err(error_turning_to_temperature) => tracing::error!(?error_turning_to_temperature), Err(error_turning_to_temperature) => {
tracing::error!(?error_turning_to_temperature)
} }
} }
} }
}
},
async {
let jacob_phone_id = "galaxy_s21_ultra_1";
let jacob_phone_object_id = ObjectId::from_str(jacob_phone_id).unwrap();
let services = Python::attach(|py| home_assistant.services(py)).unwrap();
let mut interval = interval(Duration::from_secs(15));
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
use home_assistant::notify::service::mobile_app::command;
// loop {
// let instant = interval.tick().await;
// let context: Option<Context<()>> = None;
// let target: Option<()> = None;
// let turn_off_result: Result<Py<PyAny>, _> = services.call_service(command::DoNotDisturb {
// object_id: jacob_phone_object_id.clone(),
// filter: command::DoNotDisturbFilter::Off,
// }, context, target, false).await;
// dbg!(turn_off_result);
// let instant = interval.tick().await;
// let context: Option<Context<()>> = None;
// let target: Option<()> = None;
// let alarms_only_result: Result<Py<PyAny>, _> = services.call_service(command::DoNotDisturb {
// object_id: jacob_phone_object_id.clone(),
// filter: command::DoNotDisturbFilter::AlarmsOnly,
// }, context, target, false).await;
// dbg!(alarms_only_result);
// let instant = interval.tick().await;
// let context: Option<Context<()>> = None;
// let target: Option<()> = None;
// let priority_only_result: Result<Py<PyAny>, _> = services.call_service(command::DoNotDisturb {
// object_id: jacob_phone_object_id.clone(),
// filter: command::DoNotDisturbFilter::PriorityOnly,
// }, context, target, false).await;
// dbg!(priority_only_result);
// let instant = interval.tick().await;
// let context: Option<Context<()>> = None;
// let target: Option<()> = None;
// let total_silence_result: Result<Py<PyAny>, _> = services.call_service(command::DoNotDisturb {
// object_id: jacob_phone_object_id.clone(),
// filter: command::DoNotDisturbFilter::TotalSilence,
// }, context, target, false).await;
// 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)
}),
);
}
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]

View File

@@ -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 }

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View 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}"),
}
}
}

View File

@@ -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),
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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};

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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)
} }
} }

View 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>,
}

View File

@@ -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(|_| ()) },
))
}

View 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,
}

View File

@@ -1,4 +1,4 @@
use pyo3::prelude::*; use pyo3::FromPyObject;
#[derive(Debug, FromPyObject)] #[derive(Debug, FromPyObject)]
#[pyo3(from_item_all)] #[pyo3(from_item_all)]

View File

@@ -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)?;

View File

@@ -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 })
}
} }
} }
} }

View File

@@ -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 {

View File

@@ -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)?;

View File

@@ -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())

View File

@@ -1,5 +1,5 @@
mod do_not_disturb; pub mod do_not_disturb;
mod request_location_update; pub mod request_location_update;
pub use do_not_disturb::DoNotDisturb; pub use do_not_disturb::{DoNotDisturb, Filter as DoNotDisturbFilter};
pub use request_location_update::RequestLocationUpdate; pub use request_location_update::RequestLocationUpdate;

View File

@@ -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())

View File

@@ -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);

View File

@@ -1,16 +1,21 @@
use std::{convert::Infallible, str::FromStr}; use std::str::FromStr;
use pyo3::{types::PyString, Bound, IntoPyObject, Python}; 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,19 +172,7 @@ pub enum MediaStream {
System, System,
} }
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one #[derive(Debug, Default, Clone, typed_builder::TypedBuilder)]
impl<'py> IntoPyObject<'py> for MediaStream {
type Target = PyString;
type Output = Bound<'py, Self::Target>;
type Error = Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}
#[derive(Debug, Default, Clone, IntoPyObject, typed_builder::TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))] #[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
pub struct NotifyMobileAppServiceDataData { pub struct NotifyMobileAppServiceDataData {
actions: Option<Vec<Action>>, actions: Option<Vec<Action>>,
@@ -225,13 +182,82 @@ pub struct NotifyMobileAppServiceDataData {
visibility: Option<Visibility>, visibility: Option<Visibility>,
} }
#[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)] // TODO: derive macro that does this for me
impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceDataData {
type Target = PyDict;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let NotifyMobileAppServiceDataData {
actions,
command,
media_stream,
tts_text,
visibility,
} = self;
let dict = PyDict::new(py);
if let Some(actions) = actions {
dict.set_item("actions", actions)?;
}
if let Some(command) = command {
dict.set_item("command", command)?;
}
if let Some(media_stream) = media_stream {
dict.set_item("media_stream", media_stream)?;
}
if let Some(tts_text) = tts_text {
dict.set_item("tts_text", tts_text)?;
}
if let Some(visibility) = visibility {
dict.set_item("visibility", visibility)?;
}
Ok(dict)
}
}
#[derive(Debug, 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 NotifyMobileAppServiceData { pub struct NotifyMobileAppServiceData {
#[builder(!default, setter(!strip_option))] #[builder(!default, setter(!strip_option))]
message: String, message: String,
title: Option<String>, title: Option<String>,
#[builder(setter(!strip_option))] #[builder(setter(!strip_option))]
data: NotifyMobileAppServiceDataData, data: NotifyMobileAppServiceDataData,
} }
// TODO: derive macro that does this for me
impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceData {
type Target = PyDict;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let NotifyMobileAppServiceData {
message,
title,
data,
} = self;
let dict = PyDict::new(py);
dict.set_item("message", message)?;
if let Some(title) = title {
dict.set_item("title", title)?;
}
dict.set_item("data", data)?;
Ok(dict)
}
}

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)
}
}

View File

@@ -1 +1 @@
mod power; pub mod power;

View File

@@ -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)?;

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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,
} }

View File

@@ -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)
}
}

View File

@@ -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)
} }
} }

View File

@@ -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))
} }
} }

View File

@@ -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)
} }
} }

View File

@@ -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

View File

@@ -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>,

View File

@@ -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 }

View File

@@ -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),
}; };

View File

@@ -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"] }

View File

@@ -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)?;

View 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 }

View 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);
}
}

View 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;

View 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);
}
}

View 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 }

View 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()
}

View File

@@ -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 }

View 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))
}
}

View 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)
}
}

View File

@@ -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))
}
} }

View 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 }

View 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 }
}

View 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" }

View 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
View 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
View 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 {}