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 home_assistant::home_assistant::HomeAssistant;
|
||||
use pyo3::prelude::*;
|
||||
use tokio::time::interval;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
|
||||
async fn real_main() -> ! {
|
||||
let duration = Duration::from_millis(400);
|
||||
mod arbitrary;
|
||||
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);
|
||||
|
||||
loop {
|
||||
let instant = interval.tick().await;
|
||||
|
||||
println!("it is now {instant:?}");
|
||||
tracing::debug!(?instant, "it is now");
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn main(py: Python) -> PyResult<&PyAny> {
|
||||
pyo3_asyncio::tokio::future_into_py(py, async { Ok(real_main().await) })
|
||||
fn main<'p>(py: Python<'p>, home_assistant: HomeAssistant) -> PyResult<Bound<'p, PyAny>> {
|
||||
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async {
|
||||
real_main(home_assistant).await;
|
||||
})
|
||||
}
|
||||
|
||||
/// A Python module implemented in Rust.
|
||||
#[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)?)?;
|
||||
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