Compare commits

...

11 Commits

18 changed files with 886 additions and 355 deletions

71
.gitignore vendored
View File

@@ -1,72 +1 @@
/target /target
# Byte-compiled / optimized / DLL files
__pycache__/
.pytest_cache/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
.venv/
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
include/
man/
venv/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-selfcheck.json
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
.DS_Store
# Sphinx documentation
docs/_build/
# PyCharm
.idea/
# VSCode
.vscode/
# Pyenv
.python-version

190
Cargo.lock generated
View File

@@ -41,6 +41,65 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "arbitrary-value" name = "arbitrary-value"
version = "0.1.0" version = "0.1.0"
@@ -186,6 +245,12 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "by_address"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.10.1" version = "1.10.1"
@@ -251,6 +316,52 @@ dependencies = [
"phf_codegen", "phf_codegen",
] ]
[[package]]
name = "clap"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]] [[package]]
name = "const_format" name = "const_format"
version = "0.2.34" version = "0.2.34"
@@ -407,7 +518,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"backon", "backon",
"deranged", "deranged",
"derive_more",
"mac_address", "mac_address",
"palette",
"protocol", "protocol",
"serde", "serde",
"serde_json", "serde_json",
@@ -459,6 +572,12 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@@ -974,6 +1093,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407" checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.14.0" version = "0.14.0"
@@ -1122,7 +1247,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1193,6 +1318,30 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "palette"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
dependencies = [
"approx",
"fast-srgb8",
"palette_derive",
"phf",
]
[[package]]
name = "palette_derive"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
dependencies = [
"by_address",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.9.10" version = "0.9.10"
@@ -1227,6 +1376,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [ dependencies = [
"phf_macros",
"phf_shared", "phf_shared",
] ]
@@ -1250,6 +1400,19 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
] ]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.11.3" version = "0.11.3"
@@ -1313,6 +1476,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"deranged", "deranged",
"derive_more", "derive_more",
"ext-trait",
"palette",
"serde",
"snafu",
"strum",
] ]
[[package]] [[package]]
@@ -1690,6 +1858,7 @@ dependencies = [
"arc-swap", "arc-swap",
"async-gate", "async-gate",
"axum", "axum",
"clap",
"deranged", "deranged",
"driver-kasa", "driver-kasa",
"emitter-and-signal", "emitter-and-signal",
@@ -1745,7 +1914,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1933,7 +2102,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -2136,6 +2305,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
@@ -2327,6 +2502,15 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"

View File

@@ -19,8 +19,12 @@ chrono = "0.4.40"
chrono-tz = "0.10.1" chrono-tz = "0.10.1"
deranged = "0.4" deranged = "0.4"
derive_more = "2.0.1" derive_more = "2.0.1"
snafu = "0.8.5" ext-trait = "2.0.0"
tokio = "1.32.0" palette = "0.7"
pyo3 = "0.24.0" pyo3 = "0.24.0"
pyo3-async-runtimes = "0.24.0" pyo3-async-runtimes = "0.24.0"
serde = "1.0.219"
snafu = "0.8.5"
strum = "0.27.1"
tokio = "1.32.0"
tracing = "0.1.37" tracing = "0.1.37"

View File

@@ -4,7 +4,7 @@ You probably don't want to use this if you're not me.
## Unlicense & Contributing ## Unlicense & Contributing
The contents of this repository are released under the [Unlicense](UNLICENSE). The contents of this repository are released under the [Unlicense](UNLICENSE). Cargo-based dependencies of this project use free software `licenses` marked `allow` in [the `cargo-deny` configuration](deny.toml). [Home Assistant itself is Apache 2.0](https://www.home-assistant.io/developers/license/) and libraries it uses may be licensed differently and cannot be trivially tracked from here.
Please create an issue before working on a pull request. It's helpful for you to know if the idea you have in mind will for sure be incorporated into the project, and won't require you to acquaint yourself with the project internals. It even opens the floor for someone else to do the work implementing it for you. Please create an issue before working on a pull request. It's helpful for you to know if the idea you have in mind will for sure be incorporated into the project, and won't require you to acquaint yourself with the project internals. It even opens the floor for someone else to do the work implementing it for you.

View File

@@ -7,9 +7,11 @@ license = { workspace = true }
[dependencies] [dependencies]
backon = { workspace = true } backon = { workspace = true }
deranged = { workspace = true } deranged = { workspace = true }
derive_more = { workspace = true, features = ["from"] }
mac_address = { version = "1.1.8", features = ["serde"] } mac_address = { version = "1.1.8", features = ["serde"] }
palette = { workspace = true }
protocol = { path = "../../protocol" } protocol = { path = "../../protocol" }
serde = { version = "1.0.219", features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
serde_repr = "0.1.20" serde_repr = "0.1.20"
serde_with = "3.12.0" serde_with = "3.12.0"

View File

@@ -1,10 +1,14 @@
use crate::messages::{GetSysInfo, GetSysInfoResponse, LB130USSys, SysInfo}; use crate::messages::{
GetSysInfo, GetSysInfoResponse, LB130USSys, LightState, Off, On, SetLightLastOn, SetLightOff,
SetLightState, SetLightStateArgs, SetLightStateResponse, SetLightTo, SysInfo,
};
use backon::{FibonacciBuilder, Retryable}; use backon::{FibonacciBuilder, Retryable};
use protocol::light::{Kelvin, KelvinLight, Light, Rgb, RgbLight};
use serde::{Deserialize, Serialize};
use snafu::{ResultExt, Snafu}; use snafu::{ResultExt, Snafu};
use std::{convert::Infallible, io, net::SocketAddr, num::NonZero, time::Duration}; use std::{io, net::SocketAddr, num::NonZero, time::Duration};
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter},
net::TcpStream, net::TcpStream,
sync::{mpsc, oneshot}, sync::{mpsc, oneshot},
time::timeout, time::timeout,
@@ -51,11 +55,23 @@ pub enum CommunicationError {
WrongDevice, WrongDevice,
} }
fn should_try_reconnecting(communication_error: &CommunicationError) -> bool {
matches!(
communication_error,
CommunicationError::WriteError { .. } | CommunicationError::ReadError { .. }
)
}
#[derive(Debug)] #[derive(Debug)]
enum LB130USMessage { enum LB130USMessage {
GetSysInfo(oneshot::Sender<Result<LB130USSys, CommunicationError>>), GetSysInfo(oneshot::Sender<Result<LB130USSys, CommunicationError>>),
SetLightState(
SetLightStateArgs,
oneshot::Sender<Result<SetLightStateResponse, CommunicationError>>,
),
} }
#[tracing::instrument(skip(messages))]
async fn lb130us_actor( async fn lb130us_actor(
addr: SocketAddr, addr: SocketAddr,
disconnect_after_idle: Duration, disconnect_after_idle: Duration,
@@ -116,86 +132,105 @@ async fn lb130us_actor(
tracing::info!("yay connected and got a message"); tracing::info!("yay connected and got a message");
// TODO: do something
match message { match message {
LB130USMessage::GetSysInfo(callback) => { LB130USMessage::GetSysInfo(callback) => {
tracing::info!("going to try to get sys info for you..."); let res = handle_get_sysinfo(writer, reader).await;
// TODO: extract to its own function if let Err(communication_error) = &res {
let outgoing = GetSysInfo; if should_try_reconnecting(communication_error) {
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(); 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); let _ = callback.send(res);
tracing::info!(?incoming_message); }
LB130USMessage::SetLightState(args, callback) => {
let res = handle_set_light_state(writer, reader, args).await;
let response: GetSysInfoResponse = match serde_json::from_slice(&incoming_message) { if let Err(communication_error) = &res {
Ok(response) => response, if should_try_reconnecting(communication_error) {
Err(err) => { connection_cell.take();
let _ = callback
.send(Err(CommunicationError::DeserializeError { source: err }));
continue;
} }
}; }
tracing::info!(?response);
let SysInfo::LB130US(lb130us) = response.system.get_sysinfo else { let _ = callback.send(res);
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!");
} }
} }
} }
} }
#[tracing::instrument(skip(writer, reader, request))]
async fn send_request<
AW: AsyncWrite + Unpin,
AR: AsyncRead + Unpin,
Request: Serialize,
Response: for<'de> Deserialize<'de>,
>(
writer: &mut AW,
reader: &mut AR,
request: &Request,
) -> Result<Response, CommunicationError> {
let outgoing = serde_json::to_vec(request).context(SerializeSnafu)?;
tracing::info!(?outgoing);
let encrypted_outgoing = into_encrypted(outgoing);
tracing::info!(?encrypted_outgoing);
writer
.write_all(&encrypted_outgoing)
.await
.context(WriteSnafu)?;
writer.flush().await.context(WriteSnafu)?;
tracing::info!("sent it, now about to try to get a response");
let incoming_length = reader.read_u32().await.context(ReadSnafu)?;
tracing::info!(?incoming_length);
let mut incoming_message = Vec::new();
incoming_message.resize(incoming_length as usize, 0);
reader
.read_exact(&mut incoming_message)
.await
.context(ReadSnafu)?;
XorEncryption::<171>::decrypt_in_place(&mut incoming_message);
tracing::info!(?incoming_message);
let response_as_json: serde_json::Value =
serde_json::from_slice(&incoming_message).context(DeserializeSnafu)?;
tracing::info!(?response_as_json);
let response = Response::deserialize(response_as_json).context(DeserializeSnafu)?;
Ok(response)
}
#[tracing::instrument(skip(writer, reader))]
async fn handle_get_sysinfo<AW: AsyncWrite + Unpin, AR: AsyncRead + Unpin>(
writer: &mut AW,
reader: &mut AR,
) -> Result<LB130USSys, CommunicationError> {
let request = GetSysInfo;
let response: GetSysInfoResponse = send_request(writer, reader, &request).await?;
let SysInfo::LB130US(lb130us) = response.system.get_sysinfo else {
return Err(CommunicationError::WrongDevice);
};
tracing::info!(?lb130us);
Ok(lb130us)
}
#[tracing::instrument(skip(writer, reader))]
async fn handle_set_light_state<AW: AsyncWrite + Unpin, AR: AsyncRead + Unpin>(
writer: &mut AW,
reader: &mut AR,
args: SetLightStateArgs,
) -> Result<SetLightStateResponse, CommunicationError> {
let request = SetLightState(args);
send_request(writer, reader, &request).await
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LB130USHandle { pub struct LB130USHandle {
sender: mpsc::Sender<LB130USMessage>, sender: mpsc::Sender<LB130USMessage>,
@@ -225,52 +260,19 @@ impl LB130USHandle {
.map_err(|_| HandleError::Dead)? .map_err(|_| HandleError::Dead)?
.context(CommunicationSnafu) .context(CommunicationSnafu)
} }
}
impl Light for LB130USHandle { pub async fn set_light_state(
type IsOnError = Infallible; // TODO &self,
args: SetLightStateArgs,
async fn is_on(&self) -> Result<bool, Self::IsOnError> { ) -> Result<SetLightStateResponse, HandleError> {
todo!() let (sender, receiver) = oneshot::channel();
} self.sender
.send(LB130USMessage::SetLightState(args, sender))
type IsOffError = Infallible; // TODO .await
.map_err(|_| HandleError::Dead)?;
async fn is_off(&self) -> Result<bool, Self::IsOffError> { receiver
todo!() .await
} .map_err(|_| HandleError::Dead)?
.context(CommunicationSnafu)
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!()
} }
} }

View File

@@ -0,0 +1,97 @@
use std::convert::Infallible;
use palette::{encoding::Srgb, Hsv, IntoColor};
use protocol::light::{GetState, Kelvin, SetState, TurnToColor, TurnToTemperature};
use snafu::{ResultExt, Snafu};
use crate::{
connection::{HandleError, LB130USHandle},
messages::{
Angle, Hsb, LightState, Off, On, Percentage, SetLightHsv, SetLightLastOn, SetLightOff,
SetLightStateArgs, SetLightTo,
},
};
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum GetStateError {
HandleError { source: HandleError },
}
impl GetState for LB130USHandle {
type Error = GetStateError;
async fn get_state(&self) -> Result<protocol::light::State, Self::Error> {
let sys = self
.get_sysinfo()
.await
.context(get_state_error::HandleSnafu)?;
let light_state = sys.sys_info.light_state;
let state = match light_state {
LightState::On { .. } => protocol::light::State::On,
LightState::Off { .. } => protocol::light::State::Off,
};
Ok(state)
}
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum SetStateError {
HandleError { source: HandleError },
}
impl SetState for LB130USHandle {
type Error = SetStateError;
async fn set_state(&mut self, state: protocol::light::State) -> Result<(), Self::Error> {
let to = match state {
protocol::light::State::Off => SetLightTo::Off(SetLightOff { on_off: Off }),
protocol::light::State::On => SetLightTo::LastOn(SetLightLastOn { on_off: On }),
};
let args = SetLightStateArgs {
to,
transition: None,
};
self.set_light_state(args)
.await
.context(set_state_error::HandleSnafu)?;
Ok(())
}
}
impl TurnToTemperature for LB130USHandle {
type Error = Infallible; // TODO
async fn turn_to_temperature(&mut self, temperature: Kelvin) -> Result<(), Self::Error> {
todo!()
}
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum TurnToColorError {
HandleError { source: HandleError },
}
impl TurnToColor for LB130USHandle {
type Error = TurnToColorError;
async fn turn_to_color(&mut self, color: protocol::light::Oklch) -> Result<(), Self::Error> {
let hsv: Hsv<Srgb, f64> = color.into_color();
let hsb = hsv.into_color();
self.set_light_state(SetLightStateArgs {
to: SetLightTo::Hsv(SetLightHsv { on_off: On, hsb }),
transition: None,
})
.await
.context(turn_to_color_error::HandleSnafu)?;
Ok(())
}
}

View File

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

View File

@@ -1,7 +1,8 @@
use std::{collections::BTreeMap, fmt::Display, str::FromStr}; use std::{collections::BTreeMap, fmt::Display, str::FromStr, time::Duration};
use deranged::{RangedU16, RangedU8}; use deranged::{RangedU16, RangedU8};
use mac_address::{MacAddress, MacParseError}; use mac_address::{MacAddress, MacParseError};
use palette::{FromColor, Hsv};
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize}; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
use serde_repr::Deserialize_repr; use serde_repr::Deserialize_repr;
use serde_with::{DeserializeFromStr, SerializeDisplay}; use serde_with::{DeserializeFromStr, SerializeDisplay};
@@ -36,38 +37,38 @@ pub struct GetSysInfoResponseSystem {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CommonSysInfo { pub struct CommonSysInfo {
active_mode: ActiveMode, pub active_mode: ActiveMode,
alias: String, pub alias: String,
ctrl_protocols: CtrlProtocols, pub ctrl_protocols: CtrlProtocols,
description: String, pub description: String,
dev_state: DevState, pub dev_state: DevState,
#[serde(rename = "deviceId")] #[serde(rename = "deviceId")]
device_id: DeviceId, pub device_id: DeviceId,
disco_ver: String, pub disco_ver: String,
err_code: i32, // No idea pub err_code: i32, // No idea
heapsize: u64, // No idea pub heapsize: u64, // No idea
#[serde(rename = "hwId")] #[serde(rename = "hwId")]
hw_id: HardwareId, pub hw_id: HardwareId,
hw_ver: String, pub hw_ver: String,
is_color: IsColor, pub is_color: IsColor,
is_dimmable: IsDimmable, pub is_dimmable: IsDimmable,
is_factory: bool, pub is_factory: bool,
is_variable_color_temp: IsVariableColorTemp, pub is_variable_color_temp: IsVariableColorTemp,
light_state: LightState, pub light_state: LightState,
mic_mac: MacAddressWithoutSeparators, pub mic_mac: MacAddressWithoutSeparators,
mic_type: MicType, pub mic_type: MicType,
// model: Model, // model: Model,
#[serde(rename = "oemId")] #[serde(rename = "oemId")]
oem_id: OemId, pub oem_id: OemId,
preferred_state: Vec<PreferredStateChoice>, pub preferred_state: Vec<PreferredStateChoice>,
rssi: i32, pub rssi: i32,
sw_ver: String, pub sw_ver: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct LB130USSys { pub struct LB130USSys {
#[serde(flatten)] #[serde(flatten)]
sys_info: CommonSysInfo, pub sys_info: CommonSysInfo,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -78,9 +79,9 @@ pub enum SysInfo {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct PreferredStateChoice { pub struct PreferredStateChoice {
#[serde(flatten)] #[serde(flatten)]
color: Color, pub color: Color,
} }
#[derive(Debug, SerializeDisplay, DeserializeFromStr)] #[derive(Debug, SerializeDisplay, DeserializeFromStr)]
@@ -162,9 +163,9 @@ enum IsVariableColorTemp {
VariableColorTemp = 1, VariableColorTemp = 1,
} }
type Percentage = RangedU8<0, 100>; pub type Percentage = RangedU8<0, 100>;
type Angle = RangedU16<0, 360>; pub type Angle = RangedU16<0, 360>;
type Kelvin = RangedU16<2500, 9000>; pub type Kelvin = RangedU16<2500, 9000>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct MaybeKelvin(Option<Kelvin>); struct MaybeKelvin(Option<Kelvin>);
@@ -198,13 +199,34 @@ struct RawColor {
saturation: Percentage, saturation: Percentage,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct Hsb { pub struct Hsb {
hue: Angle, hue: Angle,
saturation: Percentage, saturation: Percentage,
brightness: Percentage, brightness: Percentage,
} }
impl<S> FromColor<Hsv<S, f64>> for Hsb {
fn from_color(hsv: Hsv<S, f64>) -> Self {
let (hue, saturation, value) = hsv.into_components();
let hue = hue.into_positive_degrees();
let hue = Angle::new_saturating(hue as u16);
let saturation = saturation * (Percentage::MAX.get() as f64);
let saturation = Percentage::new_saturating(saturation as u8);
let brightness = value * (Percentage::MAX.get() as f64);
let brightness = Percentage::new_saturating(brightness as u8);
Hsb {
hue,
saturation,
brightness,
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct KelvinWithBrightness { struct KelvinWithBrightness {
kelvin: Kelvin, kelvin: Kelvin,
@@ -245,12 +267,86 @@ impl<'de> Deserialize<'de> for Color {
} }
} }
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Off;
impl<'de> Deserialize<'de> for Off {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
if value == 0 {
Ok(Off)
} else {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Unsigned(value.into()),
&"0",
))
}
}
}
impl Serialize for Off {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(0)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct On;
impl<'de> Deserialize<'de> for On {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
if value == 1 {
Ok(On)
} else {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Unsigned(value.into()),
&"1",
))
}
}
}
impl Serialize for On {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(1)
}
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
struct LightState { #[serde(untagged)]
pub enum LightState {
On {
on_off: On,
#[serde(flatten)]
color: Color,
mode: LightStateMode,
},
Off {
on_off: Off,
dft_on_state: DftOnState,
},
}
#[derive(Debug, Clone, Deserialize)]
struct DftOnState {
#[serde(flatten)] #[serde(flatten)]
color: Color, color: Color,
mode: LightStateMode, mode: LightStateMode,
on_off: OnOrOff,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
@@ -259,14 +355,6 @@ enum LightStateMode {
Normal, Normal,
} }
#[derive(Debug, Clone, Deserialize_repr)]
#[repr(u8)]
#[non_exhaustive]
enum OnOrOff {
Off = 0,
On = 1,
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
enum MicType { enum MicType {
#[serde(rename = "IOT.SMARTBULB")] #[serde(rename = "IOT.SMARTBULB")]
@@ -275,3 +363,59 @@ enum MicType {
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
struct OemId(pub String); struct OemId(pub String);
#[derive(Debug, Clone, Serialize)]
pub struct SetLightStateArgs {
#[serde(flatten)]
pub to: SetLightTo,
pub transition: Option<Duration>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SetLightOff {
pub on_off: Off,
}
#[derive(Debug, Clone, Serialize)]
pub struct SetLightLastOn {
pub on_off: On,
}
#[derive(Debug, Clone, Serialize)]
pub struct SetLightHsv {
pub on_off: On,
#[serde(flatten)]
pub hsb: Hsb,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum SetLightTo {
Off(SetLightOff),
LastOn(SetLightLastOn),
Hsv(SetLightHsv),
// TODO: kelvin
}
#[derive(Debug, Clone, derive_more::From)]
pub struct SetLightState(pub SetLightStateArgs);
impl Serialize for SetLightState {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let target = "smartlife.iot.smartbulb.lightingservice";
let cmd = "transition_light_state";
let arg = &self.0;
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, Clone, Deserialize)]
pub struct SetLightStateResponse {
// TODO
}

View File

@@ -6,5 +6,5 @@ license = { workspace = true }
[dependencies] [dependencies]
deranged = { workspace = true } deranged = { workspace = true }
ext-trait = "2.0.0" ext-trait = { workspace = true }
tokio = { workspace = true, features = ["sync"] } tokio = { workspace = true, features = ["sync"] }

View File

@@ -17,6 +17,7 @@ axum = { version = "0.8.1", default-features = false, features = [
"http1", "http1",
"tokio", "tokio",
] } ] }
clap = { version = "4", features = ["derive", "env"] }
deranged = { workspace = true, features = ["serde"] } deranged = { workspace = true, features = ["serde"] }
driver-kasa = { path = "../driver/kasa" } driver-kasa = { path = "../driver/kasa" }
emitter-and-signal = { path = "../emitter-and-signal" } emitter-and-signal = { path = "../emitter-and-signal" }

View File

@@ -1,16 +1,18 @@
use std::{str::FromStr, time::Duration}; use std::{path::PathBuf, str::FromStr, time::Duration};
use clap::Parser;
use driver_kasa::connection::LB130USHandle; use driver_kasa::connection::LB130USHandle;
use home_assistant::{ use home_assistant::{
home_assistant::HomeAssistant, light::HomeAssistantLight, object_id::ObjectId, home_assistant::HomeAssistant, light::HomeAssistantLight, object_id::ObjectId,
}; };
use protocol::light::Light; use protocol::light::{IsOff, IsOn};
use pyo3::prelude::*; use pyo3::prelude::*;
use shadow_rs::shadow; use shadow_rs::shadow;
use tokio::time::interval; use tokio::time::interval;
use tracing::{level_filters::LevelFilter, Level}; use tracing::{level_filters::LevelFilter, Level};
use tracing_appender::rolling::{self, RollingFileAppender};
use tracing_subscriber::{ use tracing_subscriber::{
fmt::{self, format::FmtSpan}, fmt::{self, fmt, format::FmtSpan},
layer::SubscriberExt, layer::SubscriberExt,
registry, registry,
util::SubscriberInitExt, util::SubscriberInitExt,
@@ -22,7 +24,53 @@ mod tracing_to_home_assistant;
shadow!(build_info); shadow!(build_info);
async fn real_main(home_assistant: HomeAssistant) -> ! { #[derive(Debug, Parser)]
struct Args {
#[arg(env)]
persistence_directory: Option<PathBuf>,
#[arg(env)]
tracing_directory: Option<PathBuf>,
#[arg(env, default_value = "")]
tracing_file_name_prefix: String,
#[arg(env, default_value = "log")]
tracing_file_name_suffix: String,
#[arg(env, default_value_t = 64)]
tracing_max_log_files: usize,
}
async fn real_main(
Args {
persistence_directory,
tracing_directory,
tracing_file_name_prefix,
tracing_file_name_suffix,
tracing_max_log_files,
}: Args,
home_assistant: HomeAssistant,
) -> ! {
let tracing_to_directory_res = tracing_directory
.map(|tracing_directory| {
tracing_appender::rolling::Builder::new()
.filename_prefix(tracing_file_name_prefix)
.filename_suffix(tracing_file_name_suffix)
.max_log_files(tracing_max_log_files)
.build(tracing_directory)
.map(tracing_appender::non_blocking)
})
.transpose();
let (tracing_to_directory, _guard, tracing_to_directory_initialization_error) =
match tracing_to_directory_res {
Ok(tracing_to_directory) => match tracing_to_directory {
Some((tracing_to_directory, guard)) => {
(Some(tracing_to_directory), Some(guard), None)
}
None => (None, None, None),
},
Err(error) => (None, None, Some(error)),
};
registry() registry()
.with( .with(
fmt::layer() fmt::layer()
@@ -31,14 +79,25 @@ async fn real_main(home_assistant: HomeAssistant) -> ! {
.with_filter(LevelFilter::from_level(Level::TRACE)), .with_filter(LevelFilter::from_level(Level::TRACE)),
) )
.with(TracingToHomeAssistant) .with(TracingToHomeAssistant)
.with(tracing_to_directory.map(|writer| {
fmt::layer()
.pretty()
.with_span_events(FmtSpan::ACTIVE)
.with_writer(writer)
.with_filter(LevelFilter::from_level(Level::TRACE))
}))
.init(); .init();
if let Some(error) = tracing_to_directory_initialization_error {
tracing::error!(?error, "cannot trace to directory");
}
let built_at = build_info::BUILD_TIME; let built_at = build_info::BUILD_TIME;
tracing::info!(built_at); tracing::info!(built_at);
// let lamp = HomeAssistantLight { // let lamp = HomeAssistantLight {
// home_assistant, // home_assistant,
// object_id: ObjectId::from_str("jacob_s_lamp_top").unwrap(), // object_id: ObjectId::from_str("jacob_s_lamp_side").unwrap(),
// }; // };
let ip = [10, 0, 3, 71]; let ip = [10, 0, 3, 71];
@@ -59,6 +118,11 @@ async fn real_main(home_assistant: HomeAssistant) -> ! {
let sysinfo_res = some_light.get_sysinfo().await; let sysinfo_res = some_light.get_sysinfo().await;
tracing::info!(?sysinfo_res, "got sys info"); tracing::info!(?sysinfo_res, "got sys info");
let is_on = some_light.is_on().await;
tracing::info!(?is_on);
let is_off = some_light.is_off().await;
tracing::info!(?is_off);
// let is_on = lamp.is_on().await; // let is_on = lamp.is_on().await;
// tracing::info!(?is_on); // tracing::info!(?is_on);
// let is_off = lamp.is_off().await; // let is_off = lamp.is_off().await;
@@ -71,8 +135,10 @@ async fn real_main(home_assistant: HomeAssistant) -> ! {
#[pyfunction] #[pyfunction]
fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, PyAny>> { fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, PyAny>> {
let args = Args::parse();
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async { pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async {
real_main(home_assistant).await; real_main(args, home_assistant).await;
}) })
} }

View File

@@ -26,7 +26,7 @@ pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
python-utils = { path = "../python-utils" } python-utils = { path = "../python-utils" }
smol_str = "0.3.2" smol_str = "0.3.2"
snafu = { workspace = true } snafu = { workspace = true }
strum = { version = "0.27.1", features = ["derive"] } strum = { workspace = true, features = ["derive"] }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { optional = true, workspace = true } tracing = { optional = true, workspace = true }
ulid = "1.2.0" ulid = "1.2.0"

View File

@@ -4,105 +4,73 @@ use crate::{
event::context::context::Context, event::context::context::Context,
state::{ErrorState, HomeAssistantState, UnexpectedState}, state::{ErrorState, HomeAssistantState, UnexpectedState},
}; };
use arbitrary_value::arbitrary::Arbitrary; use protocol::light::{GetState, SetState};
use protocol::light::Light;
use pyo3::prelude::*; use pyo3::prelude::*;
use python_utils::IsNone;
use snafu::{ResultExt, Snafu}; use snafu::{ResultExt, Snafu};
#[derive(Debug, Snafu)] #[derive(Debug, Snafu)]
pub enum IsStateError { pub enum GetStateError {
GetStateObjectError { source: GetStateObjectError }, GetStateObjectError { source: GetStateObjectError },
Error { state: ErrorState }, Error { state: ErrorState },
UnexpectedError { state: UnexpectedState }, UnexpectedError { state: UnexpectedState },
} }
impl Light for HomeAssistantLight { impl GetState for HomeAssistantLight {
type IsOnError = IsStateError; type Error = GetStateError;
async fn is_on(&self) -> Result<bool, Self::IsOnError> { async fn get_state(&self) -> Result<protocol::light::State, Self::Error> {
let state_object = self.get_state_object().context(GetStateObjectSnafu)?; let state_object = self.get_state_object().context(GetStateObjectSnafu)?;
let state = state_object.state; let state = state_object.state;
match state { match state {
HomeAssistantState::Ok(light_state) => Ok(matches!(light_state, LightState::On)), HomeAssistantState::Ok(light_state) => Ok(light_state.into()),
HomeAssistantState::Err(state) => Err(IsStateError::Error { state }), HomeAssistantState::Err(error_state) => {
Err(GetStateError::Error { state: error_state })
}
HomeAssistantState::UnexpectedErr(state) => { HomeAssistantState::UnexpectedErr(state) => {
Err(IsStateError::UnexpectedError { state }) Err(GetStateError::UnexpectedError { state })
} }
} }
} }
}
type IsOffError = IsStateError;
impl SetState for HomeAssistantLight {
async fn is_off(&self) -> Result<bool, Self::IsOffError> { type Error = PyErr;
let state_object = self.get_state_object().context(GetStateObjectSnafu)?;
let state = state_object.state; async fn set_state(&mut self, state: protocol::light::State) -> Result<(), Self::Error> {
let context: Option<Context<()>> = None;
match state { let target: Option<()> = None;
HomeAssistantState::Ok(light_state) => Ok(matches!(light_state, LightState::Off)),
HomeAssistantState::Err(state) => Err(IsStateError::Error { state }), let services = Python::with_gil(|py| self.home_assistant.services(py))?;
HomeAssistantState::UnexpectedErr(state) => {
Err(IsStateError::UnexpectedError { state }) let _: IsNone = match state {
} protocol::light::State::Off => {
} services
} .call_service(
TurnOff {
type TurnOnError = PyErr; entity_id: self.entity_id(),
},
async fn turn_on(&mut self) -> Result<(), Self::TurnOnError> { context,
let context: Option<Context<()>> = None; target,
let target: Option<()> = None; false,
)
let services = Python::with_gil(|py| self.home_assistant.services(py))?; .await
// TODO }
let service_response: Arbitrary = services protocol::light::State::On => {
.call_service( services
TurnOn { .call_service(
entity_id: self.entity_id(), TurnOn {
}, entity_id: self.entity_id(),
context, },
target, context,
false, target,
) false,
.await?; )
.await
// TODO }
#[cfg(feature = "tracing")] }?;
tracing::info!(?service_response);
Ok(())
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!()
}
} }

View File

@@ -20,3 +20,21 @@ impl<'py> FromPyObject<'py> for LightState {
Ok(state) Ok(state)
} }
} }
impl From<LightState> for protocol::light::State {
fn from(light_state: LightState) -> Self {
match light_state {
LightState::On => protocol::light::State::On,
LightState::Off => protocol::light::State::Off,
}
}
}
impl From<protocol::light::State> for LightState {
fn from(state: protocol::light::State) -> Self {
match state {
protocol::light::State::On => LightState::On,
protocol::light::State::Off => LightState::Off,
}
}
}

View File

@@ -4,6 +4,16 @@ version = "0.1.0"
edition = "2021" edition = "2021"
license = { workspace = true } license = { workspace = true }
[features]
default = []
serde = ["dep:serde"]
[dependencies] [dependencies]
deranged = { workspace = true } deranged = { workspace = true }
derive_more = { workspace = true } derive_more = { workspace = true }
ext-trait = { workspace = true }
palette = { workspace = true }
snafu = { workspace = true }
strum = { workspace = true, features = ["derive"] }
serde = { optional = true, workspace = true, features = ["derive"] }

View File

@@ -1,43 +1,124 @@
use std::{error::Error, future::Future}; use std::{error::Error, future::Future};
use deranged::RangedU16; use deranged::RangedU16;
use snafu::{ResultExt, Snafu};
pub trait Light { #[derive(
type IsOnError: Error; Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, strum::Display, strum::EnumIs,
fn is_on(&self) -> impl Future<Output = Result<bool, Self::IsOnError>> + Send; )]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
type IsOffError: Error; pub enum State {
fn is_off(&self) -> impl Future<Output = Result<bool, Self::IsOffError>> + Send; Off,
On,
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)] impl State {
pub struct Kelvin(pub RangedU16<2000, 10000>); pub const fn invert(self) -> Self {
match self {
State::Off => State::On,
State::On => State::Off,
}
}
}
pub trait KelvinLight: Light { impl From<bool> for State {
type TurnToKelvinError: Error; fn from(bool: bool) -> Self {
fn turn_to_kelvin( if bool {
State::On
} else {
State::Off
}
}
}
impl From<State> for bool {
fn from(state: State) -> Self {
state.is_on()
}
}
pub trait GetState {
type Error: Error;
fn get_state(&self) -> impl Future<Output = Result<State, Self::Error>> + Send;
}
#[ext_trait::extension(pub trait IsOff)]
impl<T: GetState> T {
async fn is_off(&self) -> Result<bool, T::Error> {
Ok(self.get_state().await?.is_off())
}
}
#[ext_trait::extension(pub trait IsOn)]
impl<T: GetState> T {
async fn is_on(&self) -> Result<bool, T::Error> {
Ok(self.get_state().await?.is_on())
}
}
pub trait SetState {
type Error: Error;
fn set_state(&mut self, state: State) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[ext_trait::extension(pub trait TurnOff)]
impl<T: SetState> T {
async fn turn_off(&mut self) -> Result<(), T::Error> {
self.set_state(State::Off).await
}
}
#[ext_trait::extension(pub trait TurnOn)]
impl<T: SetState> T {
async fn turn_on(&mut self) -> Result<(), T::Error> {
self.set_state(State::On).await
}
}
pub trait Toggle {
type Error: Error;
fn toggle(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[derive(Debug, Clone, Snafu)]
pub enum InvertToToggleError<GetStateError: Error + 'static, SetStateError: Error + 'static> {
GetStateError { source: GetStateError },
SetStateError { source: SetStateError },
}
impl<T: GetState + SetState + Send> Toggle for T
where
<T as GetState>::Error: 'static,
<T as SetState>::Error: 'static,
{
type Error = InvertToToggleError<<T as GetState>::Error, <T as SetState>::Error>;
/// Toggle the light by setting it to the inverse of its current state
async fn toggle(&mut self) -> Result<(), Self::Error> {
let state = self.get_state().await.context(GetStateSnafu)?;
self.set_state(state.invert())
.await
.context(SetStateSnafu)?;
Ok(())
}
}
pub type Kelvin = RangedU16<2000, 10000>;
pub trait TurnToTemperature {
type Error: Error;
fn turn_to_temperature(
&mut self, &mut self,
temperature: Kelvin, temperature: Kelvin,
) -> impl Future<Output = Result<(), Self::TurnToKelvinError>> + Send; ) -> impl Future<Output = Result<(), Self::Error>> + Send;
} }
// TODO: replace with a type from a respected and useful library pub type Oklch = palette::Oklch<f64>;
#[derive(Debug, Clone, Copy, derive_more::From, derive_more::Into)]
pub struct Rgb(pub u8, pub u8, pub u8);
pub trait RgbLight: Light { pub trait TurnToColor {
type TurnToRgbError: Error; type Error: Error;
fn turn_to_rgb( fn turn_to_color(
&mut self, &mut self,
color: Rgb, color: Oklch,
) -> impl Future<Output = Result<(), Self::TurnToRgbError>> + Send; ) -> impl Future<Output = Result<(), Self::Error>> + Send;
} }

View File

@@ -1,4 +1,28 @@
use pyo3::{exceptions::PyTypeError, prelude::*}; use std::convert::Infallible;
use pyo3::{exceptions::PyTypeError, prelude::*, types::PyNone};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct IsNone;
impl<'py> FromPyObject<'py> for IsNone {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
ob.downcast::<PyNone>()?;
Ok(IsNone)
}
}
impl<'py> IntoPyObject<'py> for IsNone {
type Target = PyNone;
type Output = Borrowed<'py, 'py, Self::Target>;
type Error = Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(PyNone::get(py))
}
}
/// Create a GIL-independent reference /// Create a GIL-independent reference
pub fn detach<T>(bound: &Bound<T>) -> Py<T> { pub fn detach<T>(bound: &Bound<T>) -> Py<T> {