Compare commits
13 Commits
ead3c6e4a9
...
536743a15d
Author | SHA1 | Date | |
---|---|---|---|
536743a15d | |||
2c8ece1168 | |||
18a0776edb | |||
1ca8acd21e | |||
f8b269b6ce | |||
e4fd9844cc | |||
54e0997799 | |||
a97cf73061 | |||
f422888d37 | |||
f884bc7675 | |||
38e89f31f4 | |||
ea7e9e3c53 | |||
325cb60aa1 |
48
.github/workflows/CI.yml
vendored
48
.github/workflows/CI.yml
vendored
@@ -1,48 +0,0 @@
|
||||
# This file is autogenerated by maturin v1.2.3
|
||||
# To update, run
|
||||
#
|
||||
# maturin generate-ci github
|
||||
#
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
manylinux: auto
|
||||
|
||||
sdist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build sdist
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
1016
Cargo.lock
generated
1016
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
58
Cargo.toml
58
Cargo.toml
@@ -1,45 +1,23 @@
|
||||
[package]
|
||||
name = "smart-home-in-rust-with-home-assistant"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
members = [
|
||||
"arbitrary-value",
|
||||
"driver/kasa",
|
||||
"emitter-and-signal",
|
||||
"entrypoint",
|
||||
"home-assistant",
|
||||
"protocol",
|
||||
"python-utils",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
name = "smart_home_in_rust_with_home_assistant"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
backoff = "0.4.0"
|
||||
chrono = "0.4.40"
|
||||
chrono-tz = "0.10.1"
|
||||
derive_more = { version = "2.0.1", features = [
|
||||
"display",
|
||||
"from",
|
||||
"into",
|
||||
"try_from",
|
||||
"try_into",
|
||||
] }
|
||||
ijson = "0.1.4"
|
||||
itertools = "0.14.0"
|
||||
pyo3 = { version = "0.24.0", features = [
|
||||
"auto-initialize",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
] }
|
||||
pyo3-async-runtimes = { version = "0.24.0", features = ["tokio-runtime"] }
|
||||
serde_json = "1.0.140"
|
||||
shadow-rs = { version = "1.0.1", default-features = false }
|
||||
deranged = "0.4"
|
||||
derive_more = "2.0.1"
|
||||
snafu = "0.8.5"
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
tokio = { version = "1.32.0", features = [
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"sync",
|
||||
"time",
|
||||
] }
|
||||
tokio = "1.32.0"
|
||||
pyo3 = "0.24.0"
|
||||
pyo3-async-runtimes = "0.24.0"
|
||||
tracing = "0.1.37"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = "0.3.17"
|
||||
ulid = "1.2.0"
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = "1.0.1"
|
||||
|
16
arbitrary-value/Cargo.toml
Normal file
16
arbitrary-value/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "arbitrary-value"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
pyo3 = ["dep:pyo3"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
ijson = "0.1.4"
|
||||
itertools = "0.14.0"
|
||||
pyo3 = { workspace = true, optional = true, features = ["chrono", "chrono-tz"] }
|
||||
snafu = { workspace = true }
|
@@ -1,6 +1,7 @@
|
||||
use chrono::DateTime;
|
||||
use chrono_tz::Tz;
|
||||
use ijson::{IArray, INumber, IObject, IString, IValue};
|
||||
#[cfg(feature = "pyo3")]
|
||||
use pyo3::{
|
||||
exceptions::{PyTypeError, PyValueError},
|
||||
prelude::*,
|
||||
@@ -71,6 +72,7 @@ impl From<Arbitrary> for IValue {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl<'py> FromPyObject<'py> for Arbitrary {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
if let Ok(map_key) = ob.extract::<MapKey>() {
|
||||
@@ -91,6 +93,7 @@ impl<'py> FromPyObject<'py> for Arbitrary {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl<'py> IntoPyObject<'py> for Arbitrary {
|
||||
type Target = PyAny;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use pyo3::IntoPyObject;
|
||||
use snafu::Snafu;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Into, IntoPyObject)]
|
||||
#[cfg_attr(feature = "pyo3", derive(pyo3::IntoPyObject))]
|
||||
#[derive(Debug, Clone, derive_more::Into)]
|
||||
pub struct FiniteF64(f64);
|
||||
|
||||
#[derive(Debug, Snafu)]
|
@@ -2,3 +2,5 @@ pub mod arbitrary;
|
||||
pub mod finite_f64;
|
||||
pub mod map;
|
||||
pub mod map_key;
|
||||
|
||||
pub use arbitrary::*;
|
7
arbitrary-value/src/map.rs
Normal file
7
arbitrary-value/src/map.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::{arbitrary::Arbitrary, map_key::MapKey};
|
||||
|
||||
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject, pyo3::IntoPyObject))]
|
||||
#[derive(Debug, Clone, Default, derive_more::From, derive_more::Into)]
|
||||
pub struct Map(pub BTreeMap<MapKey, Arbitrary>);
|
@@ -4,6 +4,7 @@ use chrono::DateTime;
|
||||
use chrono_tz::Tz;
|
||||
use ijson::IString;
|
||||
use itertools::Itertools;
|
||||
#[cfg(feature = "pyo3")]
|
||||
use pyo3::{
|
||||
exceptions::PyTypeError,
|
||||
prelude::*,
|
||||
@@ -41,6 +42,7 @@ impl Display for MapKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl<'py> FromPyObject<'py> for MapKey {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
if let Ok(_none) = ob.downcast::<PyNone>() {
|
||||
@@ -62,6 +64,7 @@ impl<'py> FromPyObject<'py> for MapKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl<'py> IntoPyObject<'py> for MapKey {
|
||||
type Target = PyAny;
|
||||
|
17
driver/kasa/Cargo.toml
Normal file
17
driver/kasa/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "driver-kasa"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
backoff = { workspace = true, features = ["tokio"] }
|
||||
deranged = { workspace = true }
|
||||
mac_address = { version = "1.1.8", features = ["serde"] }
|
||||
protocol = { path = "../../protocol" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
serde_repr = "0.1.20"
|
||||
serde_with = "3.12.0"
|
||||
snafu = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-util", "net", "sync", "time"] }
|
||||
tracing = { workspace = true }
|
280
driver/kasa/src/connection.rs
Normal file
280
driver/kasa/src/connection.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use std::{convert::Infallible, io, net::SocketAddr, num::NonZero, time::Duration};
|
||||
|
||||
use backoff::{ExponentialBackoff, ExponentialBackoffBuilder};
|
||||
use protocol::light::{Kelvin, KelvinLight, Light, Rgb, RgbLight};
|
||||
use snafu::{ResultExt, Snafu};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
|
||||
net::{TcpListener, TcpSocket, TcpStream},
|
||||
sync::{mpsc, oneshot, OnceCell},
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use crate::messages::{GetSysInfo, GetSysInfoResponse, LB130USSys, SysInfo};
|
||||
|
||||
struct XorEncryption<const INITIAL_KEY: u8>;
|
||||
|
||||
impl<const INITIAL_KEY: u8> XorEncryption<INITIAL_KEY> {
|
||||
fn encrypt_in_place(bytes: &mut [u8]) {
|
||||
let mut key = INITIAL_KEY;
|
||||
for unencrypted_byte in bytes {
|
||||
let encrypted_byte = key ^ *unencrypted_byte;
|
||||
key = encrypted_byte;
|
||||
*unencrypted_byte = encrypted_byte;
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_in_place(bytes: &mut [u8]) {
|
||||
let mut key = INITIAL_KEY;
|
||||
for encrypted_byte in bytes {
|
||||
let unencrypted_byte = key ^ *encrypted_byte;
|
||||
key = *encrypted_byte;
|
||||
*encrypted_byte = unencrypted_byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_encrypted(mut msg: Vec<u8>) -> Vec<u8> {
|
||||
let length = msg.len() as u32;
|
||||
let big_endian = length.to_be_bytes();
|
||||
XorEncryption::<171>::encrypt_in_place(&mut msg);
|
||||
|
||||
let all_together = big_endian.into_iter().chain(msg);
|
||||
|
||||
all_together.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub enum CommunicationError {
|
||||
SerializeError { source: serde_json::Error },
|
||||
WriteError { source: std::io::Error },
|
||||
ReadError { source: std::io::Error },
|
||||
DeserializeError { source: serde_json::Error },
|
||||
WrongDevice,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum LB130USMessage {
|
||||
GetSysInfo(oneshot::Sender<Result<LB130USSys, CommunicationError>>),
|
||||
}
|
||||
|
||||
async fn lb130us_actor(
|
||||
addr: SocketAddr,
|
||||
disconnect_after_idle: Duration,
|
||||
mut messages: mpsc::Receiver<LB130USMessage>,
|
||||
) {
|
||||
let mut connection_cell = None;
|
||||
|
||||
loop {
|
||||
let (connection, message) = match &mut connection_cell {
|
||||
Some(connection) => match timeout(disconnect_after_idle, messages.recv()).await {
|
||||
Ok(Some(message)) => (connection, message),
|
||||
Ok(None) => return,
|
||||
Err(timed_out) => {
|
||||
tracing::warn!(
|
||||
?addr,
|
||||
?timed_out,
|
||||
"disconnecting from the LB130(US) because the idle timeout has been reached",
|
||||
);
|
||||
|
||||
connection_cell.take();
|
||||
continue;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let Some(message) = messages.recv().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"connecting for a first time / reconnecting after having gone idle..."
|
||||
);
|
||||
|
||||
match backoff::future::retry_notify(
|
||||
ExponentialBackoff::default(),
|
||||
|| async {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let (reader, writer) = stream.into_split();
|
||||
|
||||
let buf_reader = BufReader::new(reader);
|
||||
let buf_writer = BufWriter::new(writer);
|
||||
|
||||
Ok((buf_reader, buf_writer))
|
||||
},
|
||||
|err, duration| {
|
||||
tracing::error!(?err, ?duration);
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(connection) => (connection_cell.insert(connection), message),
|
||||
Err(err) => {
|
||||
tracing::error!(?addr, ?err, "error connecting to an LB130(US)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (reader, writer) = connection;
|
||||
|
||||
tracing::info!("yay connected and got a message");
|
||||
|
||||
// TODO: do something
|
||||
match message {
|
||||
LB130USMessage::GetSysInfo(callback) => {
|
||||
tracing::info!("going to try to get sys info for you...");
|
||||
|
||||
// TODO: extract to its own function
|
||||
let outgoing = GetSysInfo;
|
||||
let outgoing = match serde_json::to_vec(&outgoing) {
|
||||
Ok(outgoing) => outgoing,
|
||||
Err(err) => {
|
||||
// TODO (continued) instead of doing stuff like this
|
||||
let _ =
|
||||
callback.send(Err(CommunicationError::SerializeError { source: err }));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!(?outgoing);
|
||||
|
||||
let encrypted_outgoing = into_encrypted(outgoing);
|
||||
|
||||
tracing::info!(?encrypted_outgoing);
|
||||
|
||||
if let Err(err) = writer.write_all(&encrypted_outgoing).await {
|
||||
connection_cell.take();
|
||||
let _ = callback.send(Err(CommunicationError::WriteError { source: err }));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = writer.flush().await {
|
||||
connection_cell.take();
|
||||
let _ = callback.send(Err(CommunicationError::WriteError { source: err }));
|
||||
continue;
|
||||
}
|
||||
tracing::info!("sent it, now about to try to get a response");
|
||||
|
||||
let incoming_length = match reader.read_u32().await {
|
||||
Ok(incoming_length) => incoming_length,
|
||||
Err(err) => {
|
||||
connection_cell.take();
|
||||
let _ = callback.send(Err(CommunicationError::ReadError { source: err }));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
tracing::info!(?incoming_length);
|
||||
|
||||
let mut incoming_message = Vec::new();
|
||||
incoming_message.resize(incoming_length as usize, 0);
|
||||
if let Err(err) = reader.read_exact(&mut incoming_message).await {
|
||||
connection_cell.take();
|
||||
let _ = callback.send(Err(CommunicationError::ReadError { source: err }));
|
||||
continue;
|
||||
}
|
||||
|
||||
XorEncryption::<171>::decrypt_in_place(&mut incoming_message);
|
||||
tracing::info!(?incoming_message);
|
||||
|
||||
let response: GetSysInfoResponse = match serde_json::from_slice(&incoming_message) {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
let _ = callback
|
||||
.send(Err(CommunicationError::DeserializeError { source: err }));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
tracing::info!(?response);
|
||||
|
||||
let SysInfo::LB130US(lb130us) = response.system.get_sysinfo else {
|
||||
let _ = callback.send(Err(CommunicationError::WrongDevice));
|
||||
continue;
|
||||
};
|
||||
tracing::info!(?lb130us);
|
||||
|
||||
let _ = callback.send(Ok(lb130us));
|
||||
tracing::info!("cool, gave a response! onto the next message!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LB130USHandle {
|
||||
sender: mpsc::Sender<LB130USMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub enum HandleError {
|
||||
CommunicationError { source: CommunicationError },
|
||||
Dead,
|
||||
}
|
||||
|
||||
impl LB130USHandle {
|
||||
pub fn new(addr: SocketAddr, disconnect_after_idle: Duration, buffer: NonZero<usize>) -> Self {
|
||||
let (sender, receiver) = mpsc::channel(buffer.get());
|
||||
tokio::spawn(lb130us_actor(addr, disconnect_after_idle, receiver));
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
pub async fn get_sysinfo(&self) -> Result<LB130USSys, HandleError> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
self.sender
|
||||
.send(LB130USMessage::GetSysInfo(sender))
|
||||
.await
|
||||
.map_err(|_| HandleError::Dead)?;
|
||||
receiver
|
||||
.await
|
||||
.map_err(|_| HandleError::Dead)?
|
||||
.context(CommunicationSnafu)
|
||||
}
|
||||
}
|
||||
|
||||
impl Light for LB130USHandle {
|
||||
type IsOnError = Infallible; // TODO
|
||||
|
||||
async fn is_on(&self) -> Result<bool, Self::IsOnError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
type IsOffError = Infallible; // TODO
|
||||
|
||||
async fn is_off(&self) -> Result<bool, Self::IsOffError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
type TurnOnError = Infallible; // TODO
|
||||
|
||||
async fn turn_on(&mut self) -> Result<(), Self::TurnOnError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
type TurnOffError = Infallible; // TODO
|
||||
|
||||
async fn turn_off(&mut self) -> Result<(), Self::TurnOffError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
type ToggleError = Infallible; // TODO
|
||||
|
||||
async fn toggle(&mut self) -> Result<(), Self::ToggleError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl KelvinLight for LB130USHandle {
|
||||
type TurnToKelvinError = Infallible; // TODO
|
||||
|
||||
async fn turn_to_kelvin(&mut self, temperature: Kelvin) -> Result<(), Self::TurnToKelvinError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl RgbLight for LB130USHandle {
|
||||
type TurnToRgbError = Infallible; // TODO
|
||||
|
||||
async fn turn_to_rgb(&mut self, color: Rgb) -> Result<(), Self::TurnToRgbError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
2
driver/kasa/src/lib.rs
Normal file
2
driver/kasa/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod connection;
|
||||
pub mod messages;
|
277
driver/kasa/src/messages.rs
Normal file
277
driver/kasa/src/messages.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use std::{collections::BTreeMap, fmt::Display, str::FromStr};
|
||||
|
||||
use deranged::{RangedU16, RangedU8};
|
||||
use mac_address::{MacAddress, MacParseError};
|
||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
|
||||
use serde_repr::Deserialize_repr;
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GetSysInfo;
|
||||
|
||||
impl Serialize for GetSysInfo {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let target = "system";
|
||||
let cmd = "get_sysinfo";
|
||||
let arg: Option<()> = None;
|
||||
|
||||
let mut top_level_map = serializer.serialize_map(Some(1))?;
|
||||
top_level_map.serialize_entry(target, &BTreeMap::from([(cmd, arg)]))?;
|
||||
top_level_map.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetSysInfoResponse {
|
||||
pub system: GetSysInfoResponseSystem,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetSysInfoResponseSystem {
|
||||
pub get_sysinfo: SysInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CommonSysInfo {
|
||||
active_mode: ActiveMode,
|
||||
alias: String,
|
||||
ctrl_protocols: CtrlProtocols,
|
||||
description: String,
|
||||
dev_state: DevState,
|
||||
#[serde(rename = "deviceId")]
|
||||
device_id: DeviceId,
|
||||
disco_ver: String,
|
||||
err_code: i32, // No idea
|
||||
heapsize: u64, // No idea
|
||||
#[serde(rename = "hwId")]
|
||||
hw_id: HardwareId,
|
||||
hw_ver: String,
|
||||
is_color: IsColor,
|
||||
is_dimmable: IsDimmable,
|
||||
is_factory: bool,
|
||||
is_variable_color_temp: IsVariableColorTemp,
|
||||
light_state: LightState,
|
||||
mic_mac: MacAddressWithoutSeparators,
|
||||
mic_type: MicType,
|
||||
// model: Model,
|
||||
#[serde(rename = "oemId")]
|
||||
oem_id: OemId,
|
||||
preferred_state: Vec<PreferredStateChoice>,
|
||||
rssi: i32,
|
||||
sw_ver: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LB130USSys {
|
||||
#[serde(flatten)]
|
||||
sys_info: CommonSysInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "model")]
|
||||
pub enum SysInfo {
|
||||
#[serde(rename = "LB130(US)")]
|
||||
LB130US(LB130USSys),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PreferredStateChoice {
|
||||
#[serde(flatten)]
|
||||
color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, SerializeDisplay, DeserializeFromStr)]
|
||||
struct MacAddressWithoutSeparators(MacAddress);
|
||||
|
||||
impl FromStr for MacAddressWithoutSeparators {
|
||||
type Err = MacParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let [a, b, c, d, e, f, g, h, i, j, k, l] = s
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| MacParseError::InvalidLength)?;
|
||||
|
||||
let bytes = [(a, b), (c, d), (e, f), (g, h), (i, j), (k, l)];
|
||||
|
||||
let mut digits = [0; 6];
|
||||
|
||||
for (i, (one, two)) in bytes.into_iter().enumerate() {
|
||||
let slice = [one, two];
|
||||
let as_string = std::str::from_utf8(&slice).map_err(|_| MacParseError::InvalidDigit)?;
|
||||
let number =
|
||||
u8::from_str_radix(as_string, 16).map_err(|_| MacParseError::InvalidDigit)?;
|
||||
digits[i] = number;
|
||||
}
|
||||
|
||||
Ok(MacAddressWithoutSeparators(MacAddress::new(digits)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MacAddressWithoutSeparators {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
enum ActiveMode {
|
||||
#[serde(rename = "none")]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CtrlProtocols {
|
||||
name: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DeviceId(pub String);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
enum DevState {
|
||||
#[serde(rename = "normal")]
|
||||
Normal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HardwareId(pub String);
|
||||
|
||||
#[derive(Debug, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
enum IsColor {
|
||||
NoColor = 0,
|
||||
Color = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
enum IsDimmable {
|
||||
NotDimmable = 0,
|
||||
Dimmable = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
enum IsVariableColorTemp {
|
||||
NoVariableColorTemp = 0,
|
||||
VariableColorTemp = 1,
|
||||
}
|
||||
|
||||
type Percentage = RangedU8<0, 100>;
|
||||
type Angle = RangedU16<0, 360>;
|
||||
type Kelvin = RangedU16<2500, 9000>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MaybeKelvin(Option<Kelvin>);
|
||||
|
||||
impl<'de> Deserialize<'de> for MaybeKelvin {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match u16::deserialize(deserializer)? {
|
||||
0 => Ok(MaybeKelvin(None)),
|
||||
value => {
|
||||
let kelvin = Kelvin::try_from(value).map_err(|e| {
|
||||
serde::de::Error::custom(format!(
|
||||
"{value} is not in the range {}..{}",
|
||||
Kelvin::MIN,
|
||||
Kelvin::MAX
|
||||
))
|
||||
})?;
|
||||
Ok(MaybeKelvin(Some(kelvin)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct RawColor {
|
||||
brightness: Percentage,
|
||||
color_temp: MaybeKelvin,
|
||||
hue: Angle,
|
||||
saturation: Percentage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Hsb {
|
||||
hue: Angle,
|
||||
saturation: Percentage,
|
||||
brightness: Percentage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct KelvinWithBrightness {
|
||||
kelvin: Kelvin,
|
||||
brightness: Percentage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Color {
|
||||
HSB(Hsb),
|
||||
KelvinWithBrightness(KelvinWithBrightness),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Color {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw_color = RawColor::deserialize(deserializer)?;
|
||||
|
||||
let RawColor {
|
||||
brightness,
|
||||
color_temp,
|
||||
hue,
|
||||
saturation,
|
||||
} = raw_color;
|
||||
|
||||
match color_temp.0 {
|
||||
Some(kelvin) => Ok(Color::KelvinWithBrightness(KelvinWithBrightness {
|
||||
kelvin,
|
||||
brightness,
|
||||
})),
|
||||
None => Ok(Color::HSB(Hsb {
|
||||
hue,
|
||||
saturation,
|
||||
brightness,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct LightState {
|
||||
#[serde(flatten)]
|
||||
color: Color,
|
||||
mode: LightStateMode,
|
||||
on_off: OnOrOff,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
enum LightStateMode {
|
||||
#[serde(rename = "normal")]
|
||||
Normal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
#[non_exhaustive]
|
||||
enum OnOrOff {
|
||||
Off = 0,
|
||||
On = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
enum MicType {
|
||||
#[serde(rename = "IOT.SMARTBULB")]
|
||||
IotSmartbulb,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct OemId(pub String);
|
9
emitter-and-signal/Cargo.toml
Normal file
9
emitter-and-signal/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "emitter-and-signal"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
deranged = { workspace = true }
|
||||
ext-trait = "2.0.0"
|
||||
tokio = { workspace = true, features = ["sync"] }
|
115
emitter-and-signal/src/emitter.rs
Normal file
115
emitter-and-signal/src/emitter.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::{future::Future, num::NonZero};
|
||||
|
||||
use deranged::RangedUsize;
|
||||
use tokio::{
|
||||
sync::{broadcast, mpsc},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use super::ProducerExited;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Publisher<T> {
|
||||
sender: broadcast::Sender<T>,
|
||||
}
|
||||
|
||||
impl<T> Publisher<T> {
|
||||
pub async fn all_unsubscribed(&self) {
|
||||
self.sender.closed().await
|
||||
}
|
||||
|
||||
pub fn publish(&self, event: T) {
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PublisherStream<T> {
|
||||
receiver: mpsc::Receiver<Publisher<T>>,
|
||||
}
|
||||
|
||||
impl<T> PublisherStream<T> {
|
||||
/// Returns `None` when no more subscriptions can ever be made
|
||||
pub async fn wait(&mut self) -> Option<Publisher<T>> {
|
||||
self.receiver.recv().await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Emitter<T> {
|
||||
sender: broadcast::Sender<T>,
|
||||
publisher_sender: mpsc::Sender<Publisher<T>>,
|
||||
}
|
||||
|
||||
pub type Capacity = RangedUsize<1, { usize::MAX / 2 }>;
|
||||
|
||||
impl<T> Emitter<T> {
|
||||
pub fn new<R, Fut>(
|
||||
producer: impl FnOnce(PublisherStream<T>) -> Fut,
|
||||
capacity: Capacity,
|
||||
) -> (Self, JoinHandle<R>)
|
||||
where
|
||||
Fut: Future<Output = R> + Send + 'static,
|
||||
T: Clone,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let (sender, _) = broadcast::channel(capacity.get());
|
||||
|
||||
let (publisher_sender, publisher_receiver) = mpsc::channel(1);
|
||||
|
||||
let publisher_stream = PublisherStream {
|
||||
receiver: publisher_receiver,
|
||||
};
|
||||
|
||||
let producer_join_handle = tokio::spawn(producer(publisher_stream));
|
||||
|
||||
(
|
||||
Self {
|
||||
publisher_sender,
|
||||
sender,
|
||||
},
|
||||
producer_join_handle,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn listen(&self) -> Result<Subscription<T>, ProducerExited> {
|
||||
let receiver = self.sender.subscribe();
|
||||
|
||||
if self.sender.receiver_count() == 1 {
|
||||
if let Err(mpsc::error::TrySendError::Closed(_)) =
|
||||
self.publisher_sender.try_send(Publisher {
|
||||
sender: self.sender.clone(),
|
||||
})
|
||||
{
|
||||
return Err(ProducerExited);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Subscription { receiver })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Subscription<T> {
|
||||
receiver: broadcast::Receiver<T>,
|
||||
}
|
||||
|
||||
pub enum NextError {
|
||||
ProducerExited(ProducerExited),
|
||||
Lagged { skipped_events: NonZero<u64> },
|
||||
}
|
||||
|
||||
impl<T> Subscription<T> {
|
||||
pub async fn next(&mut self) -> Result<T, NextError>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.receiver.recv().await.map_err(|err| match err {
|
||||
broadcast::error::RecvError::Closed => NextError::ProducerExited(ProducerExited),
|
||||
broadcast::error::RecvError::Lagged(skipped_events) => NextError::Lagged {
|
||||
skipped_events: skipped_events
|
||||
.try_into()
|
||||
.expect("lagging 0 events should be impossible"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
147
emitter-and-signal/src/emitter_ext.rs
Normal file
147
emitter-and-signal/src/emitter_ext.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use ext_trait::extension;
|
||||
use tokio::{select, task::JoinHandle};
|
||||
|
||||
use super::emitter::{Capacity, Emitter, NextError};
|
||||
|
||||
#[extension(pub trait EmitterExt)]
|
||||
impl<T> Emitter<T> {
|
||||
fn map<M, F>(self, mut func: F, capacity: Capacity) -> (Emitter<M>, JoinHandle<()>)
|
||||
where
|
||||
T: Send + 'static + Clone,
|
||||
M: Send + 'static + Clone,
|
||||
F: Send + 'static + FnMut(T) -> M,
|
||||
{
|
||||
Emitter::new(
|
||||
|mut publisher_stream| async move {
|
||||
while let Some(publisher) = publisher_stream.wait().await {
|
||||
let Ok(mut subscription) = self.listen() else {
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = publisher.all_unsubscribed() => {
|
||||
break;
|
||||
}
|
||||
event_res = subscription.next() => {
|
||||
match event_res {
|
||||
Ok(event) => publisher.publish(func(event)),
|
||||
Err(NextError::Lagged { .. }) => {},
|
||||
Err(NextError::ProducerExited(_)) => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
capacity,
|
||||
)
|
||||
}
|
||||
|
||||
fn filter<F>(self, mut func: F, capacity: Capacity) -> (Emitter<T>, JoinHandle<()>)
|
||||
where
|
||||
T: Send + 'static + Clone,
|
||||
F: Send + 'static + FnMut(&T) -> bool,
|
||||
{
|
||||
Emitter::new(
|
||||
|mut publisher_stream| async move {
|
||||
while let Some(publisher) = publisher_stream.wait().await {
|
||||
let Ok(mut subscription) = self.listen() else {
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = publisher.all_unsubscribed() => {
|
||||
break;
|
||||
}
|
||||
event_res = subscription.next() => {
|
||||
match event_res {
|
||||
Ok(event) => if func(&event) {
|
||||
publisher.publish(event)
|
||||
},
|
||||
Err(NextError::Lagged { .. }) => {},
|
||||
Err(NextError::ProducerExited(_)) => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
capacity,
|
||||
)
|
||||
}
|
||||
|
||||
fn filter_mut<F>(self, mut func: F, capacity: Capacity) -> (Emitter<T>, JoinHandle<()>)
|
||||
where
|
||||
T: Send + 'static + Clone,
|
||||
F: Send + 'static + FnMut(&mut T) -> bool,
|
||||
{
|
||||
Emitter::new(
|
||||
|mut publisher_stream| async move {
|
||||
while let Some(publisher) = publisher_stream.wait().await {
|
||||
let Ok(mut subscription) = self.listen() else {
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = publisher.all_unsubscribed() => {
|
||||
break;
|
||||
}
|
||||
event_res = subscription.next() => {
|
||||
match event_res {
|
||||
Ok(mut event) => if func(&mut event) {
|
||||
publisher.publish(event)
|
||||
},
|
||||
Err(NextError::Lagged { .. }) => {},
|
||||
Err(NextError::ProducerExited(_)) => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
capacity,
|
||||
)
|
||||
}
|
||||
|
||||
fn filter_map<M, F>(self, mut func: F, capacity: Capacity) -> (Emitter<M>, JoinHandle<()>)
|
||||
where
|
||||
T: Send + 'static + Clone,
|
||||
M: Send + 'static + Clone,
|
||||
F: Send + 'static + FnMut(T) -> Option<M>,
|
||||
{
|
||||
Emitter::new(
|
||||
|mut publisher_stream| async move {
|
||||
while let Some(publisher) = publisher_stream.wait().await {
|
||||
let Ok(mut subscription) = self.listen() else {
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = publisher.all_unsubscribed() => {
|
||||
break;
|
||||
}
|
||||
event_res = subscription.next() => {
|
||||
match event_res {
|
||||
Ok(event) => if let Some(mapped) = func(event) {
|
||||
publisher.publish(mapped)
|
||||
},
|
||||
Err(NextError::Lagged { .. }) => {},
|
||||
Err(NextError::ProducerExited(_)) => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
capacity,
|
||||
)
|
||||
}
|
||||
}
|
10
emitter-and-signal/src/lib.rs
Normal file
10
emitter-and-signal/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
pub mod emitter;
|
||||
mod emitter_ext;
|
||||
pub mod signal;
|
||||
mod signal_ext;
|
||||
|
||||
pub use emitter_ext::EmitterExt;
|
||||
pub use signal_ext::SignalExt;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ProducerExited;
|
@@ -1,20 +1,7 @@
|
||||
use std::future::Future;
|
||||
|
||||
use tokio::{
|
||||
sync::{mpsc, watch},
|
||||
task::{JoinError, JoinHandle},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PublisherStream<T> {
|
||||
receiver: mpsc::Receiver<Publisher<T>>,
|
||||
}
|
||||
|
||||
impl<T> PublisherStream<T> {
|
||||
pub async fn wait(&mut self) -> Option<Publisher<T>> {
|
||||
self.receiver.recv().await
|
||||
}
|
||||
}
|
||||
use tokio::sync::{mpsc, watch};
|
||||
pub use tokio::task::JoinError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Publisher<T> {
|
||||
@@ -29,58 +16,71 @@ impl<T> Publisher<T> {
|
||||
pub fn publish(&self, value: T) {
|
||||
self.sender.send_replace(value);
|
||||
}
|
||||
|
||||
pub fn publish_with<F: FnOnce(&mut T) -> bool>(&self, maybe_modify: F) {
|
||||
self.sender.send_if_modified(maybe_modify);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Store<T> {
|
||||
sender: watch::Sender<T>,
|
||||
publisher_sender: mpsc::Sender<Publisher<T>>,
|
||||
producer_join_handle: JoinHandle<()>,
|
||||
pub struct PublisherStream<T> {
|
||||
receiver: mpsc::Receiver<Publisher<T>>,
|
||||
}
|
||||
|
||||
impl<T> Store<T> {
|
||||
pub fn new<Fut: Future<Output = ()> + Send + 'static>(
|
||||
impl<T> PublisherStream<T> {
|
||||
/// Returns `None` when no more subscriptions can ever be made
|
||||
pub async fn wait(&mut self) -> Option<Publisher<T>> {
|
||||
self.receiver.recv().await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Signal<T> {
|
||||
sender: watch::Sender<T>,
|
||||
publisher_sender: mpsc::Sender<Publisher<T>>,
|
||||
}
|
||||
|
||||
impl<T> Signal<T> {
|
||||
pub fn new<R, Fut: Future<Output = R> + Send + 'static>(
|
||||
initial: T,
|
||||
producer: impl FnOnce(PublisherStream<T>) -> Fut,
|
||||
) -> Self {
|
||||
) -> (Self, impl Future<Output = Result<R, JoinError>>)
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
let (sender, _) = watch::channel(initial);
|
||||
let (publisher_sender, publisher_receiver) = mpsc::channel(1);
|
||||
|
||||
let subscribers_stream = PublisherStream {
|
||||
let publisher_stream = PublisherStream {
|
||||
receiver: publisher_receiver,
|
||||
};
|
||||
|
||||
let producer_join_handle = tokio::spawn(producer(subscribers_stream));
|
||||
let producer_join_handle = tokio::spawn(producer(publisher_stream));
|
||||
|
||||
Self {
|
||||
publisher_sender,
|
||||
sender,
|
||||
(
|
||||
Self {
|
||||
publisher_sender,
|
||||
sender,
|
||||
},
|
||||
producer_join_handle,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> Result<Subscription<T>, ProducerExited> {
|
||||
let receiver = self.sender.subscribe();
|
||||
|
||||
if self.sender.receiver_count() == 1 {
|
||||
if let Err(e) = self.publisher_sender.try_send(Publisher {
|
||||
sender: self.sender.clone(),
|
||||
}) {
|
||||
match e {
|
||||
mpsc::error::TrySendError::Full(_) => unreachable!(),
|
||||
mpsc::error::TrySendError::Closed(_) => return Err(ProducerExited),
|
||||
}
|
||||
if let Err(mpsc::error::TrySendError::Closed(_)) =
|
||||
self.publisher_sender.try_send(Publisher {
|
||||
sender: self.sender.clone(),
|
||||
})
|
||||
{
|
||||
return Err(ProducerExited);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Subscription { receiver })
|
||||
}
|
||||
|
||||
/// Signify that no one can ever subscribe again,
|
||||
/// and wait for the producer task to complete.
|
||||
pub fn run(self) -> impl Future<Output = Result<(), JoinError>> {
|
||||
self.producer_join_handle
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Subscription<T> {
|
6
emitter-and-signal/src/signal_ext.rs
Normal file
6
emitter-and-signal/src/signal_ext.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use ext_trait::extension;
|
||||
|
||||
use super::signal::Signal;
|
||||
|
||||
#[extension(pub trait SignalExt)]
|
||||
impl<T> Signal<T> {}
|
42
entrypoint/Cargo.toml
Normal file
42
entrypoint/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "smart-home-in-rust-with-home-assistant"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
name = "smart_home_in_rust_with_home_assistant"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
arbitrary-value = { path = "../arbitrary-value", features = ["pyo3"] }
|
||||
arc-swap = "1.7.1"
|
||||
async-gate = "0.4.0"
|
||||
axum = { version = "0.8.1", default-features = false, features = [
|
||||
"http1",
|
||||
"tokio",
|
||||
] }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
deranged = { workspace = true, features = ["serde"] }
|
||||
driver-kasa = { path = "../driver/kasa" }
|
||||
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||
home-assistant = { path = "../home-assistant" }
|
||||
im = { version = "15.1.0", features = ["rayon"] }
|
||||
protocol = { path = "../protocol" }
|
||||
pyo3 = { workspace = true, features = [
|
||||
"auto-initialize",
|
||||
"chrono",
|
||||
"extension-module",
|
||||
] }
|
||||
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
|
||||
shadow-rs = { version = "1.0.1", default-features = false }
|
||||
snafu = { workspace = true }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = "0.3.17"
|
||||
uom = "0.36.0"
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = "1.0.1"
|
84
entrypoint/src/lib.rs
Normal file
84
entrypoint/src/lib.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use driver_kasa::connection::LB130USHandle;
|
||||
use home_assistant::{
|
||||
home_assistant::HomeAssistant, light::HomeAssistantLight, object_id::ObjectId,
|
||||
};
|
||||
use protocol::light::Light;
|
||||
use pyo3::prelude::*;
|
||||
use shadow_rs::shadow;
|
||||
use tokio::time::interval;
|
||||
use tracing::{level_filters::LevelFilter, Level};
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, format::FmtSpan},
|
||||
layer::SubscriberExt,
|
||||
registry,
|
||||
util::SubscriberInitExt,
|
||||
Layer,
|
||||
};
|
||||
use tracing_to_home_assistant::TracingToHomeAssistant;
|
||||
|
||||
mod tracing_to_home_assistant;
|
||||
|
||||
shadow!(build_info);
|
||||
|
||||
async fn real_main(home_assistant: HomeAssistant) -> ! {
|
||||
registry()
|
||||
.with(
|
||||
fmt::layer()
|
||||
.pretty()
|
||||
.with_span_events(FmtSpan::ACTIVE)
|
||||
.with_filter(LevelFilter::from_level(Level::TRACE)),
|
||||
)
|
||||
.with(TracingToHomeAssistant)
|
||||
.init();
|
||||
|
||||
let built_at = build_info::BUILD_TIME;
|
||||
tracing::info!(built_at);
|
||||
|
||||
// let lamp = HomeAssistantLight {
|
||||
// home_assistant,
|
||||
// object_id: ObjectId::from_str("jacob_s_lamp_top").unwrap(),
|
||||
// };
|
||||
|
||||
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 mut interval = interval(Duration::from_secs(20));
|
||||
interval.tick().await;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
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 = lamp.is_on().await;
|
||||
// tracing::info!(?is_on);
|
||||
// let is_off = lamp.is_off().await;
|
||||
// tracing::info!(?is_off);
|
||||
|
||||
// let something = lamp.turn_on().await;
|
||||
// tracing::info!(?something);
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, 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(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_function(wrap_pyfunction!(main, module)?)?;
|
||||
Ok(())
|
||||
}
|
@@ -7,7 +7,7 @@ use tracing::{
|
||||
};
|
||||
use tracing_subscriber::{layer::Context, Layer};
|
||||
|
||||
use crate::home_assistant::logger::{HassLogger, LogData};
|
||||
use home_assistant::logger::{HassLogger, LogData};
|
||||
|
||||
pub struct TracingToHomeAssistant;
|
||||
|
32
home-assistant/Cargo.toml
Normal file
32
home-assistant/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "home-assistant"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[dependencies]
|
||||
arbitrary-value = { path = "../arbitrary-value" }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
derive_more = { workspace = true, features = [
|
||||
"display",
|
||||
"from",
|
||||
"from_str",
|
||||
"into",
|
||||
"try_from",
|
||||
"try_into",
|
||||
] }
|
||||
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||
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 = { version = "0.27.1", features = ["derive"] }
|
||||
tokio = { workspace = true }
|
||||
tracing = { optional = true, workspace = true }
|
||||
ulid = "1.2.0"
|
@@ -59,7 +59,7 @@ impl<'py> FromPyObject<'py> for EntityId {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &EntityId {
|
||||
impl<'py> IntoPyObject<'py> for EntityId {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
39
home-assistant/src/event/context/context.rs
Normal file
39
home-assistant/src/event/context/context.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::id::Id;
|
||||
use once_cell::sync::OnceCell;
|
||||
use pyo3::{prelude::*, types::PyType};
|
||||
|
||||
/// The context that triggered something.
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct Context<Event> {
|
||||
pub id: Id,
|
||||
pub user_id: Option<String>,
|
||||
pub parent_id: Option<String>,
|
||||
/// In order to prevent cycles, the user must decide to pass [`Py<PyAny>`] for the `Event` type here
|
||||
/// or for the `Context` type in [`Event`]
|
||||
pub origin_event: Event,
|
||||
}
|
||||
|
||||
impl<'py, Event: IntoPyObject<'py>> IntoPyObject<'py> for Context<Event> {
|
||||
type Target = PyAny;
|
||||
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
|
||||
type Error = PyErr;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
static HOMEASSISTANT_CORE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||
|
||||
let homeassistant_core = HOMEASSISTANT_CORE
|
||||
.get_or_try_init(|| Result::<_, PyErr>::Ok(py.import("homeassistant.core")?.unbind()))?
|
||||
.bind(py);
|
||||
|
||||
let context_class = homeassistant_core.getattr("Context")?;
|
||||
let context_class = context_class.downcast_into::<PyType>()?;
|
||||
|
||||
let context_instance = context_class.call1((self.user_id, self.parent_id, self.id))?;
|
||||
|
||||
context_instance.setattr("origin_event", self.origin_event)?;
|
||||
|
||||
Ok(context_instance)
|
||||
}
|
||||
}
|
38
home-assistant/src/event/context/id.rs
Normal file
38
home-assistant/src/event/context/id.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::{prelude::*, types::PyString};
|
||||
use smol_str::SmolStr;
|
||||
use ulid::Ulid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Id {
|
||||
Ulid(Ulid),
|
||||
Other(SmolStr),
|
||||
}
|
||||
|
||||
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.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Id {
|
||||
type Target = PyString;
|
||||
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self {
|
||||
Id::Ulid(ulid) => ulid.to_string().into_pyobject(py),
|
||||
Id::Other(id) => id.as_str().into_pyobject(py),
|
||||
}
|
||||
}
|
||||
}
|
58
home-assistant/src/event/specific/state_changed.rs
Normal file
58
home-assistant/src/event/specific/state_changed.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
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>()?;
|
||||
|
||||
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<
|
||||
OldState,
|
||||
OldAttributes,
|
||||
OldStateContextEvent,
|
||||
NewState,
|
||||
NewAttributes,
|
||||
NewStateContextEvent,
|
||||
> {
|
||||
pub entity_id: EntityId,
|
||||
pub old_state: Option<StateObject<OldState, OldAttributes, OldStateContextEvent>>,
|
||||
pub new_state: Option<StateObject<NewState, NewAttributes, NewStateContextEvent>>,
|
||||
}
|
||||
|
||||
/// A state changed event is fired when on state write the state is changed.
|
||||
pub type Event<
|
||||
OldState,
|
||||
OldAttributes,
|
||||
OldStateContextEvent,
|
||||
NewState,
|
||||
NewAttributes,
|
||||
NewStateContextEvent,
|
||||
Context,
|
||||
> = super::super::event::Event<
|
||||
Type,
|
||||
Data<
|
||||
OldState,
|
||||
OldAttributes,
|
||||
OldStateContextEvent,
|
||||
NewState,
|
||||
NewAttributes,
|
||||
NewStateContextEvent,
|
||||
>,
|
||||
Context,
|
||||
>;
|
@@ -2,9 +2,9 @@ use std::convert::Infallible;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use crate::python_utils::{detach, validate_type_by_name};
|
||||
use python_utils::{detach, validate_type_by_name};
|
||||
|
||||
use super::state_machine::StateMachine;
|
||||
use super::{service_registry::ServiceRegistry, state_machine::StateMachine};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HomeAssistant(Py<PyAny>);
|
||||
@@ -52,4 +52,9 @@ impl HomeAssistant {
|
||||
let states = self.0.getattr(py, "states")?;
|
||||
states.extract(py)
|
||||
}
|
||||
|
||||
pub fn services(&self, py: Python<'_>) -> Result<ServiceRegistry, PyErr> {
|
||||
let services = self.0.getattr(py, "services")?;
|
||||
services.extract(py)
|
||||
}
|
||||
}
|
@@ -2,7 +2,12 @@ pub mod domain;
|
||||
pub mod entity_id;
|
||||
pub mod event;
|
||||
pub mod home_assistant;
|
||||
pub mod light;
|
||||
pub mod logger;
|
||||
pub mod object_id;
|
||||
pub mod service;
|
||||
pub mod service_registry;
|
||||
pub mod slug;
|
||||
pub mod state;
|
||||
pub mod state_machine;
|
||||
pub mod state_object;
|
8
home-assistant/src/light/attributes.rs
Normal file
8
home-assistant/src/light/attributes.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
#[pyo3(from_item_all)]
|
||||
pub struct LightAttributes {
|
||||
min_color_temp_kelvin: Option<u16>, // TODO: only here to allow compilation!
|
||||
max_color_temp_kelvin: Option<u16>, // TODO: only here to allow compilation!
|
||||
}
|
54
home-assistant/src/light/mod.rs
Normal file
54
home-assistant/src/light/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use attributes::LightAttributes;
|
||||
use pyo3::prelude::*;
|
||||
use snafu::{ResultExt, Snafu};
|
||||
use state::LightState;
|
||||
|
||||
use crate::state::HomeAssistantState;
|
||||
|
||||
use super::{
|
||||
domain::Domain, entity_id::EntityId, home_assistant::HomeAssistant, object_id::ObjectId,
|
||||
state_object::StateObject,
|
||||
};
|
||||
|
||||
mod attributes;
|
||||
mod protocol;
|
||||
mod service;
|
||||
mod state;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HomeAssistantLight {
|
||||
pub home_assistant: HomeAssistant,
|
||||
pub object_id: ObjectId,
|
||||
}
|
||||
|
||||
impl HomeAssistantLight {
|
||||
fn entity_id(&self) -> EntityId {
|
||||
EntityId(Domain::Light, self.object_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub enum GetStateObjectError {
|
||||
PythonError { source: PyErr },
|
||||
EntityMissing,
|
||||
}
|
||||
|
||||
impl HomeAssistantLight {
|
||||
fn get_state_object(
|
||||
&self,
|
||||
) -> Result<
|
||||
StateObject<HomeAssistantState<LightState>, LightAttributes, Py<PyAny>>,
|
||||
GetStateObjectError,
|
||||
> {
|
||||
Python::with_gil(|py| {
|
||||
let states = self.home_assistant.states(py).context(PythonSnafu)?;
|
||||
let entity_id = self.entity_id();
|
||||
let state_object = states
|
||||
.get(py, entity_id)
|
||||
.context(PythonSnafu)?
|
||||
.ok_or(GetStateObjectError::EntityMissing)?;
|
||||
|
||||
Ok(state_object)
|
||||
})
|
||||
}
|
||||
}
|
108
home-assistant/src/light/protocol.rs
Normal file
108
home-assistant/src/light/protocol.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use super::service::{turn_off::TurnOff, turn_on::TurnOn};
|
||||
use super::{state::LightState, GetStateObjectError, HomeAssistantLight};
|
||||
use crate::{
|
||||
event::context::context::Context,
|
||||
state::{ErrorState, HomeAssistantState, UnexpectedState},
|
||||
};
|
||||
use arbitrary_value::arbitrary::Arbitrary;
|
||||
use protocol::light::Light;
|
||||
use pyo3::prelude::*;
|
||||
use snafu::{ResultExt, Snafu};
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub enum IsStateError {
|
||||
GetStateObjectError { source: GetStateObjectError },
|
||||
Error { state: ErrorState },
|
||||
UnexpectedError { state: UnexpectedState },
|
||||
}
|
||||
|
||||
impl Light for HomeAssistantLight {
|
||||
type IsOnError = IsStateError;
|
||||
|
||||
async fn is_on(&self) -> Result<bool, Self::IsOnError> {
|
||||
let state_object = self.get_state_object().context(GetStateObjectSnafu)?;
|
||||
let state = state_object.state;
|
||||
|
||||
match state {
|
||||
HomeAssistantState::Ok(light_state) => Ok(matches!(light_state, LightState::On)),
|
||||
HomeAssistantState::Err(state) => Err(IsStateError::Error { state }),
|
||||
HomeAssistantState::UnexpectedErr(state) => {
|
||||
Err(IsStateError::UnexpectedError { state })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type IsOffError = IsStateError;
|
||||
|
||||
async fn is_off(&self) -> Result<bool, Self::IsOffError> {
|
||||
let state_object = self.get_state_object().context(GetStateObjectSnafu)?;
|
||||
let state = state_object.state;
|
||||
|
||||
match state {
|
||||
HomeAssistantState::Ok(light_state) => Ok(matches!(light_state, LightState::Off)),
|
||||
HomeAssistantState::Err(state) => Err(IsStateError::Error { state }),
|
||||
HomeAssistantState::UnexpectedErr(state) => {
|
||||
Err(IsStateError::UnexpectedError { state })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TurnOnError = PyErr;
|
||||
|
||||
async fn turn_on(&mut self) -> Result<(), Self::TurnOnError> {
|
||||
let context: Option<Context<()>> = None;
|
||||
let target: Option<()> = None;
|
||||
|
||||
let services = Python::with_gil(|py| self.home_assistant.services(py))?;
|
||||
// TODO
|
||||
let service_response: Arbitrary = services
|
||||
.call_service(
|
||||
TurnOn {
|
||||
entity_id: self.entity_id(),
|
||||
},
|
||||
context,
|
||||
target,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!(?service_response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type TurnOffError = PyErr;
|
||||
|
||||
async fn turn_off(&mut self) -> Result<(), Self::TurnOffError> {
|
||||
let context: Option<Context<()>> = None;
|
||||
let target: Option<()> = None;
|
||||
|
||||
let services = Python::with_gil(|py| self.home_assistant.services(py))?;
|
||||
// TODO
|
||||
let service_response: Arbitrary // TODO: a type that validates as None
|
||||
= services
|
||||
.call_service(
|
||||
TurnOff {
|
||||
entity_id: self.entity_id(),
|
||||
},
|
||||
context,
|
||||
target,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!(?service_response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type ToggleError = PyErr;
|
||||
|
||||
async fn toggle(&mut self) -> Result<(), Self::ToggleError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
2
home-assistant/src/light/service/mod.rs
Normal file
2
home-assistant/src/light/service/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod turn_off;
|
||||
pub mod turn_on;
|
33
home-assistant/src/light/service/turn_off.rs
Normal file
33
home-assistant/src/light/service/turn_off.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::IntoPyObject;
|
||||
|
||||
use crate::{
|
||||
entity_id::EntityId,
|
||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TurnOff {
|
||||
pub entity_id: EntityId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoPyObject)]
|
||||
pub struct TurnOffServiceData {
|
||||
entity_id: EntityId,
|
||||
}
|
||||
|
||||
impl IntoServiceCall for TurnOff {
|
||||
type ServiceData = TurnOffServiceData;
|
||||
|
||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||
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 service_data = TurnOffServiceData { entity_id };
|
||||
|
||||
(service_domain, service_id, service_data)
|
||||
}
|
||||
}
|
32
home-assistant/src/light/service/turn_on.rs
Normal file
32
home-assistant/src/light/service/turn_on.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::IntoPyObject;
|
||||
|
||||
use crate::{
|
||||
entity_id::EntityId,
|
||||
service::{service_domain::ServiceDomain, service_id::ServiceId, IntoServiceCall},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TurnOn {
|
||||
pub entity_id: EntityId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoPyObject)]
|
||||
pub struct TurnOnServiceData {
|
||||
entity_id: EntityId,
|
||||
}
|
||||
|
||||
impl IntoServiceCall for TurnOn {
|
||||
type ServiceData = TurnOnServiceData;
|
||||
|
||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData) {
|
||||
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 service_data = TurnOnServiceData { entity_id };
|
||||
|
||||
(service_domain, service_id, service_data)
|
||||
}
|
||||
}
|
22
home-assistant/src/light/state.rs
Normal file
22
home-assistant/src/light/state.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::{exceptions::PyValueError, prelude::*};
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(Debug, Clone, EnumString, strum::Display)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum LightState {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
impl<'py> FromPyObject<'py> for LightState {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let s = ob.extract::<String>()?;
|
||||
|
||||
let state =
|
||||
LightState::from_str(&s).map_err(|err| PyValueError::new_err(err.to_string()))?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
@@ -1,9 +1,7 @@
|
||||
use arbitrary_value::{arbitrary::Arbitrary, map::Map};
|
||||
use once_cell::sync::OnceCell;
|
||||
use pyo3::{prelude::*, types::PyTuple};
|
||||
|
||||
use crate::{
|
||||
arbitrary::{arbitrary::Arbitrary, map::Map},
|
||||
python_utils::{detach, validate_type_by_name},
|
||||
};
|
||||
use python_utils::{detach, validate_type_by_name};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HassLogger(Py<PyAny>);
|
||||
@@ -55,8 +53,12 @@ pub struct LogData<ExcInfo> {
|
||||
|
||||
impl HassLogger {
|
||||
pub fn new(py: Python<'_>, name: &str) -> PyResult<Self> {
|
||||
let logging = py.import("logging")?;
|
||||
let logger = logging.call_method1("getLogger", (name,))?;
|
||||
static LOGGING_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||
|
||||
let logging_module = LOGGING_MODULE
|
||||
.get_or_try_init(|| Result::<_, PyErr>::Ok(py.import("logging")?.unbind()))?
|
||||
.bind(py);
|
||||
let logger = logging_module.call_method1("getLogger", (name,))?;
|
||||
|
||||
Ok(logger.extract()?)
|
||||
}
|
21
home-assistant/src/object_id.rs
Normal file
21
home-assistant/src/object_id.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::{prelude::*, types::PyString};
|
||||
|
||||
use super::slug::Slug;
|
||||
|
||||
pub use super::slug::SlugParsingError as ObjectIdParsingError;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
||||
pub struct ObjectId(pub Slug);
|
||||
|
||||
impl<'py> IntoPyObject<'py> for ObjectId {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let s = self.to_string();
|
||||
s.into_pyobject(py)
|
||||
}
|
||||
}
|
11
home-assistant/src/service/mod.rs
Normal file
11
home-assistant/src/service/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use service_domain::ServiceDomain;
|
||||
use service_id::ServiceId;
|
||||
|
||||
pub mod service_domain;
|
||||
pub mod service_id;
|
||||
|
||||
pub trait IntoServiceCall {
|
||||
type ServiceData;
|
||||
|
||||
fn into_service_call(self) -> (ServiceDomain, ServiceId, Self::ServiceData);
|
||||
}
|
21
home-assistant/src/service/service_domain.rs
Normal file
21
home-assistant/src/service/service_domain.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::{prelude::*, types::PyString};
|
||||
|
||||
use super::super::slug::Slug;
|
||||
|
||||
pub use super::super::slug::SlugParsingError as ServiceDomainParsingError;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
||||
pub struct ServiceDomain(pub Slug);
|
||||
|
||||
impl<'py> IntoPyObject<'py> for ServiceDomain {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let s = self.to_string();
|
||||
s.into_pyobject(py)
|
||||
}
|
||||
}
|
21
home-assistant/src/service/service_id.rs
Normal file
21
home-assistant/src/service/service_id.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use pyo3::{prelude::*, types::PyString};
|
||||
|
||||
use super::super::slug::Slug;
|
||||
|
||||
pub use super::super::slug::SlugParsingError as ServiceIdParsingError;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display, derive_more::FromStr)]
|
||||
pub struct ServiceId(pub Slug);
|
||||
|
||||
impl<'py> IntoPyObject<'py> for ServiceId {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let s = self.to_string();
|
||||
s.into_pyobject(py)
|
||||
}
|
||||
}
|
54
home-assistant/src/service_registry.rs
Normal file
54
home-assistant/src/service_registry.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use super::{event::context::context::Context, service::IntoServiceCall};
|
||||
use pyo3::prelude::*;
|
||||
use python_utils::{detach, validate_type_by_name};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceRegistry(Py<PyAny>);
|
||||
|
||||
impl<'py> FromPyObject<'py> for ServiceRegistry {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
// region: Validation
|
||||
validate_type_by_name(ob, "ServiceRegistry")?;
|
||||
// endregion: Validation
|
||||
|
||||
Ok(Self(detach(ob)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceRegistry {
|
||||
pub async fn call_service<
|
||||
ServiceData: for<'py> IntoPyObject<'py>,
|
||||
Target: for<'py> IntoPyObject<'py>,
|
||||
Event: for<'py> IntoPyObject<'py>,
|
||||
ServiceResponse: for<'py> FromPyObject<'py>,
|
||||
>(
|
||||
&self,
|
||||
service_call: impl IntoServiceCall<ServiceData = ServiceData>,
|
||||
context: Option<Context<Event>>,
|
||||
target: Option<Target>,
|
||||
return_response: bool,
|
||||
) -> PyResult<ServiceResponse> {
|
||||
let (domain, service, service_data) = service_call.into_service_call();
|
||||
|
||||
let blocking = true;
|
||||
|
||||
let args = (
|
||||
domain,
|
||||
service,
|
||||
service_data,
|
||||
blocking,
|
||||
context,
|
||||
target,
|
||||
return_response,
|
||||
);
|
||||
|
||||
let future = Python::with_gil::<_, 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)
|
||||
})?;
|
||||
|
||||
let service_response = future.await?;
|
||||
Python::with_gil(|py| service_response.extract(py))
|
||||
}
|
||||
}
|
@@ -1,19 +1,26 @@
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use std::str::FromStr;
|
||||
|
||||
use pyo3::{exceptions::PyValueError, PyErr};
|
||||
use smol_str::SmolStr;
|
||||
use snafu::Snafu;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display)]
|
||||
pub struct ObjectId(Arc<str>);
|
||||
pub struct Slug(SmolStr);
|
||||
|
||||
#[derive(Debug, Clone, Snafu)]
|
||||
#[snafu(display("expected a lowercase ASCII alphabetical character (i.e. a through z) or a digit (i.e. 0 through 9) or an underscore (i.e. _) but encountered {encountered}"))]
|
||||
pub struct ObjectIdParsingError {
|
||||
pub struct SlugParsingError {
|
||||
encountered: char,
|
||||
}
|
||||
|
||||
impl FromStr for ObjectId {
|
||||
type Err = ObjectIdParsingError;
|
||||
impl From<SlugParsingError> for PyErr {
|
||||
fn from(error: SlugParsingError) -> Self {
|
||||
PyValueError::new_err(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Slug {
|
||||
type Err = SlugParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
for c in s.chars() {
|
||||
@@ -21,16 +28,10 @@ impl FromStr for ObjectId {
|
||||
'a'..='z' => {}
|
||||
'0'..='9' => {}
|
||||
'_' => {}
|
||||
_ => return Err(ObjectIdParsingError { encountered: c }),
|
||||
_ => return Err(SlugParsingError { encountered: c }),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(s.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectIdParsingError> for PyErr {
|
||||
fn from(error: ObjectIdParsingError) -> Self {
|
||||
PyValueError::new_err(error.to_string())
|
||||
}
|
||||
}
|
71
home-assistant/src/state.rs
Normal file
71
home-assistant/src/state.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
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)
|
||||
}
|
||||
}
|
@@ -1,11 +1,7 @@
|
||||
use super::entity_id::EntityId;
|
||||
use super::state_object::StateObject;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use crate::{
|
||||
home_assistant::entity_id::EntityId,
|
||||
python_utils::{detach, validate_type_by_name},
|
||||
};
|
||||
|
||||
use super::state::State;
|
||||
use python_utils::{detach, validate_type_by_name};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StateMachine(Py<PyAny>);
|
||||
@@ -21,11 +17,16 @@ impl<'py> FromPyObject<'py> for StateMachine {
|
||||
}
|
||||
|
||||
impl StateMachine {
|
||||
pub fn get<Attributes: for<'py> FromPyObject<'py>, ContextEvent: for<'py> FromPyObject<'py>>(
|
||||
pub fn get<
|
||||
'py,
|
||||
State: FromPyObject<'py>,
|
||||
Attributes: FromPyObject<'py>,
|
||||
ContextEvent: FromPyObject<'py>,
|
||||
>(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
py: Python<'py>,
|
||||
entity_id: EntityId,
|
||||
) -> PyResult<Option<State<Attributes, ContextEvent>>> {
|
||||
) -> PyResult<Option<StateObject<State, Attributes, ContextEvent>>> {
|
||||
let args = (entity_id.to_string(),);
|
||||
let state = self.0.call_method1(py, "get", args)?;
|
||||
state.extract(py)
|
151
home-assistant/src/state_object.rs
Normal file
151
home-assistant/src/state_object.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use super::{
|
||||
event::{context::context::Context, specific::state_changed},
|
||||
home_assistant::HomeAssistant,
|
||||
};
|
||||
use crate::entity_id::EntityId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use emitter_and_signal::signal::Signal;
|
||||
use once_cell::sync::OnceCell;
|
||||
use pyo3::{
|
||||
prelude::*,
|
||||
types::{PyCFunction, PyDict, PyTuple},
|
||||
};
|
||||
use std::{future::Future, sync::Arc};
|
||||
use tokio::{select, sync::mpsc};
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct StateObject<State, Attributes, ContextEvent> {
|
||||
pub entity_id: EntityId,
|
||||
pub state: State,
|
||||
pub attributes: Attributes,
|
||||
pub last_changed: Option<DateTime<Utc>>,
|
||||
pub last_reported: Option<DateTime<Utc>>,
|
||||
pub last_updated: Option<DateTime<Utc>>,
|
||||
pub context: Context<ContextEvent>,
|
||||
}
|
||||
|
||||
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>,
|
||||
> StateObject<State, Attributes, ContextEvent>
|
||||
{
|
||||
pub fn store(
|
||||
py: Python<'_>,
|
||||
home_assistant: &HomeAssistant,
|
||||
entity_id: EntityId,
|
||||
) -> PyResult<(
|
||||
Signal<Option<Arc<Self>>>,
|
||||
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())?;
|
||||
|
||||
let py_home_assistant = home_assistant.into_pyobject(py)?.unbind();
|
||||
|
||||
let (store, 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| {
|
||||
static EVENT_MODULE: OnceCell<Py<PyModule>> = OnceCell::new();
|
||||
|
||||
let event_module = EVENT_MODULE
|
||||
.get_or_try_init(|| {
|
||||
Result::<_, PyErr>::Ok(
|
||||
py.import("homeassistant.helpers.event")?.unbind(),
|
||||
)
|
||||
})?
|
||||
.bind(py);
|
||||
|
||||
let untrack = {
|
||||
let callback =
|
||||
move |args: &Bound<'_, PyTuple>,
|
||||
_kwargs: Option<&Bound<'_, PyDict>>| {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("calling the closure");
|
||||
|
||||
if let Ok((event,)) = args.extract::<(
|
||||
state_changed::Event<
|
||||
State,
|
||||
Attributes,
|
||||
ContextEvent,
|
||||
State,
|
||||
Attributes,
|
||||
ContextEvent,
|
||||
Py<PyAny>,
|
||||
>,
|
||||
)>() {
|
||||
let new_state = event.data.new_state;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("sending a new state"); // TODO: remove
|
||||
new_state_sender.try_send(new_state).unwrap();
|
||||
}
|
||||
};
|
||||
let callback = PyCFunction::new_closure(py, None, None, callback)?;
|
||||
let args = (
|
||||
py_home_assistant.clone_ref(py),
|
||||
vec![entity_id.clone()],
|
||||
callback,
|
||||
);
|
||||
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...");
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = publisher.all_unsubscribed() => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("calling untrack");
|
||||
let res = Python::with_gil(|py| untrack.call0(py));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?res);
|
||||
break;
|
||||
}
|
||||
new_state = new_state_receiver.recv() => {
|
||||
match new_state {
|
||||
Some(new_state) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("publishing new state");
|
||||
publisher.publish(new_state.map(Arc::new))
|
||||
},
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("channel dropped");
|
||||
break
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("untrack is err");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok((store, task))
|
||||
}
|
||||
}
|
8
protocol/Cargo.toml
Normal file
8
protocol/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "protocol"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
deranged = { workspace = true }
|
||||
derive_more = { workspace = true }
|
1
protocol/src/lib.rs
Normal file
1
protocol/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod light;
|
43
protocol/src/light.rs
Normal file
43
protocol/src/light.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::{error::Error, future::Future};
|
||||
|
||||
use deranged::RangedU16;
|
||||
|
||||
pub trait Light {
|
||||
type IsOnError: Error;
|
||||
fn is_on(&self) -> impl Future<Output = Result<bool, Self::IsOnError>> + Send;
|
||||
|
||||
type IsOffError: Error;
|
||||
fn is_off(&self) -> impl Future<Output = Result<bool, Self::IsOffError>> + Send;
|
||||
|
||||
type TurnOnError: Error;
|
||||
fn turn_on(&mut self) -> impl Future<Output = Result<(), Self::TurnOnError>> + Send;
|
||||
|
||||
type TurnOffError: Error;
|
||||
fn turn_off(&mut self) -> impl Future<Output = Result<(), Self::TurnOffError>> + Send;
|
||||
|
||||
type ToggleError: Error;
|
||||
fn toggle(&mut self) -> impl Future<Output = Result<(), Self::ToggleError>> + Send;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, derive_more::From, derive_more::Into)]
|
||||
pub struct Kelvin(pub RangedU16<2000, 10000>);
|
||||
|
||||
pub trait KelvinLight: Light {
|
||||
type TurnToKelvinError: Error;
|
||||
fn turn_to_kelvin(
|
||||
&mut self,
|
||||
temperature: Kelvin,
|
||||
) -> impl Future<Output = Result<(), Self::TurnToKelvinError>> + Send;
|
||||
}
|
||||
|
||||
// TODO: replace with a type from a respected and useful library
|
||||
#[derive(Debug, Clone, Copy, derive_more::From, derive_more::Into)]
|
||||
pub struct Rgb(pub u8, pub u8, pub u8);
|
||||
|
||||
pub trait RgbLight: Light {
|
||||
type TurnToRgbError: Error;
|
||||
fn turn_to_rgb(
|
||||
&mut self,
|
||||
color: Rgb,
|
||||
) -> impl Future<Output = Result<(), Self::TurnToRgbError>> + Send;
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
[tool.poetry]
|
||||
name = "smart-home-in-rust-with-home-assistant"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["J / Jacob Babich <jacobbabichpublic+git@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[build-system]
|
||||
requires = ["maturin>=1.2,<2.0"]
|
||||
build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "smart-home-in-rust-with-home-assistant"
|
||||
requires-python = ">=3.7"
|
||||
classifiers = [
|
||||
"Programming Language :: Rust",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
]
|
||||
|
||||
|
||||
[tool.maturin]
|
||||
features = ["pyo3/extension-module"]
|
7
python-utils/Cargo.toml
Normal file
7
python-utils/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "python-utils"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
pyo3 = { workspace = true }
|
@@ -1,6 +1,6 @@
|
||||
use pyo3::{exceptions::PyTypeError, prelude::*};
|
||||
|
||||
/// Create a GIL-independent reference (similar to [`Arc`](std::sync::Arc))
|
||||
/// Create a GIL-independent reference
|
||||
pub fn detach<T>(bound: &Bound<T>) -> Py<T> {
|
||||
let py = bound.py();
|
||||
bound.as_unbound().clone_ref(py)
|
@@ -1,16 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::{arbitrary::Arbitrary, map_key::MapKey};
|
||||
|
||||
#[derive(Debug, Clone, Default, derive_more::From, derive_more::Into, IntoPyObject)]
|
||||
pub struct Map(pub BTreeMap<MapKey, Arbitrary>);
|
||||
|
||||
impl<'py> FromPyObject<'py> for Map {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let inner = ob.extract()?;
|
||||
|
||||
Ok(Self(inner))
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::id::Id;
|
||||
|
||||
/// The context that triggered something.
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct Context<Event> {
|
||||
pub id: Id,
|
||||
pub user_id: Option<String>,
|
||||
pub parent_id: Option<String>,
|
||||
/// In order to prevent cycles, the user must decide to pass [`Py<PyAny>`] for the `Event` type here
|
||||
/// or for the `Context` type in [`Event`]
|
||||
pub origin_event: Event,
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
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<OldAttributes, OldStateContextEvent, NewAttributes, NewStateContextEvent> {
|
||||
pub entity_id: EntityId,
|
||||
pub old_state: Option<State<OldAttributes, OldStateContextEvent>>,
|
||||
pub new_state: Option<State<NewAttributes, NewStateContextEvent>>,
|
||||
}
|
||||
|
||||
/// A state changed event is fired when on state write the state is changed.
|
||||
pub type Event<OldAttributes, OldStateContextEvent, NewAttributes, NewStateContextEvent, Context> =
|
||||
super::super::event::Event<
|
||||
Type,
|
||||
Data<OldAttributes, OldStateContextEvent, NewAttributes, NewStateContextEvent>,
|
||||
Context,
|
||||
>;
|
@@ -1,17 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use crate::home_assistant::entity_id::EntityId;
|
||||
|
||||
use super::event::context::context::Context;
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct State<Attributes, ContextEvent> {
|
||||
pub entity_id: EntityId,
|
||||
pub state: String,
|
||||
pub attributes: Attributes,
|
||||
pub last_changed: Option<DateTime<Utc>>,
|
||||
pub last_reported: Option<DateTime<Utc>>,
|
||||
pub last_updated: Option<DateTime<Utc>>,
|
||||
pub context: Context<ContextEvent>,
|
||||
}
|
61
src/lib.rs
61
src/lib.rs
@@ -1,61 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use home_assistant::home_assistant::HomeAssistant;
|
||||
use pyo3::prelude::*;
|
||||
use shadow_rs::shadow;
|
||||
use tokio::time::interval;
|
||||
use tracing::{level_filters::LevelFilter, Level};
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, format::FmtSpan},
|
||||
layer::SubscriberExt,
|
||||
registry,
|
||||
util::SubscriberInitExt,
|
||||
Layer,
|
||||
};
|
||||
use tracing_to_home_assistant::TracingToHomeAssistant;
|
||||
|
||||
mod arbitrary;
|
||||
mod home_assistant;
|
||||
mod python_utils;
|
||||
mod store;
|
||||
mod tracing_to_home_assistant;
|
||||
|
||||
shadow!(build_info);
|
||||
|
||||
async fn real_main(home_assistant: HomeAssistant) -> ! {
|
||||
registry()
|
||||
.with(
|
||||
fmt::layer()
|
||||
.pretty()
|
||||
.with_span_events(FmtSpan::ACTIVE)
|
||||
.with_filter(LevelFilter::from_level(Level::TRACE)),
|
||||
)
|
||||
.with(TracingToHomeAssistant)
|
||||
.init();
|
||||
|
||||
let built_at = build_info::BUILD_TIME;
|
||||
tracing::info!(built_at);
|
||||
|
||||
let duration = Duration::from_millis(5900);
|
||||
let mut interval = interval(duration);
|
||||
|
||||
loop {
|
||||
let instant = interval.tick().await;
|
||||
|
||||
tracing::debug!(?instant, "it is now");
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, 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(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(main, m)?)?;
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user