Compare commits
15 Commits
cc51a262ae
...
main
Author | SHA1 | Date | |
---|---|---|---|
10bceb55b8 | |||
e219edb64b | |||
da321db40b | |||
cff48691ef | |||
089e96b99f | |||
50e9ee43f7 | |||
c0b27dc5f0 | |||
277182a93e | |||
d6515521a4 | |||
472ca50ec0 | |||
e680f10be8 | |||
de3ab27414 | |||
c95d2f8d99 | |||
6e366a9c51 | |||
d1daa0bc01 |
71
.gitignore
vendored
71
.gitignore
vendored
@@ -1,72 +1 @@
|
||||
/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
|
193
Cargo.lock
generated
193
Cargo.lock
generated
@@ -41,6 +41,65 @@ dependencies = [
|
||||
"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]]
|
||||
name = "arbitrary-value"
|
||||
version = "0.1.0"
|
||||
@@ -186,6 +245,12 @@ version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "by_address"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
@@ -251,6 +316,52 @@ dependencies = [
|
||||
"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]]
|
||||
name = "const_format"
|
||||
version = "0.2.34"
|
||||
@@ -407,7 +518,9 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"backon",
|
||||
"deranged",
|
||||
"derive_more",
|
||||
"mac_address",
|
||||
"palette",
|
||||
"protocol",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -459,6 +572,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -648,7 +767,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"arbitrary-value",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"derive_more",
|
||||
"emitter-and-signal",
|
||||
"once_cell",
|
||||
@@ -975,6 +1093,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -1123,7 +1247,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1194,6 +1318,30 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.10"
|
||||
@@ -1228,6 +1376,7 @@ version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
@@ -1251,6 +1400,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
@@ -1314,6 +1476,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"derive_more",
|
||||
"ext-trait",
|
||||
"palette",
|
||||
"serde",
|
||||
"snafu",
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1691,8 +1858,7 @@ dependencies = [
|
||||
"arc-swap",
|
||||
"async-gate",
|
||||
"axum",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"clap",
|
||||
"deranged",
|
||||
"driver-kasa",
|
||||
"emitter-and-signal",
|
||||
@@ -1748,7 +1914,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1936,7 +2102,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2139,6 +2305,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -2330,6 +2502,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
|
@@ -19,8 +19,12 @@ chrono = "0.4.40"
|
||||
chrono-tz = "0.10.1"
|
||||
deranged = "0.4"
|
||||
derive_more = "2.0.1"
|
||||
snafu = "0.8.5"
|
||||
tokio = "1.32.0"
|
||||
ext-trait = "2.0.0"
|
||||
palette = "0.7"
|
||||
pyo3 = "0.24.0"
|
||||
pyo3-async-runtimes = "0.24.0"
|
||||
serde = "1.0.219"
|
||||
snafu = "0.8.5"
|
||||
strum = "0.27.1"
|
||||
tokio = "1.32.0"
|
||||
tracing = "0.1.37"
|
||||
|
41
PULL_REQUEST_TEMPLATE
Normal file
41
PULL_REQUEST_TEMPLATE
Normal file
@@ -0,0 +1,41 @@
|
||||
# Prior Issue
|
||||
<!--
|
||||
Please identify the tracked issue relating to this matter, or create one first if it does not yet exist.
|
||||
Write it like e.g.
|
||||
* `closes #567` (so that the issue is linked by keyword https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue),
|
||||
* `partially addresses #12 and #345`
|
||||
->
|
||||
|
||||
|
||||
# Future Improvements
|
||||
<!--
|
||||
Do you believe there's any more work to be done to round out this contribution? Perhaps documentation or testing?
|
||||
Any limitations in the feature or solution offered in this code? Anything else you want to say about the contribution?
|
||||
Or, like if this is a trivial change, of course you can just say succinctly that this is everything.
|
||||
-->
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Read, ensure you agree with, and affirm the copyright waiver below by keeping it in this pull request description.
|
||||
If you do not agree or cannot or will not fulfill the requirements described, then you must not include it in your contribution. You are recommended not to attempt to submit the contribution to this project in that case.
|
||||
-->
|
||||
|
||||
# Copyright waiver for <https://gitea.katniss.top/jacob/smart-home-in-rust-with-home-assistant> (mirrored at <https://github.com/babichjacob/smart-home-in-rust-with-home-assistant>)
|
||||
|
||||
I dedicate any and all copyright interest in this software to the
|
||||
public domain. I make this dedication for the benefit of the public at
|
||||
large and to the detriment of my heirs and successors. I intend this
|
||||
dedication to be an overt act of relinquishment in perpetuity of all
|
||||
present and future rights to this software under copyright law.
|
||||
|
||||
To the best of my knowledge and belief, my contributions are either
|
||||
originally authored by me or are derived from prior works which I have
|
||||
verified are also in the public domain and are not subject to claims
|
||||
of copyright by other parties.
|
||||
|
||||
To the best of my knowledge and belief, no individual, business,
|
||||
organization, government, or other entity has any copyright interest
|
||||
in my contributions, and I affirm that I will not make contributions
|
||||
that are otherwise encumbered.
|
17
README.md
Normal file
17
README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# smart home in Rust with Home Assistant
|
||||
|
||||
You probably don't want to use this if you're not me.
|
||||
|
||||
## Unlicense & Contributing
|
||||
|
||||
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.
|
||||
|
||||
Some [existing issues are labeled straightforward](https://gitea.katniss.top/jacob/smart-home-in-rust-with-home-assistant/issues?labels=42) and expected to be the easiest to work on, if you'd like to try.
|
||||
|
||||
Any pull request you make to this repository must
|
||||
1. contain exclusively commits that are [cryptographically verified](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) to have been authored by you.
|
||||
2. be explicitly dedicated to the public domain. You can do this by retaining the copywright waiver in [the pull request template](PULL_REQUEST_TEMPLATE).
|
||||
|
||||
Your contribution will be declined if it does not ensure this project remains completely free and unencumbered by anyone's copyright monopoly.
|
@@ -7,9 +7,11 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
backon = { workspace = true }
|
||||
deranged = { workspace = true }
|
||||
derive_more = { workspace = true, features = ["from"] }
|
||||
mac_address = { version = "1.1.8", features = ["serde"] }
|
||||
palette = { workspace = true }
|
||||
protocol = { path = "../../protocol" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
serde_repr = "0.1.20"
|
||||
serde_with = "3.12.0"
|
||||
|
@@ -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 protocol::light::{Kelvin, KelvinLight, Light, Rgb, RgbLight};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
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::{
|
||||
io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
|
||||
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter},
|
||||
net::TcpStream,
|
||||
sync::{mpsc, oneshot},
|
||||
time::timeout,
|
||||
@@ -51,11 +55,23 @@ pub enum CommunicationError {
|
||||
WrongDevice,
|
||||
}
|
||||
|
||||
fn should_try_reconnecting(communication_error: &CommunicationError) -> bool {
|
||||
matches!(
|
||||
communication_error,
|
||||
CommunicationError::WriteError { .. } | CommunicationError::ReadError { .. }
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum LB130USMessage {
|
||||
GetSysInfo(oneshot::Sender<Result<LB130USSys, CommunicationError>>),
|
||||
SetLightState(
|
||||
SetLightStateArgs,
|
||||
oneshot::Sender<Result<SetLightStateResponse, CommunicationError>>,
|
||||
),
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(messages))]
|
||||
async fn lb130us_actor(
|
||||
addr: SocketAddr,
|
||||
disconnect_after_idle: Duration,
|
||||
@@ -116,86 +132,105 @@ async fn lb130us_actor(
|
||||
|
||||
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...");
|
||||
let res = handle_get_sysinfo(writer, reader).await;
|
||||
|
||||
// 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) => {
|
||||
if let Err(communication_error) = &res {
|
||||
if should_try_reconnecting(communication_error) {
|
||||
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 _ = callback.send(res);
|
||||
}
|
||||
LB130USMessage::SetLightState(args, callback) => {
|
||||
let res = handle_set_light_state(writer, reader, args).await;
|
||||
|
||||
let response: GetSysInfoResponse = match serde_json::from_slice(&incoming_message) {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
let _ = callback
|
||||
.send(Err(CommunicationError::DeserializeError { source: err }));
|
||||
continue;
|
||||
if let Err(communication_error) = &res {
|
||||
if should_try_reconnecting(communication_error) {
|
||||
connection_cell.take();
|
||||
}
|
||||
};
|
||||
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!");
|
||||
let _ = callback.send(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub struct LB130USHandle {
|
||||
sender: mpsc::Sender<LB130USMessage>,
|
||||
@@ -225,52 +260,19 @@ impl LB130USHandle {
|
||||
.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!()
|
||||
pub async fn set_light_state(
|
||||
&self,
|
||||
args: SetLightStateArgs,
|
||||
) -> Result<SetLightStateResponse, HandleError> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
self.sender
|
||||
.send(LB130USMessage::SetLightState(args, sender))
|
||||
.await
|
||||
.map_err(|_| HandleError::Dead)?;
|
||||
receiver
|
||||
.await
|
||||
.map_err(|_| HandleError::Dead)?
|
||||
.context(CommunicationSnafu)
|
||||
}
|
||||
}
|
||||
|
97
driver/kasa/src/impl_protocol.rs
Normal file
97
driver/kasa/src/impl_protocol.rs
Normal 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(())
|
||||
}
|
||||
}
|
@@ -1,2 +1,3 @@
|
||||
pub mod connection;
|
||||
mod impl_protocol;
|
||||
pub mod messages;
|
||||
|
@@ -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 mac_address::{MacAddress, MacParseError};
|
||||
use palette::{FromColor, Hsv};
|
||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
|
||||
use serde_repr::Deserialize_repr;
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
@@ -36,38 +37,38 @@ pub struct GetSysInfoResponseSystem {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CommonSysInfo {
|
||||
active_mode: ActiveMode,
|
||||
alias: String,
|
||||
ctrl_protocols: CtrlProtocols,
|
||||
description: String,
|
||||
dev_state: DevState,
|
||||
pub active_mode: ActiveMode,
|
||||
pub alias: String,
|
||||
pub ctrl_protocols: CtrlProtocols,
|
||||
pub description: String,
|
||||
pub dev_state: DevState,
|
||||
#[serde(rename = "deviceId")]
|
||||
device_id: DeviceId,
|
||||
disco_ver: String,
|
||||
err_code: i32, // No idea
|
||||
heapsize: u64, // No idea
|
||||
pub device_id: DeviceId,
|
||||
pub disco_ver: String,
|
||||
pub err_code: i32, // No idea
|
||||
pub 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,
|
||||
pub hw_id: HardwareId,
|
||||
pub hw_ver: String,
|
||||
pub is_color: IsColor,
|
||||
pub is_dimmable: IsDimmable,
|
||||
pub is_factory: bool,
|
||||
pub is_variable_color_temp: IsVariableColorTemp,
|
||||
pub light_state: LightState,
|
||||
pub mic_mac: MacAddressWithoutSeparators,
|
||||
pub mic_type: MicType,
|
||||
// model: Model,
|
||||
#[serde(rename = "oemId")]
|
||||
oem_id: OemId,
|
||||
preferred_state: Vec<PreferredStateChoice>,
|
||||
rssi: i32,
|
||||
sw_ver: String,
|
||||
pub oem_id: OemId,
|
||||
pub preferred_state: Vec<PreferredStateChoice>,
|
||||
pub rssi: i32,
|
||||
pub sw_ver: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LB130USSys {
|
||||
#[serde(flatten)]
|
||||
sys_info: CommonSysInfo,
|
||||
pub sys_info: CommonSysInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -78,9 +79,9 @@ pub enum SysInfo {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PreferredStateChoice {
|
||||
pub struct PreferredStateChoice {
|
||||
#[serde(flatten)]
|
||||
color: Color,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, SerializeDisplay, DeserializeFromStr)]
|
||||
@@ -162,9 +163,9 @@ enum IsVariableColorTemp {
|
||||
VariableColorTemp = 1,
|
||||
}
|
||||
|
||||
type Percentage = RangedU8<0, 100>;
|
||||
type Angle = RangedU16<0, 360>;
|
||||
type Kelvin = RangedU16<2500, 9000>;
|
||||
pub type Percentage = RangedU8<0, 100>;
|
||||
pub type Angle = RangedU16<0, 360>;
|
||||
pub type Kelvin = RangedU16<2500, 9000>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MaybeKelvin(Option<Kelvin>);
|
||||
@@ -198,13 +199,34 @@ struct RawColor {
|
||||
saturation: Percentage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Hsb {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Hsb {
|
||||
hue: Angle,
|
||||
saturation: 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)]
|
||||
struct KelvinWithBrightness {
|
||||
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)]
|
||||
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)]
|
||||
color: Color,
|
||||
mode: LightStateMode,
|
||||
on_off: OnOrOff,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -259,14 +355,6 @@ enum LightStateMode {
|
||||
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")]
|
||||
@@ -275,3 +363,59 @@ enum MicType {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
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
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@ license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
deranged = { workspace = true }
|
||||
ext-trait = "2.0.0"
|
||||
ext-trait = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
|
@@ -17,8 +17,7 @@ axum = { version = "0.8.1", default-features = false, features = [
|
||||
"http1",
|
||||
"tokio",
|
||||
] }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
deranged = { workspace = true, features = ["serde"] }
|
||||
driver-kasa = { path = "../driver/kasa" }
|
||||
emitter-and-signal = { path = "../emitter-and-signal" }
|
||||
|
@@ -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 home_assistant::{
|
||||
home_assistant::HomeAssistant, light::HomeAssistantLight, object_id::ObjectId,
|
||||
};
|
||||
use protocol::light::Light;
|
||||
use protocol::light::{IsOff, IsOn};
|
||||
use pyo3::prelude::*;
|
||||
use shadow_rs::shadow;
|
||||
use tokio::time::interval;
|
||||
use tracing::{level_filters::LevelFilter, Level};
|
||||
use tracing_appender::rolling::{self, RollingFileAppender};
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, format::FmtSpan},
|
||||
fmt::{self, fmt, format::FmtSpan},
|
||||
layer::SubscriberExt,
|
||||
registry,
|
||||
util::SubscriberInitExt,
|
||||
@@ -22,7 +24,53 @@ mod tracing_to_home_assistant;
|
||||
|
||||
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()
|
||||
.with(
|
||||
fmt::layer()
|
||||
@@ -31,14 +79,25 @@ async fn real_main(home_assistant: HomeAssistant) -> ! {
|
||||
.with_filter(LevelFilter::from_level(Level::TRACE)),
|
||||
)
|
||||
.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();
|
||||
|
||||
if let Some(error) = tracing_to_directory_initialization_error {
|
||||
tracing::error!(?error, "cannot trace to directory");
|
||||
}
|
||||
|
||||
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(),
|
||||
// object_id: ObjectId::from_str("jacob_s_lamp_side").unwrap(),
|
||||
// };
|
||||
|
||||
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;
|
||||
tracing::info!(?sysinfo_res, "got sys info");
|
||||
|
||||
let is_on = some_light.is_on().await;
|
||||
tracing::info!(?is_on);
|
||||
let is_off = some_light.is_off().await;
|
||||
tracing::info!(?is_off);
|
||||
|
||||
// let is_on = lamp.is_on().await;
|
||||
// tracing::info!(?is_on);
|
||||
// let is_off = lamp.is_off().await;
|
||||
@@ -71,8 +135,10 @@ async fn real_main(home_assistant: HomeAssistant) -> ! {
|
||||
|
||||
#[pyfunction]
|
||||
fn main<'py>(py: Python<'py>, home_assistant: HomeAssistant) -> PyResult<Bound<'py, PyAny>> {
|
||||
let args = Args::parse();
|
||||
|
||||
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async {
|
||||
real_main(home_assistant).await;
|
||||
real_main(args, home_assistant).await;
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -8,9 +8,8 @@ license = { workspace = true }
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[dependencies]
|
||||
arbitrary-value = { path = "../arbitrary-value" }
|
||||
arbitrary-value = { path = "../arbitrary-value", features = ["pyo3"] }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
derive_more = { workspace = true, features = [
|
||||
"display",
|
||||
"from",
|
||||
@@ -27,7 +26,7 @@ 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"] }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true }
|
||||
tracing = { optional = true, workspace = true }
|
||||
ulid = "1.2.0"
|
||||
|
@@ -4,105 +4,73 @@ use crate::{
|
||||
event::context::context::Context,
|
||||
state::{ErrorState, HomeAssistantState, UnexpectedState},
|
||||
};
|
||||
use arbitrary_value::arbitrary::Arbitrary;
|
||||
use protocol::light::Light;
|
||||
use protocol::light::{GetState, SetState};
|
||||
use pyo3::prelude::*;
|
||||
use python_utils::IsNone;
|
||||
use snafu::{ResultExt, Snafu};
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub enum IsStateError {
|
||||
pub enum GetStateError {
|
||||
GetStateObjectError { source: GetStateObjectError },
|
||||
Error { state: ErrorState },
|
||||
UnexpectedError { state: UnexpectedState },
|
||||
}
|
||||
|
||||
impl Light for HomeAssistantLight {
|
||||
type IsOnError = IsStateError;
|
||||
impl GetState for HomeAssistantLight {
|
||||
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 = state_object.state;
|
||||
|
||||
match state {
|
||||
HomeAssistantState::Ok(light_state) => Ok(matches!(light_state, LightState::On)),
|
||||
HomeAssistantState::Err(state) => Err(IsStateError::Error { state }),
|
||||
HomeAssistantState::Ok(light_state) => Ok(light_state.into()),
|
||||
HomeAssistantState::Err(error_state) => {
|
||||
Err(GetStateError::Error { state: error_state })
|
||||
}
|
||||
HomeAssistantState::UnexpectedErr(state) => {
|
||||
Err(IsStateError::UnexpectedError { state })
|
||||
Err(GetStateError::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!()
|
||||
}
|
||||
}
|
||||
|
||||
impl SetState for HomeAssistantLight {
|
||||
type Error = PyErr;
|
||||
|
||||
async fn set_state(&mut self, state: protocol::light::State) -> Result<(), Self::Error> {
|
||||
let context: Option<Context<()>> = None;
|
||||
let target: Option<()> = None;
|
||||
|
||||
let services = Python::with_gil(|py| self.home_assistant.services(py))?;
|
||||
|
||||
let _: IsNone = match state {
|
||||
protocol::light::State::Off => {
|
||||
services
|
||||
.call_service(
|
||||
TurnOff {
|
||||
entity_id: self.entity_id(),
|
||||
},
|
||||
context,
|
||||
target,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
protocol::light::State::On => {
|
||||
services
|
||||
.call_service(
|
||||
TurnOn {
|
||||
entity_id: self.entity_id(),
|
||||
},
|
||||
context,
|
||||
target,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -20,3 +20,21 @@ impl<'py> FromPyObject<'py> for LightState {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,16 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serde = ["dep:serde"]
|
||||
|
||||
[dependencies]
|
||||
deranged = { 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"] }
|
||||
|
@@ -1,43 +1,124 @@
|
||||
use std::{error::Error, future::Future};
|
||||
|
||||
use deranged::RangedU16;
|
||||
use snafu::{ResultExt, Snafu};
|
||||
|
||||
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, PartialEq, Eq, PartialOrd, Ord, Hash, strum::Display, strum::EnumIs,
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum State {
|
||||
Off,
|
||||
On,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, derive_more::From, derive_more::Into)]
|
||||
pub struct Kelvin(pub RangedU16<2000, 10000>);
|
||||
impl State {
|
||||
pub const fn invert(self) -> Self {
|
||||
match self {
|
||||
State::Off => State::On,
|
||||
State::On => State::Off,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait KelvinLight: Light {
|
||||
type TurnToKelvinError: Error;
|
||||
fn turn_to_kelvin(
|
||||
impl From<bool> for State {
|
||||
fn from(bool: bool) -> Self {
|
||||
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,
|
||||
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
|
||||
#[derive(Debug, Clone, Copy, derive_more::From, derive_more::Into)]
|
||||
pub struct Rgb(pub u8, pub u8, pub u8);
|
||||
pub type Oklch = palette::Oklch<f64>;
|
||||
|
||||
pub trait RgbLight: Light {
|
||||
type TurnToRgbError: Error;
|
||||
fn turn_to_rgb(
|
||||
pub trait TurnToColor {
|
||||
type Error: Error;
|
||||
fn turn_to_color(
|
||||
&mut self,
|
||||
color: Rgb,
|
||||
) -> impl Future<Output = Result<(), Self::TurnToRgbError>> + Send;
|
||||
color: Oklch,
|
||||
) -> impl Future<Output = Result<(), Self::Error>> + Send;
|
||||
}
|
||||
|
@@ -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
|
||||
pub fn detach<T>(bound: &Bound<T>) -> Py<T> {
|
||||
|
Reference in New Issue
Block a user