Compare commits
19 Commits
61bb3519ca
...
main
Author | SHA1 | Date | |
---|---|---|---|
10bceb55b8 | |||
e219edb64b | |||
da321db40b | |||
cff48691ef | |||
089e96b99f | |||
50e9ee43f7 | |||
c0b27dc5f0 | |||
277182a93e | |||
d6515521a4 | |||
472ca50ec0 | |||
e680f10be8 | |||
de3ab27414 | |||
c95d2f8d99 | |||
6e366a9c51 | |||
d1daa0bc01 | |||
cc51a262ae | |||
78e4be3fd9 | |||
fb0ad50954 | |||
fb8fb38611 |
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"
|
||||
|
11
Cargo.toml
11
Cargo.toml
@@ -10,14 +10,21 @@ members = [
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
license = "Unlicense"
|
||||
|
||||
[workspace.dependencies]
|
||||
backon = "1.5"
|
||||
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.
|
24
UNLICENSE
Normal file
24
UNLICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org/>
|
@@ -2,6 +2,7 @@
|
||||
name = "arbitrary-value"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = { workspace = true }
|
||||
|
||||
[features]
|
||||
pyo3 = ["dep:pyo3"]
|
||||
|
240
deny.toml
Normal file
240
deny.toml
Normal file
@@ -0,0 +1,240 @@
|
||||
# This template contains all of the possible sections and their default values
|
||||
|
||||
# Note that all fields that take a lint level have these possible values:
|
||||
# * deny - An error will be produced and the check will fail
|
||||
# * warn - A warning will be produced, but the check will not fail
|
||||
# * allow - No warning or error will be produced, though in some cases a note
|
||||
# will be
|
||||
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
# dependency, such as, for example, the `nix` crate only being used via the
|
||||
# `target_family = "unix"` configuration, that only having windows targets in
|
||||
# this list would mean the nix crate, as well as any of its exclusive
|
||||
# dependencies not shared by any other crates, would be ignored, as the target
|
||||
# list here is effectively saying which targets you are building for.
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Unlicense",
|
||||
"Zlib",
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.8
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# github.com organizations to allow git sources for
|
||||
github = []
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
@@ -2,13 +2,16 @@
|
||||
name = "driver-kasa"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
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
|
||||
}
|
||||
|
@@ -2,8 +2,9 @@
|
||||
name = "emitter-and-signal"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
deranged = { workspace = true }
|
||||
ext-trait = "2.0.0"
|
||||
ext-trait = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
|
@@ -2,6 +2,7 @@
|
||||
name = "smart-home-in-rust-with-home-assistant"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
license = { workspace = true }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
@@ -16,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;
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -2,14 +2,14 @@
|
||||
name = "home-assistant"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = { workspace = true }
|
||||
|
||||
[features]
|
||||
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",
|
||||
@@ -26,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,18 @@
|
||||
name = "protocol"
|
||||
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;
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
name = "python-utils"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
pyo3 = { workspace = true }
|
||||
|
@@ -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