feat: early interface with Python
This commit is contained in:
91
src/arbitrary/arbitrary.rs
Normal file
91
src/arbitrary/arbitrary.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use chrono::DateTime;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
use ijson::{IArray, INumber, IObject, IString, IValue};
|
||||||
|
use pyo3::{
|
||||||
|
exceptions::{PyTypeError, PyValueError},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use snafu::Snafu;
|
||||||
|
|
||||||
|
use super::{finite_f64::FiniteF64, map::Map, map_key::MapKey};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::From, derive_more::TryInto)]
|
||||||
|
pub enum Arbitrary {
|
||||||
|
Null,
|
||||||
|
Bool(bool),
|
||||||
|
Integer(i64),
|
||||||
|
Float(FiniteF64),
|
||||||
|
String(String),
|
||||||
|
Array(Vec<Arbitrary>),
|
||||||
|
Map(Map),
|
||||||
|
DateTime(DateTime<Tz>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MapKey> for Arbitrary {
|
||||||
|
fn from(map_key: MapKey) -> Self {
|
||||||
|
match map_key {
|
||||||
|
MapKey::Null => Arbitrary::Null,
|
||||||
|
MapKey::Bool(b) => Arbitrary::Bool(b),
|
||||||
|
MapKey::Integer(int) => Arbitrary::Integer(int),
|
||||||
|
MapKey::String(s) => Arbitrary::String(s),
|
||||||
|
// close enough
|
||||||
|
MapKey::Tuple(vec) => Arbitrary::Array(vec.into_iter().map(Into::into).collect()),
|
||||||
|
MapKey::DateTime(date_time) => Arbitrary::DateTime(date_time),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum MapKeyFromArbitraryError {
|
||||||
|
#[snafu(display("floats aren't supported as map keys yet. got {value:?}"))]
|
||||||
|
FloatNotSupported { value: FiniteF64 },
|
||||||
|
#[snafu(display("a map cannot be a map key. got {value:?}"))]
|
||||||
|
MapCannotBeAMapKey { value: Map },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arbitrary> for IValue {
|
||||||
|
fn from(value: Arbitrary) -> Self {
|
||||||
|
match value {
|
||||||
|
Arbitrary::Null => IValue::NULL,
|
||||||
|
Arbitrary::Bool(true) => IValue::TRUE,
|
||||||
|
Arbitrary::Bool(false) => IValue::FALSE,
|
||||||
|
Arbitrary::Integer(int) => INumber::from(int).into(),
|
||||||
|
Arbitrary::Float(float) => INumber::try_from(f64::from(float)).unwrap().into(),
|
||||||
|
Arbitrary::String(s) => IString::from(s).into(),
|
||||||
|
Arbitrary::Array(vec) => {
|
||||||
|
IArray::from_iter(vec.into_iter().map(Into::<IValue>::into)).into()
|
||||||
|
}
|
||||||
|
Arbitrary::Map(Map(btree_map)) => {
|
||||||
|
let mut object = IObject::new();
|
||||||
|
|
||||||
|
for (key, value) in btree_map {
|
||||||
|
let key: IString = key.into();
|
||||||
|
object.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
object.into()
|
||||||
|
}
|
||||||
|
Arbitrary::DateTime(date_time) => IString::from(date_time.to_rfc3339()).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'py> FromPyObject<'py> for Arbitrary {
|
||||||
|
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||||
|
if let Ok(map_key) = ob.extract::<MapKey>() {
|
||||||
|
Ok(map_key.into())
|
||||||
|
} else if let Ok(map) = ob.extract() {
|
||||||
|
Ok(Self::Map(map))
|
||||||
|
} else if let Ok(f) = ob.extract::<f64>() {
|
||||||
|
let f = FiniteF64::try_from(f).map_err(|err| PyValueError::new_err(err.to_string()))?;
|
||||||
|
Ok(Self::Float(f))
|
||||||
|
} else if let Ok(vec) = ob.extract() {
|
||||||
|
Ok(Self::Array(vec))
|
||||||
|
} else {
|
||||||
|
let type_name = ob.get_type().fully_qualified_name()?;
|
||||||
|
Err(PyTypeError::new_err(format!(
|
||||||
|
"can't extract an arbitrary from a {type_name}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
src/arbitrary/finite_f64.rs
Normal file
22
src/arbitrary/finite_f64.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use snafu::Snafu;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::Into)]
|
||||||
|
pub struct FiniteF64(f64);
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
#[snafu(display("{value:?} is not finite"))]
|
||||||
|
pub struct NotFinite {
|
||||||
|
value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<f64> for FiniteF64 {
|
||||||
|
type Error = NotFinite;
|
||||||
|
|
||||||
|
fn try_from(value: f64) -> Result<Self, Self::Error> {
|
||||||
|
if value.is_finite() {
|
||||||
|
Ok(Self(value))
|
||||||
|
} else {
|
||||||
|
Err(NotFinite { value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/arbitrary/map.rs
Normal file
16
src/arbitrary/map.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use super::{arbitrary::Arbitrary, map_key::MapKey};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::From, derive_more::Into)]
|
||||||
|
pub struct Map(pub BTreeMap<MapKey, Arbitrary>);
|
||||||
|
|
||||||
|
impl<'py> FromPyObject<'py> for Map {
|
||||||
|
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||||
|
let inner: BTreeMap<MapKey, Arbitrary> = ob.extract()?;
|
||||||
|
|
||||||
|
Ok(Self(inner))
|
||||||
|
}
|
||||||
|
}
|
87
src/arbitrary/map_key.rs
Normal file
87
src/arbitrary/map_key.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
use ijson::IString;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use pyo3::{exceptions::PyTypeError, prelude::*, types::PyNone};
|
||||||
|
|
||||||
|
use super::arbitrary::{Arbitrary, MapKeyFromArbitraryError};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum MapKey {
|
||||||
|
Null,
|
||||||
|
Bool(bool),
|
||||||
|
Integer(i64),
|
||||||
|
String(String),
|
||||||
|
Tuple(Vec<MapKey>),
|
||||||
|
DateTime(DateTime<Tz>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MapKey {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MapKey::Null => write!(f, "null"),
|
||||||
|
MapKey::Bool(b) => write!(f, "{b}"),
|
||||||
|
MapKey::Integer(i) => write!(f, "{i}"),
|
||||||
|
MapKey::String(s) => write!(f, "{s}"),
|
||||||
|
MapKey::Tuple(vec) => {
|
||||||
|
let comma_separated =
|
||||||
|
Itertools::intersperse(vec.iter().map(ToString::to_string), ", ".to_string());
|
||||||
|
write!(f, "({})", String::from_iter(comma_separated))
|
||||||
|
}
|
||||||
|
MapKey::DateTime(date_time) => {
|
||||||
|
write!(f, "{}", date_time.to_rfc3339())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'py> FromPyObject<'py> for MapKey {
|
||||||
|
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||||
|
if let Ok(_none) = ob.downcast::<PyNone>() {
|
||||||
|
Ok(Self::Null)
|
||||||
|
} else if let Ok(b) = ob.extract() {
|
||||||
|
Ok(Self::Bool(b))
|
||||||
|
} else if let Ok(int) = ob.extract() {
|
||||||
|
Ok(Self::Integer(int))
|
||||||
|
} else if let Ok(s) = ob.extract() {
|
||||||
|
Ok(Self::String(s))
|
||||||
|
} else if let Ok(tuple) = ob.extract() {
|
||||||
|
Ok(Self::Tuple(tuple))
|
||||||
|
} else {
|
||||||
|
let type_name = ob.get_type().fully_qualified_name()?;
|
||||||
|
Err(PyTypeError::new_err(format!(
|
||||||
|
"can't extract a map key from a {type_name}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MapKey> for IString {
|
||||||
|
fn from(map_key: MapKey) -> Self {
|
||||||
|
Self::from(map_key.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Arbitrary> for MapKey {
|
||||||
|
type Error = MapKeyFromArbitraryError;
|
||||||
|
|
||||||
|
fn try_from(arbitrary: Arbitrary) -> Result<Self, Self::Error> {
|
||||||
|
match arbitrary {
|
||||||
|
Arbitrary::Null => Ok(MapKey::Null),
|
||||||
|
Arbitrary::Bool(b) => Ok(MapKey::Bool(b)),
|
||||||
|
Arbitrary::Integer(i) => Ok(MapKey::Integer(i)),
|
||||||
|
Arbitrary::String(s) => Ok(MapKey::String(s)),
|
||||||
|
Arbitrary::Array(vec) => {
|
||||||
|
let tuple = Result::from_iter(vec.into_iter().map(TryInto::try_into))?;
|
||||||
|
Ok(MapKey::Tuple(tuple))
|
||||||
|
}
|
||||||
|
Arbitrary::DateTime(date_time) => Ok(MapKey::DateTime(date_time)),
|
||||||
|
Arbitrary::Float(float) => {
|
||||||
|
Err(MapKeyFromArbitraryError::FloatNotSupported { value: float })
|
||||||
|
}
|
||||||
|
Arbitrary::Map(map) => Err(MapKeyFromArbitraryError::MapCannotBeAMapKey { value: map }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
src/arbitrary/mod.rs
Normal file
4
src/arbitrary/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod arbitrary;
|
||||||
|
pub mod finite_f64;
|
||||||
|
pub mod map;
|
||||||
|
pub mod map_key;
|
34
src/home_assistant/domain.rs
Normal file
34
src/home_assistant/domain.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, EnumString, strum::Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum Domain {
|
||||||
|
Automation,
|
||||||
|
BinarySensor,
|
||||||
|
Button,
|
||||||
|
Camera,
|
||||||
|
Climate,
|
||||||
|
Conversation,
|
||||||
|
Cover,
|
||||||
|
DeviceTracker,
|
||||||
|
Group,
|
||||||
|
InputDatetime,
|
||||||
|
InputNumber,
|
||||||
|
InputSelect,
|
||||||
|
InputText,
|
||||||
|
Light,
|
||||||
|
Lock,
|
||||||
|
MediaPlayer,
|
||||||
|
Notify,
|
||||||
|
Person,
|
||||||
|
Remote,
|
||||||
|
Scene,
|
||||||
|
Select,
|
||||||
|
Sensor,
|
||||||
|
Sun,
|
||||||
|
Switch,
|
||||||
|
Tag,
|
||||||
|
Update,
|
||||||
|
Weather,
|
||||||
|
Zone,
|
||||||
|
}
|
60
src/home_assistant/entity_id.rs
Normal file
60
src/home_assistant/entity_id.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use pyo3::{exceptions::PyValueError, prelude::*};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
domain::Domain,
|
||||||
|
object_id::{ObjectId, ObjectIdParsingError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EntityId(pub Domain, pub ObjectId);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Snafu)]
|
||||||
|
pub enum EntityIdParsingError {
|
||||||
|
#[snafu(display("entity IDs have a dot / period in them, e.g. light.kitchen_lamp"))]
|
||||||
|
MissingDot,
|
||||||
|
|
||||||
|
#[snafu(display("could not parse the domain part of the entity ID"))]
|
||||||
|
ParsingDomain { source: <Domain as FromStr>::Err },
|
||||||
|
|
||||||
|
#[snafu(display("could not parse the object ID part of the entity ID"))]
|
||||||
|
ParsingObjectId { source: ObjectIdParsingError },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for EntityId {
|
||||||
|
type Err = EntityIdParsingError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (domain, object_id) = s.split_once('.').ok_or(EntityIdParsingError::MissingDot)?;
|
||||||
|
|
||||||
|
let domain = domain.parse().context(ParsingDomainSnafu)?;
|
||||||
|
let object_id = object_id.parse().context(ParsingObjectIdSnafu)?;
|
||||||
|
|
||||||
|
Ok(Self(domain, object_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for EntityId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Self(domain, object_id) = self;
|
||||||
|
|
||||||
|
write!(f, "{domain}.{object_id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EntityIdParsingError> for PyErr {
|
||||||
|
fn from(error: EntityIdParsingError) -> Self {
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
Ok(entity_id)
|
||||||
|
}
|
||||||
|
}
|
24
src/home_assistant/event/context/context.rs
Normal file
24
src/home_assistant/event/context/context.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use crate::home_assistant::event::event::Event;
|
||||||
|
|
||||||
|
use super::id::Id;
|
||||||
|
|
||||||
|
/// The context that triggered something.
|
||||||
|
#[derive(Debug, FromPyObject)]
|
||||||
|
pub struct Context {
|
||||||
|
pub id: Id,
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
pub parent_id: Option<String>,
|
||||||
|
/// In order to prevent cycles, the user must extract this to an [`Event<Arbitrary>`](super::event::Event) themself (or even specify a specific type parameter!)
|
||||||
|
origin_event: Py<PyAny>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn origin_event<'py, Type: FromPyObject<'py>, Data: FromPyObject<'py>>(
|
||||||
|
&self,
|
||||||
|
py: Python<'py>,
|
||||||
|
) -> PyResult<Event<Type, Data>> {
|
||||||
|
self.origin_event.extract(py)
|
||||||
|
}
|
||||||
|
}
|
20
src/home_assistant/event/context/id.rs
Normal file
20
src/home_assistant/event/context/id.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use pyo3::prelude::*;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Id {
|
||||||
|
Ulid(Ulid),
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'py> FromPyObject<'py> for Id {
|
||||||
|
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||||
|
let s = ob.extract::<String>()?;
|
||||||
|
|
||||||
|
if let Ok(ulid) = s.parse() {
|
||||||
|
Ok(Id::Ulid(ulid))
|
||||||
|
} else {
|
||||||
|
Ok(Id::Other(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
src/home_assistant/event/context/mod.rs
Normal file
2
src/home_assistant/event/context/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod context;
|
||||||
|
pub mod id;
|
30
src/home_assistant/event/event.rs
Normal file
30
src/home_assistant/event/event.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use super::{context::context::Context, event_origin::EventOrigin};
|
||||||
|
|
||||||
|
/// Representation of an event within the bus.
|
||||||
|
#[derive(Debug, FromPyObject)]
|
||||||
|
pub struct Event<Type, Data> {
|
||||||
|
pub event_type: Type,
|
||||||
|
pub data: Data,
|
||||||
|
pub origin: EventOrigin,
|
||||||
|
/// In order to prevent cycles, the user must extract this to a [`Context`](super::context::Context) themself, using the [`context`](Self::context) method
|
||||||
|
context: Py<PyAny>,
|
||||||
|
time_fired_timestamp: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Type, Data> Event<Type, Data> {
|
||||||
|
pub fn context<'py>(&self, py: Python<'py>) -> PyResult<Context> {
|
||||||
|
self.context.extract(py)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
21
src/home_assistant/event/event_origin.rs
Normal file
21
src/home_assistant/event/event_origin.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use pyo3::{exceptions::PyValueError, prelude::*};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, strum::EnumString, strum::Display)]
|
||||||
|
#[strum(serialize_all = "UPPERCASE")]
|
||||||
|
pub enum EventOrigin {
|
||||||
|
Local,
|
||||||
|
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()))?;
|
||||||
|
|
||||||
|
Ok(event_origin)
|
||||||
|
}
|
||||||
|
}
|
4
src/home_assistant/event/mod.rs
Normal file
4
src/home_assistant/event/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod context;
|
||||||
|
pub mod event;
|
||||||
|
pub mod event_origin;
|
||||||
|
pub mod specific;
|
1
src/home_assistant/event/specific/mod.rs
Normal file
1
src/home_assistant/event/specific/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod state_changed;
|
34
src/home_assistant/event/specific/state_changed.rs
Normal file
34
src/home_assistant/event/specific/state_changed.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use pyo3::exceptions::PyValueError;
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use crate::home_assistant::{entity_id::EntityId, state::State};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Type;
|
||||||
|
|
||||||
|
impl<'py> FromPyObject<'py> for Type {
|
||||||
|
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||||
|
let s = ob.extract::<&str>()?;
|
||||||
|
|
||||||
|
if s == "state_changed" {
|
||||||
|
Ok(Type)
|
||||||
|
} else {
|
||||||
|
Err(PyValueError::new_err(format!(
|
||||||
|
"expected a string of value 'state_changed', but got {s}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromPyObject)]
|
||||||
|
#[pyo3(from_item_all)]
|
||||||
|
pub struct Data {
|
||||||
|
pub entity_id: EntityId,
|
||||||
|
pub old_state: Option<State>,
|
||||||
|
pub new_state: Option<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A state changed event is fired when on state write the state is changed.
|
||||||
|
pub type Event = super::super::event::Event<Type, Data>;
|
43
src/home_assistant/home_assistant.rs
Normal file
43
src/home_assistant/home_assistant.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use crate::python_utils::{detach, validate_type_by_name};
|
||||||
|
|
||||||
|
use super::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> {
|
||||||
|
// region: Validation
|
||||||
|
validate_type_by_name(ob, "HomeAssistant")?;
|
||||||
|
// endregion: Validation
|
||||||
|
|
||||||
|
Ok(Self(detach(ob)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HomeAssistant {
|
||||||
|
/// Return the representation
|
||||||
|
pub fn repr(&self, py: &Python) -> Result<String, PyErr> {
|
||||||
|
let bound = self.0.bind(*py);
|
||||||
|
let repr = bound.repr()?;
|
||||||
|
repr.extract()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return if Home Assistant is running.
|
||||||
|
pub fn is_running(&self, py: &Python) -> Result<bool, PyErr> {
|
||||||
|
let is_running = self.0.getattr(*py, "is_running")?;
|
||||||
|
is_running.extract(*py)
|
||||||
|
}
|
||||||
|
/// Return if Home Assistant is stopping.
|
||||||
|
pub fn is_stopping(&self, py: &Python) -> Result<bool, PyErr> {
|
||||||
|
let is_stopping = self.0.getattr(*py, "is_stopping")?;
|
||||||
|
is_stopping.extract(*py)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn states(&self, py: &Python) -> Result<StateMachine, PyErr> {
|
||||||
|
let states = self.0.getattr(*py, "states")?;
|
||||||
|
states.extract(*py)
|
||||||
|
}
|
||||||
|
}
|
7
src/home_assistant/mod.rs
Normal file
7
src/home_assistant/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod domain;
|
||||||
|
pub mod entity_id;
|
||||||
|
pub mod event;
|
||||||
|
pub mod home_assistant;
|
||||||
|
pub mod object_id;
|
||||||
|
pub mod state;
|
||||||
|
pub mod state_machine;
|
35
src/home_assistant/object_id.rs
Normal file
35
src/home_assistant/object_id.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use std::{str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use pyo3::{exceptions::PyValueError, PyErr};
|
||||||
|
use snafu::Snafu;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::Display)]
|
||||||
|
pub struct ObjectId(Arc<str>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Snafu)]
|
||||||
|
#[snafu(display("expected a lowercase ASCII alphabetical character (i.e. a through z) or an underscore (i.e. _) but encountered {encountered}"))]
|
||||||
|
pub struct ObjectIdParsingError {
|
||||||
|
encountered: char,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ObjectId {
|
||||||
|
type Err = ObjectIdParsingError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
for c in s.chars() {
|
||||||
|
match c {
|
||||||
|
'a'..='z' => {}
|
||||||
|
'_' => {}
|
||||||
|
_ => return Err(ObjectIdParsingError { encountered: c }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(s.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ObjectIdParsingError> for PyErr {
|
||||||
|
fn from(error: ObjectIdParsingError) -> Self {
|
||||||
|
PyValueError::new_err(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
17
src/home_assistant/state.rs
Normal file
17
src/home_assistant/state.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use crate::{arbitrary::map::Map, home_assistant::entity_id::EntityId};
|
||||||
|
|
||||||
|
use super::event::context::context::Context;
|
||||||
|
|
||||||
|
#[derive(Debug, FromPyObject)]
|
||||||
|
pub struct State {
|
||||||
|
pub entity_id: EntityId,
|
||||||
|
pub state: String,
|
||||||
|
pub attributes: Map,
|
||||||
|
pub last_changed: Option<DateTime<Utc>>,
|
||||||
|
pub last_reported: Option<DateTime<Utc>>,
|
||||||
|
pub last_updated: Option<DateTime<Utc>>,
|
||||||
|
pub context: Context,
|
||||||
|
}
|
29
src/home_assistant/state_machine.rs
Normal file
29
src/home_assistant/state_machine.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
home_assistant::entity_id::EntityId,
|
||||||
|
python_utils::{detach, validate_type_by_name},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::state::State;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StateMachine(Py<PyAny>);
|
||||||
|
|
||||||
|
impl<'py> FromPyObject<'py> for StateMachine {
|
||||||
|
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||||
|
// region: Validation
|
||||||
|
validate_type_by_name(ob, "StateMachine")?;
|
||||||
|
// endregion: Validation
|
||||||
|
|
||||||
|
Ok(Self(detach(ob)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateMachine {
|
||||||
|
pub fn get(&self, py: &Python, entity_id: EntityId) -> Result<Option<State>, PyErr> {
|
||||||
|
let args = (entity_id.to_string(),);
|
||||||
|
let state = self.0.call_method1(*py, "get", args)?;
|
||||||
|
state.extract(*py)
|
||||||
|
}
|
||||||
|
}
|
27
src/lib.rs
27
src/lib.rs
@@ -1,27 +1,42 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use home_assistant::home_assistant::HomeAssistant;
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
|
use tracing::Level;
|
||||||
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
|
|
||||||
async fn real_main() -> ! {
|
mod arbitrary;
|
||||||
let duration = Duration::from_millis(400);
|
mod home_assistant;
|
||||||
|
mod python_utils;
|
||||||
|
|
||||||
|
async fn real_main(home_assistant: HomeAssistant) -> ! {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(Level::TRACE)
|
||||||
|
.with_span_events(FmtSpan::ACTIVE)
|
||||||
|
.pretty()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let duration = Duration::from_millis(5900);
|
||||||
let mut interval = interval(duration);
|
let mut interval = interval(duration);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let instant = interval.tick().await;
|
let instant = interval.tick().await;
|
||||||
|
|
||||||
println!("it is now {instant:?}");
|
tracing::debug!(?instant, "it is now");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn main(py: Python) -> PyResult<&PyAny> {
|
fn main<'p>(py: Python<'p>, home_assistant: HomeAssistant) -> PyResult<Bound<'p, PyAny>> {
|
||||||
pyo3_asyncio::tokio::future_into_py(py, async { Ok(real_main().await) })
|
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async {
|
||||||
|
real_main(home_assistant).await;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Python module implemented in Rust.
|
/// A Python module implemented in Rust.
|
||||||
#[pymodule]
|
#[pymodule]
|
||||||
fn smart_home_in_rust_with_home_assistant(_py: Python, m: &PyModule) -> PyResult<()> {
|
fn smart_home_in_rust_with_home_assistant(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
m.add_function(wrap_pyfunction!(main, m)?)?;
|
m.add_function(wrap_pyfunction!(main, m)?)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
21
src/python_utils.rs
Normal file
21
src/python_utils.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use pyo3::{exceptions::PyTypeError, prelude::*};
|
||||||
|
|
||||||
|
/// Create a GIL-independent reference (similar to [`Arc`](std::sync::Arc))
|
||||||
|
pub fn detach<T>(bound: &Bound<T>) -> Py<T> {
|
||||||
|
let py = bound.py();
|
||||||
|
bound.as_unbound().clone_ref(py)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_type_by_name(bound: &Bound<PyAny>, expected_type_name: &str) -> PyResult<()> {
|
||||||
|
let py_type = bound.get_type();
|
||||||
|
let type_name = py_type.name()?;
|
||||||
|
let type_name = type_name.to_str()?;
|
||||||
|
|
||||||
|
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}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
Reference in New Issue
Block a user