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