split: initial (and bad)
All checks were successful
Build and release .deb / Release (push) Successful in 52s

This commit is contained in:
Alex Janka 2024-12-28 18:54:21 +11:00
parent 5d2d310e6e
commit 2c7aa8641c
29 changed files with 869 additions and 1808 deletions

View file

@ -3,7 +3,7 @@ name: Build and release .deb
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- "v*"
jobs:
Release:
@ -31,5 +31,5 @@ jobs:
with:
files: |-
./target/aarch64-unknown-linux-musl/debian/*.deb
./target/aarch64-unknown-linux-musl/release/tesla-charge-controller
./target/aarch64-unknown-linux-musl/release/charge-controller-supervisor
api_key: "${{secrets.PACKAGING_TOKEN}}"

578
Cargo.lock generated
View file

@ -144,32 +144,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "aws-lc-rs"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148"
dependencies = [
"aws-lc-sys",
"paste",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8478a5c29ead3f3be14aff8a202ad965cf7da6856860041bfca271becf8ba48b"
dependencies = [
"bindgen",
"cc",
"cmake",
"dunce",
"fs_extra",
"libc",
"paste",
]
[[package]]
name = "backtrace"
version = "0.3.74"
@ -185,15 +159,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "backtrace-ext"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
dependencies = [
"backtrace",
]
[[package]]
name = "base64"
version = "0.21.7"
@ -212,29 +177,6 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
[[package]]
name = "bindgen"
version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"bitflags 2.6.0",
"cexpr",
"clang-sys",
"itertools",
"lazy_static",
"lazycell",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn",
"which",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -250,15 +192,6 @@ dependencies = [
"serde",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -289,20 +222,9 @@ version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -315,6 +237,27 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "charge-controller-supervisor"
version = "1.9.9-pre"
dependencies = [
"chrono",
"clap",
"env_logger",
"eyre",
"futures",
"lazy_static",
"log",
"prometheus",
"rocket",
"serde",
"serde_json",
"serialport",
"tokio",
"tokio-modbus",
"tokio-serial",
]
[[package]]
name = "chrono"
version = "0.4.39"
@ -325,22 +268,10 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.23"
@ -381,41 +312,12 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cmake"
version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e"
dependencies = [
"cc",
]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "colored_json"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35980a1b846f8e3e359fd18099172a0857140ba9230affc4f71348081e039b6"
dependencies = [
"serde",
"serde_json",
"yansi",
]
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.18.1"
@ -427,24 +329,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
dependencies = [
"cookie",
"document-features",
"idna",
"log",
"publicsuffix",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "core-foundation"
version = "0.10.0"
@ -461,25 +345,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "deranged"
version = "0.3.11"
@ -489,28 +354,6 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_more"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "devise"
version = "0.4.2"
@ -544,16 +387,6 @@ dependencies = [
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@ -565,21 +398,6 @@ dependencies = [
"syn",
]
[[package]]
name = "document-features"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
dependencies = [
"litrs",
]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.13.0"
@ -634,6 +452,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@ -681,12 +509,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@ -798,16 +620,6 @@ dependencies = [
"windows",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
@ -876,15 +688,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "home"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "http"
version = "0.2.12"
@ -1226,6 +1029,12 @@ dependencies = [
"quote",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "2.7.0"
@ -1299,42 +1108,18 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[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.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.76"
@ -1371,28 +1156,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libloading"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
]
[[package]]
name = "libredox"
version = "0.1.3"
@ -1436,12 +1205,6 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lock_api"
version = "0.4.12"
@ -1506,49 +1269,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miette"
version = "7.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64"
dependencies = [
"backtrace",
"backtrace-ext",
"cfg-if",
"miette-derive",
"owo-colors",
"supports-color",
"supports-hyperlinks",
"supports-unicode",
"terminal_size",
"textwrap",
"thiserror 1.0.69",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "7.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.2"
@ -1627,16 +1353,6 @@ dependencies = [
"pin-utils",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "notify"
version = "7.0.0"
@ -1733,12 +1449,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
[[package]]
name = "parking_lot"
version = "0.12.3"
@ -1762,12 +1472,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pear"
version = "0.2.9"
@ -1809,17 +1513,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkce"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e228dbe2aebd82de09c914fe28d36d3170ed5192e8d52b9c070ee0794519c2d3"
dependencies = [
"base64 0.21.7",
"rand",
"sha2",
]
[[package]]
name = "pkg-config"
version = "0.3.31"
@ -1841,16 +1534,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.92"
@ -1894,22 +1577,6 @@ version = "2.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
[[package]]
name = "psl-types"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "publicsuffix"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
dependencies = [
"idna",
"psl-types",
]
[[package]]
name = "quinn"
version = "0.11.6"
@ -1920,7 +1587,7 @@ dependencies = [
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.0",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.9",
@ -1938,7 +1605,7 @@ dependencies = [
"getrandom",
"rand",
"ring",
"rustc-hash 2.1.0",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
@ -2082,8 +1749,6 @@ checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
dependencies = [
"base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
"futures-core",
"futures-util",
"http 1.2.0",
@ -2233,12 +1898,6 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.0"
@ -2264,8 +1923,6 @@ version = "0.23.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
@ -2298,7 +1955,6 @@ version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@ -2409,17 +2065,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -2505,55 +2150,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "supports-color"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
dependencies = [
"is_ci",
]
[[package]]
name = "supports-hyperlinks"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
[[package]]
name = "supports-unicode"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "2.0.91"
@ -2598,23 +2200,14 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "terminal_size"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
dependencies = [
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "tesla-charge-controller"
version = "1.5.2"
version = "1.9.9-pre"
dependencies = [
"chrono",
"clap",
"env_logger",
"eyre",
"if_chain",
"include_dir",
"lazy_static",
@ -2628,59 +2221,12 @@ dependencies = [
"serde",
"serde_json",
"serialport",
"tesla-common",
"thiserror 2.0.9",
"tokio",
"tokio-modbus",
"tokio-serial",
]
[[package]]
name = "tesla-common"
version = "0.3.1"
source = "git+https://git.alexjanka.com/alex/tesla-common#74859ebe6f1c96ae6a62d9156beffd8df46dea37"
dependencies = [
"ron",
"serde",
"teslatte",
"thiserror 1.0.69",
]
[[package]]
name = "teslatte"
version = "0.1.16"
source = "git+https://git.alexjanka.com/alex/teslatte#416f3a20ee3e8dcf540b8196a836acd671b2d0c3"
dependencies = [
"chrono",
"clap",
"colored_json",
"derive_more",
"miette",
"pkce",
"rand",
"reqwest",
"rustls",
"serde",
"serde_json",
"strum",
"thiserror 2.0.9",
"tokio",
"tracing",
"tracing-subscriber",
"url",
"urlencoding",
]
[[package]]
name = "textwrap"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
dependencies = [
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@ -2988,12 +2534,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ubyte"
version = "0.10.4"
@ -3028,24 +2568,6 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
version = "0.2.6"
@ -3069,12 +2591,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf16_iter"
version = "1.0.5"
@ -3226,18 +2742,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix",
]
[[package]]
name = "winapi"
version = "0.3.9"

View file

@ -1,60 +1,12 @@
[package]
name = "tesla-charge-controller"
version = "1.5.2"
edition = "2021"
license = "MITNFA"
description = "Controls Tesla charge rate based on solar charge data"
authors = ["Alex Janka"]
[workspace]
members = ["charge-controller-supervisor", "tesla-charge-controller"]
default-members = ["charge-controller-supervisor"]
resolver = "2"
[lib]
name = "common"
path = "src/lib/lib.rs"
[workspace.package]
version = "1.9.9-pre"
[[bin]]
name = "tesla-charge-controller"
path = "src/bins/tesla/main.rs"
[[bin]]
name = "charge-controller-supervisor"
path = "src/bins/controllers/main.rs"
[package.metadata.deb]
maintainer-scripts = "pkg/debian/"
systemd-units = [
{ unit-name = "tesla-charge-controller", unit-scripts = "pkg/systemd/", enable = false },
{ unit-name = "charge-controller-supervisor", unit-scripts = "pkg/systemd/", enable = false },
]
depends = ""
[dependencies]
tesla-common = { git = "https://git.alexjanka.com/alex/tesla-common" }
clap = { version = "4.5.23", features = ["derive"] }
ron = "0.8.1"
serde = { version = "1.0.216", features = ["derive"] }
serde_json = "1.0.134"
tokio = { version = "1.42.0", features = ["full"] }
thiserror = "2.0.9"
rocket = { version = "0.5.1", features = ["json"] }
include_dir = "0.7.4"
chrono = "0.4.39"
prometheus = "0.13.4"
env_logger = "0.11.6"
log = "0.4.22"
serialport = "4.6.1"
tokio-modbus = "0.16.1"
tokio-serial = "5.4.4"
if_chain = "1.0.2"
notify-debouncer-mini = { version = "0.5.0", default-features = false }
lazy_static = "1.5.0"
rand = "0.8.5"
reqwest = { version = "0.12.9", default-features = false, features = [
"rustls-tls",
] }
# [patch."https://git.alexjanka.com/alex/tesla-common"]
# tesla-common = { path = "../tesla-common" }
[lints.clippy]
[workspace.lints.clippy]
pedantic = "warn"
module-name-repetitions = { level = "allow", priority = 1 }
struct-excessive-bools = { level = "allow", priority = 1 }
@ -62,3 +14,5 @@ too-many-lines = { level = "allow", priority = 1 }
default-trait-access = { level = "allow", priority = 1 }
cast-precision-loss = { level = "allow", priority = 1 }
cast-possible-truncation = { level = "allow", priority = 1 }
missing-errors-doc = { level = "allow", priority = 1 }
missing-panics-doc = { level = "allow", priority = 1 }

View file

@ -0,0 +1,32 @@
[package]
name = "charge-controller-supervisor"
version.workspace = true
edition = "2021"
license = "MITNFA"
description = "Monitors multiple charge controllers"
authors = ["Alex Janka"]
[package.metadata.deb]
maintainer-scripts = "debian/"
systemd-units = { enable = false }
depends = ""
[dependencies]
clap = { version = "4.5.23", features = ["derive"] }
serde = { version = "1.0.216", features = ["derive"] }
serde_json = "1.0.134"
tokio = { version = "1.42.0", features = ["full"] }
rocket = { version = "0.5.1", features = ["json"] }
chrono = "0.4.39"
prometheus = "0.13.4"
env_logger = "0.11.6"
log = "0.4.22"
serialport = "4.6.1"
futures = "0.3.31"
tokio-modbus = "0.16.1"
tokio-serial = "5.4.4"
lazy_static = "1.5.0"
eyre = "0.6.12"
[lints]
workspace = true

View file

@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
#[serde(default)]
pub struct Config {
pub primary_charge_controller: String,
pub charge_controllers: Vec<ChargeControllerConfig>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum ChargeControllerConfig {
Pl {
serial_port: String,
baud_rate: u32,
timeout_milliseconds: u64,
watch_interval_seconds: u64,
},
Tristar {
serial_port: String,
baud_rate: u32,
watch_interval_seconds: u64,
},
}
impl ChargeControllerConfig {
pub fn interval(&self) -> u64 {
match self {
ChargeControllerConfig::Pl {
serial_port: _,
baud_rate: _,
timeout_milliseconds: _,
watch_interval_seconds,
}
| ChargeControllerConfig::Tristar {
serial_port: _,
baud_rate: _,
watch_interval_seconds,
} => *watch_interval_seconds,
}
}
}

View file

@ -0,0 +1,160 @@
use clap::Parser;
use futures::StreamExt;
use std::path::PathBuf;
mod config;
#[derive(clap::Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[command(subcommand)]
command: Commands,
#[clap(long, default_value = "/etc/charge-controller-supervisor/config.json")]
config: PathBuf,
}
#[derive(clap::Subcommand, Debug, Clone)]
enum Commands {
/// Run charge controller server
Watch,
/// Print the default config file
GenerateConfig,
}
mod gauges;
mod pl;
mod tristar;
mod web;
pub const CHARGE_CONTROLLER_LABEL: &str = "charge_controller";
pub const PL_LABEL: &str = "pl_device";
pub const TRISTAR_LABEL: &str = "tristar_device";
#[tokio::main]
async fn main() {
env_logger::builder()
.format_module_path(false)
.format_timestamp(
if std::env::var("LOG_TIMESTAMP").is_ok_and(|v| v == "false") {
None
} else {
Some(env_logger::TimestampPrecision::Seconds)
},
)
.init();
if let Err(e) = run().await {
log::error!("{e:?}");
std::process::exit(1);
}
}
enum Controller {
Pl(pl::Pli),
Tristar(tristar::Tristar),
}
impl Controller {
async fn refresh(&mut self) -> eyre::Result<()> {
match self {
Controller::Pl(pli) => {
pli.refresh()?;
}
Controller::Tristar(tristar) => {
tristar.refresh().await?;
}
}
Ok(())
}
fn name(&self) -> String {
match self {
Controller::Pl(pli) => pli.name().to_owned(),
Controller::Tristar(tristar) => tristar.name().to_owned(),
}
}
}
impl config::ChargeControllerConfig {
fn connect(&self) -> eyre::Result<Controller> {
match self {
config::ChargeControllerConfig::Pl {
serial_port,
baud_rate,
timeout_milliseconds,
watch_interval_seconds: _,
} => {
let pl = pl::Pli::new(serial_port.to_owned(), *baud_rate, *timeout_milliseconds)?;
Ok(Controller::Pl(pl))
}
config::ChargeControllerConfig::Tristar {
serial_port,
baud_rate,
watch_interval_seconds: _,
} => {
let tristar = tristar::Tristar::new(serial_port.to_owned(), *baud_rate)?;
Ok(Controller::Tristar(tristar))
}
}
}
}
async fn run() -> eyre::Result<()> {
let args = Args::parse();
match args.command {
Commands::Watch => watch(args).await,
Commands::GenerateConfig => {
let config = config::Config::default();
let json = serde_json::to_string_pretty(&config)?;
println!("{json}");
Ok(())
}
}
}
async fn watch(args: Args) -> eyre::Result<()> {
let config: config::Config = serde_json::from_reader(std::fs::File::open(args.config)?)?;
let mut controllers = futures::stream::FuturesUnordered::new();
for controller in config.charge_controllers {
match controller.connect() {
Ok(v) => controllers.push(run_loop(v, controller.interval())),
Err(e) => log::error!("couldn't connect to {controller:?}: {e:?}"),
}
}
let server = web::rocket();
let server_task = tokio::task::spawn(server.launch());
tokio::select! {
v = controllers.next() => {
match v {
Some(Err(e)) => {
log::error!("{e:?}");
std::process::exit(1);
}
_ => {
log::error!("no controller tasks left???");
}
}
}
v = server_task => {
log::error!("server exited: {v:#?}");
}
}
std::process::exit(1)
}
async fn run_loop(mut controller: Controller, timeout_interval: u64) -> eyre::Result<()> {
let mut timeout = tokio::time::interval(std::time::Duration::from_secs(timeout_interval));
loop {
timeout.tick().await;
if let Err(e) = controller.refresh().await {
log::warn!("error reading controller {}: {e:?}", controller.name());
}
}
}

View file

@ -8,12 +8,9 @@ use chrono::Timelike;
use serde::{Deserialize, Serialize};
use serialport::SerialPort;
use crate::{
charge_controllers::gauges::{
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, INPUT_CURRENT, PL_DUTY_CYCLE, PL_LOAD_CURRENT,
TARGET_VOLTAGE,
},
errors::{PliError, PrintErrors},
use crate::gauges::{
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, INPUT_CURRENT, PL_DUTY_CYCLE, PL_LOAD_CURRENT,
TARGET_VOLTAGE,
};
pub struct Pli {
@ -104,6 +101,7 @@ fn set_regulator_gauges(state: RegulatorState, label: &str) {
}
}
#[expect(dead_code, reason = "writing settings is not yet implemented")]
#[derive(Debug, Clone, Copy)]
pub enum PliRequest {
ReadRam(u8),
@ -129,83 +127,66 @@ impl Pli {
})
}
pub fn refresh(&mut self) {
if let Some(new_state) = self.read_state().some_or_print_with("reading pl state") {
BATTERY_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.battery_voltage);
TARGET_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.target_voltage);
PL_DUTY_CYCLE
.with_label_values(&[&self.port_name])
.set(new_state.duty_cycle);
INPUT_CURRENT
.with_label_values(&[&self.port_name])
.set(new_state.internal_charge_current);
PL_LOAD_CURRENT
.with_label_values(&[&self.port_name])
.set(new_state.internal_load_current);
BATTERY_TEMP
.with_label_values(&[&self.port_name])
.set(new_state.battery_temp);
pub fn refresh(&mut self) -> eyre::Result<()> {
let new_state = self.read_state()?;
BATTERY_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.battery_voltage);
TARGET_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.target_voltage);
PL_DUTY_CYCLE
.with_label_values(&[&self.port_name])
.set(new_state.duty_cycle);
INPUT_CURRENT
.with_label_values(&[&self.port_name])
.set(new_state.internal_charge_current);
PL_LOAD_CURRENT
.with_label_values(&[&self.port_name])
.set(new_state.internal_load_current);
BATTERY_TEMP
.with_label_values(&[&self.port_name])
.set(new_state.battery_temp);
set_regulator_gauges(new_state.regulator_state, &self.port_name);
set_regulator_gauges(new_state.regulator_state, &self.port_name);
*self.state.write().expect("PLI state handler panicked!!") = new_state;
}
*self.state.write().expect("PLI state handler panicked!!") = new_state;
Ok(())
}
pub fn process_request(&mut self, message: PliRequest) {
#[expect(dead_code, reason = "writing settings is not yet implemented")]
pub fn process_request(&mut self, message: PliRequest) -> eyre::Result<()> {
match message {
PliRequest::ReadRam(address) => {
if let Some(data) = self.read_ram(address).some_or_print_with("reading pl ram") {
log::warn!("Read RAM at {address}: data {data}");
}
let data = self.read_ram(address)?;
log::warn!("Read RAM at {address}: data {data}");
}
PliRequest::ReadEeprom(address) => {
if let Some(data) = self
.read_eeprom(address)
.some_or_print_with("reading pl eeprom")
{
log::warn!("Read EEPROM at {address}: data {data}");
}
let data = self.read_eeprom(address)?;
log::warn!("Read EEPROM at {address}: data {data}");
}
PliRequest::SyncTime => {
let now = chrono::Local::now();
let timestamp = (((now.hour() * 10) + (now.minute() / 6)) & 0xFF) as u8;
let min = (now.minute() % 6) as u8;
let sec = (now.second() / 2).min(29) as u8;
if self
.write_ram(PlRamAddress::Hour, timestamp)
.some_or_print_with("Setting time")
.is_none()
{
return;
}
if self
.write_ram(PlRamAddress::Min, min)
.some_or_print_with("Setting time (minutes)")
.is_none()
{
return;
}
let _ = self
.write_ram(PlRamAddress::Sec, sec)
.some_or_print_with("Setting time (seconds)");
self.write_ram(PlRamAddress::Hour, timestamp)?;
self.write_ram(PlRamAddress::Min, min)?;
self.write_ram(PlRamAddress::Sec, sec)?;
log::warn!(
"Set time: {now} corresponds to {timestamp} + minutes {min} + seconds {sec}"
);
}
PliRequest::SetRegulatorState(state) => {
log::warn!("Setting regulator state to {state:?}");
self.write_ram(PlRamAddress::Rstate, state.into())
.some_or_print_with("setting regulator state");
self.write_ram(PlRamAddress::Rstate, state.into())?;
}
}
Ok(())
}
fn read_state(&mut self) -> Result<PlState, PliError> {
fn read_state(&mut self) -> eyre::Result<PlState> {
Ok(PlState {
battery_voltage: f64::from(self.read_ram(PlRamAddress::Batv)?) * (4. / 10.),
target_voltage: f64::from(self.read_ram(PlRamAddress::Vreg)?) * (4. / 10.),
@ -217,13 +198,13 @@ impl Pli {
})
}
fn send_command(&mut self, req: [u8; 4]) -> Result<(), PliError> {
fn send_command(&mut self, req: [u8; 4]) -> eyre::Result<()> {
self.flush()?;
self.port.write_all(&req)?;
Ok(())
}
fn flush(&mut self) -> Result<(), PliError> {
fn flush(&mut self) -> eyre::Result<()> {
self.port.flush()?;
while let Ok(num) = self.port.bytes_to_read() {
if num == 0 {
@ -234,13 +215,13 @@ impl Pli {
Ok(())
}
fn receive<const LENGTH: usize>(&mut self) -> Result<[u8; LENGTH], PliError> {
fn receive<const LENGTH: usize>(&mut self) -> eyre::Result<[u8; LENGTH]> {
let mut buf = [0; LENGTH];
self.port.read_exact(&mut buf)?;
Ok(buf)
}
fn read_ram<T>(&mut self, address: T) -> Result<u8, PliError>
fn read_ram<T>(&mut self, address: T) -> eyre::Result<u8>
where
T: Into<u8>,
{
@ -249,11 +230,11 @@ impl Pli {
if buf[0] == 200 {
Ok(self.receive::<1>()?[0])
} else {
Err(PliError::ReadError(buf[0]))
Err(eyre::eyre!("read error: result is {}", buf[0]))
}
}
fn write_ram<T>(&mut self, address: T, data: u8) -> Result<(), PliError>
fn write_ram<T>(&mut self, address: T, data: u8) -> eyre::Result<()>
where
T: Into<u8>,
{
@ -261,7 +242,7 @@ impl Pli {
Ok(())
}
fn read_eeprom<T>(&mut self, address: T) -> Result<u8, PliError>
fn read_eeprom<T>(&mut self, address: T) -> eyre::Result<u8>
where
T: Into<u8>,
{
@ -270,9 +251,13 @@ impl Pli {
if buf[0] == 200 {
Ok(self.receive::<1>()?[0])
} else {
Err(PliError::ReadError(buf[0]))
Err(eyre::eyre!("read error: result is {}", buf[0]))
}
}
pub fn name(&self) -> &str {
&self.port_name
}
}
enum PlRamAddress {

View file

@ -1,14 +1,10 @@
use prometheus::core::{AtomicI64, GenericGauge};
use tokio_modbus::client::Reader;
use crate::{
charge_controllers::gauges::{
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, INPUT_CURRENT, TARGET_VOLTAGE,
TRISTAR_CHARGE_CURRENT, TRISTAR_INPUT_VOLTAGE, TRISTAR_MAX_ARRAY_POWER,
TRISTAR_MAX_ARRAY_VOLTAGE, TRISTAR_OPEN_CIRCUIT_VOLTAGE, TRISTAR_POWER_IN,
TRISTAR_POWER_OUT,
},
errors::{PrintErrors, TristarError},
use crate::gauges::{
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, INPUT_CURRENT, TARGET_VOLTAGE,
TRISTAR_CHARGE_CURRENT, TRISTAR_INPUT_VOLTAGE, TRISTAR_MAX_ARRAY_POWER,
TRISTAR_MAX_ARRAY_VOLTAGE, TRISTAR_OPEN_CIRCUIT_VOLTAGE, TRISTAR_POWER_IN, TRISTAR_POWER_OUT,
};
const DEVICE_ID: u8 = 0x01;
@ -238,7 +234,7 @@ impl ChargeStateGauges {
}
impl Tristar {
pub fn new(serial_port: String, baud_rate: u32) -> Result<Self, TristarError> {
pub fn new(serial_port: String, baud_rate: u32) -> eyre::Result<Self> {
let modbus_serial =
tokio_serial::SerialStream::open(&tokio_serial::new(&serial_port, baud_rate))?;
let slave = tokio_modbus::Slave(DEVICE_ID);
@ -253,53 +249,54 @@ impl Tristar {
})
}
pub async fn refresh(&mut self) {
if let Some(new_state) = self
.get_data()
.await
.some_or_print_with("reading tristar state")
{
self.consecutive_errors = 0;
BATTERY_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.battery_voltage);
TARGET_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.target_voltage);
INPUT_CURRENT
.with_label_values(&[&self.port_name])
.set(new_state.input_current);
BATTERY_TEMP
.with_label_values(&[&self.port_name])
.set(new_state.battery_temp.into());
TRISTAR_INPUT_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.tristar_input_voltage);
TRISTAR_CHARGE_CURRENT
.with_label_values(&[&self.port_name])
.set(new_state.tristar_charge_current);
TRISTAR_POWER_OUT
.with_label_values(&[&self.port_name])
.set(new_state.tristar_power_out);
TRISTAR_POWER_IN
.with_label_values(&[&self.port_name])
.set(new_state.tristar_power_in);
TRISTAR_MAX_ARRAY_POWER
.with_label_values(&[&self.port_name])
.set(new_state.tristar_max_array_power);
TRISTAR_MAX_ARRAY_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.tristar_max_array_voltage);
TRISTAR_OPEN_CIRCUIT_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.tristar_open_circuit_voltage);
pub async fn refresh(&mut self) -> eyre::Result<()> {
let new_state = self.get_data().await?;
self.consecutive_errors = 0;
BATTERY_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.battery_voltage);
TARGET_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.target_voltage);
INPUT_CURRENT
.with_label_values(&[&self.port_name])
.set(new_state.input_current);
BATTERY_TEMP
.with_label_values(&[&self.port_name])
.set(new_state.battery_temp.into());
TRISTAR_INPUT_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.tristar_input_voltage);
TRISTAR_CHARGE_CURRENT
.with_label_values(&[&self.port_name])
.set(new_state.tristar_charge_current);
TRISTAR_POWER_OUT
.with_label_values(&[&self.port_name])
.set(new_state.tristar_power_out);
TRISTAR_POWER_IN
.with_label_values(&[&self.port_name])
.set(new_state.tristar_power_in);
TRISTAR_MAX_ARRAY_POWER
.with_label_values(&[&self.port_name])
.set(new_state.tristar_max_array_power);
TRISTAR_MAX_ARRAY_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.tristar_max_array_voltage);
TRISTAR_OPEN_CIRCUIT_VOLTAGE
.with_label_values(&[&self.port_name])
.set(new_state.tristar_open_circuit_voltage);
self.charge_state_gauges.set(new_state.charge_state);
self.state = new_state;
}
self.charge_state_gauges.set(new_state.charge_state);
self.state = new_state;
Ok(())
}
async fn get_data(&mut self) -> Result<TristarState, TristarError> {
pub fn name(&self) -> &str {
&self.port_name
}
async fn get_data(&mut self) -> eyre::Result<TristarState> {
let data = self
.modbus
.read_holding_registers(0x0000, RAM_DATA_SIZE + 1)

View file

@ -0,0 +1,31 @@
use rocket::{get, routes};
pub fn rocket() -> rocket::Rocket<rocket::Build> {
rocket::build().mount("/", routes![metrics,])
}
#[get("/metrics")]
fn metrics() -> Result<String, ServerError> {
Ok(
prometheus::TextEncoder::new()
.encode_to_string(&prometheus::default_registry().gather())?,
)
}
enum ServerError {
Prometheus,
}
impl From<prometheus::Error> for ServerError {
fn from(_: prometheus::Error) -> Self {
Self::Prometheus
}
}
impl<'a> rocket::response::Responder<'a, 'a> for ServerError {
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
Err(match self {
ServerError::Prometheus => rocket::http::Status::InternalServerError,
})
}
}

View file

View file

@ -1 +0,0 @@
fn main() {}

View file

@ -1 +0,0 @@
fn main() {}

View file

@ -1,263 +0,0 @@
use lazy_static::lazy_static;
use prometheus::{
core::{AtomicI64, GenericGauge},
register_gauge, register_int_gauge, register_int_gauge_vec, Gauge, IntGauge, IntGaugeVec,
};
use std::sync::{Arc, RwLock};
mod http;
use crate::types::{CarState, ChargingState};
lazy_static! {
pub static ref BATTERY_LEVEL: IntGauge =
register_int_gauge!("tesla_battery_level", "Battery level",).unwrap();
pub static ref CHARGE_RATE: Gauge =
register_gauge!("tesla_charge_rate", "Charge rate",).unwrap();
pub static ref CHARGE_REQUEST: IntGauge =
register_int_gauge!("tesla_charge_request", "Requested charge rate",).unwrap();
pub static ref CHARGE_ENABLE_REQUEST: IntGauge =
register_int_gauge!("tesla_charge_enable_request", "Charge enable request",).unwrap();
pub static ref CHARGER_CONNECTED: IntGauge =
register_int_gauge!("tesla_charger_connected", "Charger connected",).unwrap();
pub static ref INSIDE_TEMP: Gauge =
register_gauge!("tesla_inside_temp", "Inside temperature",).unwrap();
pub static ref OUTSIDE_TEMP: Gauge =
register_gauge!("tesla_outside_temp", "Outside temperature",).unwrap();
pub static ref BATTERY_HEATER: IntGauge =
register_int_gauge!("tesla_battery_heater", "Battery heater",).unwrap();
pub static ref CLIMATE_ON: IntGauge =
register_int_gauge!("tesla_climate_on", "Climate control",).unwrap();
pub static ref PRECONDITIONING: IntGauge =
register_int_gauge!("tesla_preconditioning", "Preconditioning",).unwrap();
pub static ref REMOTE_HEATER_CONTROL_ENABLED: IntGauge = register_int_gauge!(
"tesla_remote_heater_control_enabled",
"Remote heater control enabled",
)
.unwrap();
pub static ref IS_AUTO_CONDITIONING_ON: IntGauge =
register_int_gauge!("tesla_is_auto_conditioning_on", "Auto conditioning on",).unwrap();
pub static ref DRIVER_TEMP_SETTING: Gauge =
register_gauge!("tesla_driver_temp_setting", "Driver temp",).unwrap();
pub static ref PASSENGER_TEMP_SETTING: Gauge =
register_gauge!("tesla_passenger_temp_setting", "Passenger temp",).unwrap();
pub static ref TESLA_ONLINE: IntGauge =
register_int_gauge!("tesla_online", "Tesla online",).unwrap();
pub static ref HOME: IntGauge = register_int_gauge!("tesla_home", "Is home",).unwrap();
pub static ref SENTRY_MODE: IntGauge =
register_int_gauge!("tesla_sentry_mode", "Sentry mode",).unwrap();
pub static ref SENTRY_MODE_AVAILABLE: IntGauge =
register_int_gauge!("tesla_sentry_mode_available", "Sentry mode available",).unwrap();
pub static ref CHARGER_ACTUAL_CURRENT: IntGauge =
register_int_gauge!("tesla_charger_actual_current", "Charger actual current",).unwrap();
pub static ref CHARGER_PHASES: IntGauge =
register_int_gauge!("tesla_charger_phases", "Charger phases",).unwrap();
pub static ref CHARGER_PILOT_CURRENT: IntGauge =
register_int_gauge!("tesla_charger_pilot_current", "Charger pilot current",).unwrap();
pub static ref CHARGER_POWER: IntGauge =
register_int_gauge!("tesla_charger_power", "Charger power",).unwrap();
pub static ref CHARGER_VOLTAGE: IntGauge =
register_int_gauge!("tesla_charger_voltage", "Charger voltage",).unwrap();
pub static ref CHARGING_STATE: IntGaugeVec =
register_int_gauge_vec!("tesla_charging_state", "Tesla charging state", &["state"])
.unwrap();
pub static ref CABIN_OVERHEAT_PROTECTION: IntGaugeVec = register_int_gauge_vec!(
"tesla_cabin_overheat_protection_state",
"Cabin overheat protection state",
&["state"]
)
.unwrap();
pub static ref HVAC_AUTO: IntGaugeVec =
register_int_gauge_vec!("tesla_hvac_auto_request", "HVAC auto", &["state"]).unwrap();
}
struct ChargingStateGauges {
charging: GenericGauge<AtomicI64>,
stopped: GenericGauge<AtomicI64>,
disconnected: GenericGauge<AtomicI64>,
complete: GenericGauge<AtomicI64>,
other: GenericGauge<AtomicI64>,
unavailable: GenericGauge<AtomicI64>,
}
impl ChargingStateGauges {
fn new() -> Self {
let charging = CHARGING_STATE.with_label_values(&["charging"]);
let stopped = CHARGING_STATE.with_label_values(&["stopped"]);
let disconnected = CHARGING_STATE.with_label_values(&["disconnected"]);
let complete = CHARGING_STATE.with_label_values(&["complete"]);
let other = CHARGING_STATE.with_label_values(&["other"]);
let unavailable = CHARGING_STATE.with_label_values(&["unavailable"]);
Self {
charging,
stopped,
disconnected,
complete,
other,
unavailable,
}
}
fn set(&mut self, state: ChargingState) {
match state {
ChargingState::Charging => {
self.charging.set(1);
self.stopped.set(0);
self.disconnected.set(0);
self.complete.set(0);
self.other.set(0);
self.unavailable.set(0);
}
ChargingState::Stopped => {
self.charging.set(0);
self.stopped.set(1);
self.disconnected.set(0);
self.complete.set(0);
self.other.set(0);
self.unavailable.set(0);
}
ChargingState::Disconnected => {
self.charging.set(0);
self.stopped.set(0);
self.disconnected.set(1);
self.complete.set(0);
self.other.set(0);
self.unavailable.set(0);
}
ChargingState::Other => {
self.charging.set(0);
self.stopped.set(0);
self.disconnected.set(0);
self.complete.set(0);
self.other.set(1);
self.unavailable.set(0);
}
ChargingState::Complete => {
self.charging.set(0);
self.stopped.set(0);
self.disconnected.set(0);
self.complete.set(1);
self.other.set(0);
self.unavailable.set(0);
}
ChargingState::Unavailable => {
self.charging.set(0);
self.stopped.set(0);
self.disconnected.set(0);
self.complete.set(0);
self.other.set(0);
self.unavailable.set(1);
}
}
}
}
pub struct TeslaInterface {
pub state: Arc<RwLock<CarState>>,
vehicle: http::Vehicle,
charging_state: ChargingStateGauges,
}
#[derive(Clone, Debug)]
// these are the messages that the webserver can send the api thread
pub enum InterfaceRequest {
FlashLights,
StopCharge,
SetChargeRate(i64),
}
impl TeslaInterface {
pub fn new(vin: &str) -> Self {
let vehicle = http::Vehicle::new(vin);
Self {
state: Arc::new(RwLock::new(Default::default())),
vehicle,
charging_state: ChargingStateGauges::new(),
}
}
pub async fn process_request(&mut self, request: InterfaceRequest) {
match request {
InterfaceRequest::FlashLights => {
let _ = self.vehicle.flash_lights().await;
}
InterfaceRequest::StopCharge => match self.vehicle.charge_stop().await {
Ok(()) => log::warn!("Successfully stopped charge"),
Err(e) => log::error!("Error stopping charge: {e:?}"),
},
InterfaceRequest::SetChargeRate(charging_amps) => {
match self.vehicle.set_charging_amps(charging_amps).await {
Ok(()) => {}
Err(e) => log::error!("Error setting charge rate: {e:?}"),
}
}
}
}
pub async fn refresh(&mut self) {
if let Some(new_charge_state) = self.get_state().await {
TESLA_ONLINE.set(1);
let mut state = self
.state
.write()
.expect("Tesla API state handler panicked!!");
BATTERY_LEVEL.set(new_charge_state.battery_level);
CHARGE_RATE.set(new_charge_state.charge_rate);
CHARGE_REQUEST.set(new_charge_state.charge_current_request);
CHARGE_ENABLE_REQUEST.set(bi(new_charge_state.charge_enable_request));
CHARGER_CONNECTED.set(bi(
new_charge_state.charging_state != ChargingState::Disconnected
));
self.charging_state.set(new_charge_state.charging_state);
if let Some(v) = new_charge_state.charger_actual_current {
CHARGER_ACTUAL_CURRENT.set(v);
}
if let Some(v) = new_charge_state.charger_phases {
CHARGER_PHASES.set(v);
}
if let Some(v) = new_charge_state.charger_pilot_current {
CHARGER_PILOT_CURRENT.set(v);
}
if let Some(v) = new_charge_state.charger_power {
CHARGER_POWER.set(v);
}
if let Some(v) = new_charge_state.charger_voltage {
CHARGER_VOLTAGE.set(v);
}
state.charge_state = Some(new_charge_state);
} else {
TESLA_ONLINE.set(0);
self.charging_state.set(ChargingState::Unavailable);
match self.state.write() {
Ok(mut state) => {
state.charge_state = None;
}
Err(e) => {
log::error!("failed to lock state: {e:?}");
}
}
}
}
pub async fn get_state(&self) -> Option<crate::types::ChargeState> {
match self.vehicle.charge_data().await {
Ok(v) => Some(v),
Err(e) => {
if let crate::errors::TeslaError::Reqwest(e) = &e {
if e.is_timeout() {
return None;
}
}
log::error!("error fetching charge data: {e:?}");
None
}
}
}
}
fn bi(value: bool) -> i64 {
i64::from(value)
}

View file

@ -1,7 +0,0 @@
pub mod gauges;
pub mod pl;
pub mod tristar;
pub const CHARGE_CONTROLLER_LABEL: &str = "charge_controller";
pub const PL_LABEL: &str = "pl_device";
pub const TRISTAR_LABEL: &str = "tristar_device";

View file

@ -1,223 +0,0 @@
use std::{
ops::DerefMut,
path::{Path, PathBuf},
sync::{OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
time::Duration,
};
use notify_debouncer_mini::{new_debouncer, notify::RecommendedWatcher, Debouncer};
use serde::{Deserialize, Serialize};
use tokio::task::JoinHandle;
use crate::errors::{ConfigError, PrintErrors};
static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
pub(super) static CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
pub fn access_config<'a>() -> RwLockReadGuard<'a, Config> {
CONFIG.get().unwrap().read().unwrap()
}
pub(super) struct ConfigWatcher {
_debouncer: Debouncer<RecommendedWatcher>,
_handle: JoinHandle<()>,
}
impl ConfigWatcher {
pub fn new(path: &Path) -> Option<Self> {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let mut debouncer = new_debouncer(Duration::from_secs(1), move |v| {
tx.send(v)
.some_or_print_with("Failed to send event to queue");
})
.ok()?;
debouncer
.watcher()
.watch(
path,
notify_debouncer_mini::notify::RecursiveMode::NonRecursive,
)
.ok()?;
let config_path = PathBuf::from(path);
let handle = tokio::task::spawn(async move {
loop {
match rx.recv().await {
Some(Ok(_event)) => {
let mut config = Config::load(&config_path);
config.validate();
if let Some(mut c) = CONFIG.get().and_then(|v| v.write().ok()) {
*c = config;
} else {
log::error!("Reloading config: got notified, but failed to lock");
}
}
Some(Err(e)) => log::error!("Error {e:?} from watcher"),
None => {}
}
}
});
Some(Self {
_debouncer: debouncer,
_handle: handle,
})
}
}
pub fn init_config(path: PathBuf) -> (Config, Option<ConfigWatcher>) {
log::trace!("loading config...");
let config = Config::load_and_save_defaults(&path);
log::trace!("watching config for changes...");
let config_watcher = ConfigWatcher::new(&path);
let _ = CONFIG_PATH.get_or_init(|| path);
(config, config_watcher)
}
pub struct ConfigHandle<'a> {
handle: RwLockWriteGuard<'a, Config>,
}
impl<'a> core::ops::Deref for ConfigHandle<'a> {
type Target = RwLockWriteGuard<'a, Config>;
fn deref(&self) -> &Self::Target {
&self.handle
}
}
impl DerefMut for ConfigHandle<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.handle
}
}
impl Drop for ConfigHandle<'_> {
fn drop(&mut self) {
let _ = self.save().some_or_print_with("saving config");
}
}
pub fn write_to_config<'a>() -> ConfigHandle<'a> {
ConfigHandle {
handle: CONFIG.get().unwrap().write().unwrap(),
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default)]
pub struct Config {
pub car_vin: String,
pub control_enable: bool,
pub tesla_update_interval_seconds: u64,
pub pl_watch_interval_seconds: u64,
pub pl_timeout_milliseconds: u64,
pub serial_port: String,
pub baud_rate: u32,
pub shutoff_voltage: f64,
pub shutoff_voltage_time_seconds: u64,
pub min_rate: i64,
pub max_rate: i64,
pub duty_cycle_too_high: f64,
pub duty_cycle_too_low: f64,
pub additional_charge_controllers: Vec<ChargeControllerConfig>,
pub pid_controls: PidControls,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
#[serde(default)]
pub struct PidControls {
pub proportional_gain: f64,
pub derivative_gain: f64,
pub loop_time_seconds: u64,
pub load_divisor: f64,
}
impl Default for PidControls {
fn default() -> Self {
Self {
proportional_gain: 1.,
derivative_gain: 1.,
loop_time_seconds: 5,
load_divisor: 30.,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum ChargeControllerConfig {
Pl {
serial_port: String,
baud_rate: u32,
timeout_milliseconds: u64,
watch_interval_seconds: u64,
},
Tristar {
serial_port: String,
baud_rate: u32,
watch_interval_seconds: u64,
},
}
impl Default for Config {
fn default() -> Self {
Self {
car_vin: String::from("unknown"),
control_enable: false,
tesla_update_interval_seconds: 120,
pl_watch_interval_seconds: 5,
pl_timeout_milliseconds: 400,
serial_port: String::from("/dev/ttyUSB0"),
baud_rate: 9600,
shutoff_voltage: 50.,
shutoff_voltage_time_seconds: 15,
min_rate: 5,
max_rate: 10,
duty_cycle_too_high: 0.8,
duty_cycle_too_low: 0.1,
additional_charge_controllers: Vec::new(),
pid_controls: Default::default(),
}
}
}
impl Config {
fn load(path: &Path) -> Self {
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
}
fn save_to(&self, path: &Path) -> Result<(), ConfigError> {
Ok(serde_json::ser::to_writer_pretty(
std::io::BufWriter::new(std::fs::File::create(path)?),
self,
)?)
}
fn save(&self) -> Result<(), ConfigError> {
self.save_to(CONFIG_PATH.get().unwrap())
}
fn load_and_save_defaults(path: &Path) -> Self {
let mut config = Self::load(path);
config.validate();
let result = config.save_to(path);
if let Err(e) = result {
log::error!("Failed to save config: {e:#?}",);
}
config
}
fn validate(&mut self) {
self.shutoff_voltage = self.shutoff_voltage.clamp(40.0, 60.0);
self.max_rate = self.max_rate.clamp(0, 30);
self.min_rate = self.min_rate.clamp(0, self.max_rate);
self.duty_cycle_too_high = self.duty_cycle_too_high.clamp(0.0, 1.0);
self.duty_cycle_too_low = self.duty_cycle_too_low.clamp(0.0, 1.0);
self.pid_controls.proportional_gain = self.pid_controls.proportional_gain.clamp(0.0, 50.0);
self.pid_controls.derivative_gain = self.pid_controls.derivative_gain.clamp(0.0, 50.0);
self.pid_controls.load_divisor = self.pid_controls.load_divisor.clamp(1.0, 50.0);
}
}

View file

@ -1,117 +0,0 @@
use std::sync::PoisonError;
use rocket::response::Responder;
use thiserror::Error;
pub trait PrintErrors {
type Inner;
fn some_or_print_with(self, context: &str) -> Option<Self::Inner>;
}
impl<T, E> PrintErrors for Result<T, E>
where
E: std::error::Error,
{
type Inner = T;
fn some_or_print_with(self, context: &str) -> Option<Self::Inner> {
match self {
Ok(val) => Some(val),
Err(e) => {
log::error!("{context}: {e:?}");
None
}
}
}
}
#[derive(Error, Debug)]
pub enum ServerError {
#[error("rwlock error")]
// 500
Lock,
#[error("no data")]
// 503
NoData,
#[error("invalid parameters")]
InvalidParameters,
#[error("prometheus")]
Prometheus(#[from] prometheus::Error),
#[error("uri")]
Uri,
#[error(transparent)]
Auth(#[from] AuthKeyError),
#[error(transparent)]
Channel(#[from] tokio::sync::mpsc::error::SendError<crate::api_interface::InterfaceRequest>),
}
impl From<rocket::http::uri::Error<'_>> for ServerError {
fn from(_: rocket::http::uri::Error<'_>) -> Self {
Self::Uri
}
}
impl<T> From<PoisonError<T>> for ServerError {
fn from(_: PoisonError<T>) -> Self {
Self::Lock
}
}
impl<'a> Responder<'a, 'a> for ServerError {
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
Err(match self {
ServerError::NoData => rocket::http::Status::ServiceUnavailable,
ServerError::InvalidParameters => rocket::http::Status::BadRequest,
ServerError::Auth(_) => rocket::http::Status::Unauthorized,
ServerError::Channel(_)
| ServerError::Uri
| ServerError::Lock
| ServerError::Prometheus(_) => rocket::http::Status::InternalServerError,
})
}
}
#[derive(Error, Debug)]
pub enum PliError {
#[error("read error")]
ReadError(u8),
#[error("io error")]
StdioErr(#[from] std::io::Error),
}
#[derive(Error, Debug)]
pub enum TristarError {
#[error(transparent)]
Modbus(#[from] tokio_modbus::Error),
#[error(transparent)]
ModbusException(#[from] tokio_modbus::ExceptionCode),
#[error(transparent)]
Serial(#[from] tokio_serial::Error),
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("json")]
Json(#[from] serde_json::Error),
#[error("io")]
Io(#[from] std::io::Error),
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum AuthKeyError {
#[error(transparent)]
SerdeError(#[from] serde_json::Error),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum TeslaError {
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("{0:#?}")]
Tesla(crate::types::ApiResponse),
}

View file

@ -1,218 +0,0 @@
#[macro_use]
extern crate rocket;
use api_interface::TeslaInterface;
use charge_controllers::{pl::Pli, tristar::Tristar};
use clap::{Parser, Subcommand};
use config::{access_config, ChargeControllerConfig};
use errors::PrintErrors;
use std::{path::PathBuf, sync::RwLock};
use tesla_charge_rate::TeslaChargeRateController;
use crate::config::Config;
mod api_interface;
mod charge_controllers;
mod config;
mod errors;
mod server;
mod tesla_charge_rate;
mod types;
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[command(subcommand)]
command: Commands,
#[clap(long, default_value = "/etc/tesla-charge-controller")]
config_dir: PathBuf,
}
#[derive(Subcommand, Debug, Clone)]
enum Commands {
/// Run charge controller server
Watch,
/// Print the default config file
GenerateConfig,
}
#[tokio::main]
async fn main() {
env_logger::builder()
.format_module_path(false)
.format_timestamp(
if std::env::var("LOG_TIMESTAMP").is_ok_and(|v| v == "false") {
None
} else {
Some(env_logger::TimestampPrecision::Seconds)
},
)
.init();
let args = Args::parse();
match args.command {
Commands::GenerateConfig => {
println!(
"{}",
serde_json::ser::to_string_pretty(&Config::default()).unwrap(),
);
}
Commands::Watch => {
log::trace!("begin");
let config_path = args.config_dir.join("config.json");
let (config, _config_watcher) = config::init_config(config_path);
log::trace!("config initialised, create interface and get state");
let interface = TeslaInterface::new(&config.car_vin);
config::CONFIG.get_or_init(|| RwLock::new(config));
// build the channel that takes messages from the webserver thread to the api thread
let (api_requests, mut api_receiver) = tokio::sync::mpsc::unbounded_channel();
// and to the pli thread
let (pli_requests, mut pli_receiver) = tokio::sync::mpsc::unbounded_channel();
// try to spawn the pli loop
let pli = {
let config = access_config();
Pli::new(
config.serial_port.clone(),
config.baud_rate,
config.pl_timeout_milliseconds,
)
};
let pl_state = match pli {
Ok(mut pli) => {
log::trace!("begin charge controller monitoring...");
let pl_state = pli.state.clone();
tokio::task::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
access_config().pl_watch_interval_seconds,
));
loop {
tokio::select! {
_ = interval.tick() => pli.refresh(),
message = pli_receiver.recv() => match message {
Some(message) => pli.process_request(message),
None => panic!("PLI send channel dropped")
}
}
}
});
Some(pl_state)
}
Err(e) => {
log::error!("Error connecting to serial device for PLI: {e:?}");
None
}
};
let local = tokio::task::LocalSet::new();
// spawn a loop for each additional charge controller to log
// failed connections will print an error but the program will continue
let _additional_controllers: Vec<_> = access_config()
.additional_charge_controllers
.clone()
.into_iter()
.filter_map(|v| match v {
ChargeControllerConfig::Pl {
serial_port,
baud_rate,
timeout_milliseconds,
watch_interval_seconds,
} => Pli::new(serial_port.clone(), baud_rate, timeout_milliseconds)
.some_or_print_with("Failed to connect to additional PLI")
.map(|mut pli| {
log::trace!("monitoring additional PL (port: {serial_port}");
tokio::task::spawn(async move {
let mut interval = tokio::time::interval(
std::time::Duration::from_secs(watch_interval_seconds),
);
loop {
interval.tick().await;
pli.refresh();
}
})
}),
ChargeControllerConfig::Tristar {
serial_port,
baud_rate,
watch_interval_seconds,
} => Tristar::new(serial_port.clone(), baud_rate)
.some_or_print_with("Failed to connect to additional Tristar")
.map(|mut tristar| {
log::trace!("monitoring additional tristar (port: {serial_port}");
local.spawn_local(async move {
let mut interval = tokio::time::interval(
std::time::Duration::from_secs(watch_interval_seconds),
);
loop {
interval.tick().await;
tristar.refresh().await;
}
})
}),
})
.collect();
let tcrc = TeslaChargeRateController::new(interface.state.clone(), pl_state.clone());
let car_state = interface.state.clone();
let server_handle = server::launch_server(server::ServerState::new(
car_state,
pl_state,
api_requests,
pli_requests,
));
// spawn the api / charge rate control loop
tokio::task::spawn(async move {
let mut normal_data_update_interval = tokio::time::interval(
std::time::Duration::from_secs(access_config().tesla_update_interval_seconds),
);
let mut charge_rate_update_interval = tokio::time::interval(
std::time::Duration::from_secs(access_config().pid_controls.loop_time_seconds),
);
let mut was_connected = false;
let (mut interface, mut tcrc) = (interface, tcrc);
log::trace!("begin control loop...");
loop {
// await either the next interval OR a message from the other thread
tokio::select! {
_ = normal_data_update_interval.tick() => {
if !interface.state.read().unwrap().is_charging() {
interface.refresh().await;
}
},
_ = charge_rate_update_interval.tick() => {
if interface.state.read().unwrap().is_charging() {
was_connected = true;
if let Some(request) = tcrc.control_charge_rate() {
interface.process_request(request).await;
}
interface.refresh().await;
} else if was_connected
&& interface
.state
.read()
.unwrap()
.charge_state
.as_ref()
.is_some_and(|v| v.charging_state == types::ChargingState::Disconnected)
{
// reenable control when charger is disconnected
was_connected = false;
}
},
api_message = api_receiver.recv() => match api_message {
Some(message) => interface.process_request(message).await,
None => panic!("Tesla send channel dropped")
}
}
}
});
tokio::join!(server_handle, local);
}
}
}

View file

@ -1,138 +0,0 @@
use std::sync::{Arc, RwLock};
use lazy_static::lazy_static;
use prometheus::{register_gauge, register_int_gauge, Gauge, IntGauge};
use crate::{
api_interface::InterfaceRequest,
charge_controllers::pl::PlState,
config::access_config,
types::{CarState, ChargeState},
};
lazy_static! {
pub static ref CONTROL_ENABLE_GAUGE: IntGauge =
register_int_gauge!("tcrc_control_enable", "Enable Tesla charge rate control",).unwrap();
pub static ref PROPORTIONAL_GAUGE: Gauge = register_gauge!(
"tcrc_proportional",
"Proportional component of requested change to charge rate",
)
.unwrap();
pub static ref DERIVATIVE_GAUGE: Gauge = register_gauge!(
"tcrc_derivative",
"Derivative component of requested change to charge rate",
)
.unwrap();
pub static ref LOAD_GAUGE: Gauge = register_gauge!(
"tcrc_load",
"Fudge factor from internal load of requested change to charge rate",
)
.unwrap();
pub static ref CHANGE_REQUEST_GAUGE: Gauge =
register_gauge!("tcrc_change_request", "Requested change to charge rate",).unwrap();
}
pub struct TeslaChargeRateController {
pub car_state: Arc<RwLock<CarState>>,
pub pl_state: Option<Arc<RwLock<PlState>>>,
pid: PidLoop,
voltage_low: u64,
}
#[derive(Clone, Copy)]
#[expect(dead_code)]
pub enum TcrcRequest {
DisableAutomaticControl,
EnableAutomaticControl,
}
impl TeslaChargeRateController {
pub fn new(car_state: Arc<RwLock<CarState>>, pl_state: Option<Arc<RwLock<PlState>>>) -> Self {
Self {
car_state,
pl_state,
pid: Default::default(),
voltage_low: 0,
}
}
pub fn control_charge_rate(&mut self) -> Option<InterfaceRequest> {
let delta_time = access_config().pid_controls.loop_time_seconds;
if let Some(pl_state) = self.pl_state.as_ref().and_then(|v| v.read().ok()) {
if let Ok(car_state) = self.car_state.read() {
if let Some(charge_state) = car_state.charge_state.as_ref() {
if pl_state.battery_voltage < access_config().shutoff_voltage {
self.voltage_low += 1;
if (self.voltage_low * delta_time)
>= access_config().shutoff_voltage_time_seconds
{
return Some(InterfaceRequest::StopCharge);
}
} else {
self.voltage_low = 0;
if crate::config::access_config().control_enable {
return self
.pid
.step(&pl_state, charge_state, delta_time as f64)
.map(InterfaceRequest::SetChargeRate);
}
}
}
}
}
None
}
}
struct PidLoop {
previous_error: f64,
}
impl Default for PidLoop {
fn default() -> Self {
Self { previous_error: 0. }
}
}
impl PidLoop {
fn step(
&mut self,
pl_state: &PlState,
charge_state: &ChargeState,
delta_time: f64,
) -> Option<i64> {
let error = pl_state.battery_voltage - pl_state.target_voltage;
let derivative = (error - self.previous_error) / delta_time;
let config = access_config();
let proportional_component = config.pid_controls.proportional_gain * error;
let derivative_component = config.pid_controls.derivative_gain * derivative;
let extra_offsets =
(pl_state.internal_load_current / config.pid_controls.load_divisor).clamp(0., 2.);
let offset = proportional_component + derivative_component + extra_offsets;
PROPORTIONAL_GAUGE.set(proportional_component);
DERIVATIVE_GAUGE.set(derivative_component);
LOAD_GAUGE.set(extra_offsets);
CHANGE_REQUEST_GAUGE.set(offset);
let new_target = offset + charge_state.charge_amps as f64;
self.previous_error = error;
let new_target_int = new_target.round() as i64;
valid_rate(new_target_int, charge_state.charge_amps)
}
}
fn valid_rate(rate: i64, previous: i64) -> Option<i64> {
let config = access_config();
let new = rate.clamp(config.min_rate, config.max_rate);
if new == previous {
None
} else {
Some(new)
}
}

View file

@ -0,0 +1,40 @@
[package]
name = "tesla-charge-controller"
version.workspace = true
edition = "2021"
license = "MITNFA"
description = "Controls Tesla charge rate based on solar charge data"
authors = ["Alex Janka"]
[package.metadata.deb]
maintainer-scripts = "debian/"
systemd-units = { enable = false }
depends = "charge-controller-supervisor"
[dependencies]
clap = { version = "4.5.23", features = ["derive"] }
ron = "0.8.1"
serde = { version = "1.0.216", features = ["derive"] }
serde_json = "1.0.134"
tokio = { version = "1.42.0", features = ["full"] }
thiserror = "2.0.9"
rocket = { version = "0.5.1", features = ["json"] }
include_dir = "0.7.4"
chrono = "0.4.39"
prometheus = "0.13.4"
env_logger = "0.11.6"
log = "0.4.22"
serialport = "4.6.1"
tokio-modbus = "0.16.1"
tokio-serial = "5.4.4"
if_chain = "1.0.2"
notify-debouncer-mini = { version = "0.5.0", default-features = false }
lazy_static = "1.5.0"
rand = "0.8.5"
reqwest = { version = "0.12.9", default-features = false, features = [
"rustls-tls",
] }
eyre = "0.6.12"
[lints]
workspace = true

View file

@ -1,3 +1,5 @@
use super::ChargeState;
const API_URL: &str = if cfg!(debug_assertions) {
"http://cnut.internal.alexjanka.com:4443/api/1"
} else {
@ -11,12 +13,22 @@ pub struct Vehicle {
#[derive(serde::Deserialize)]
struct ApiResponseOuter {
response: crate::types::ApiResponse,
response: ApiResponse,
}
#[allow(dead_code)]
#[derive(serde::Deserialize, Debug)]
pub struct ApiResponse {
pub command: String,
pub reason: String,
pub response: serde_json::Value,
pub result: bool,
pub vin: String,
}
#[derive(serde::Deserialize, Debug)]
struct ChargeStateOuter {
charge_state: crate::types::ChargeState,
charge_state: ChargeState,
}
macro_rules! commands {
@ -24,7 +36,7 @@ macro_rules! commands {
#[allow(dead_code)]
impl $v {
$(
pub async fn $command(&self) -> Result<(), Box<dyn std::error::Error>> {
pub async fn $command(&self) -> eyre::Result<()> {
self.client
.post(format!(
"{API_URL}/vehicles/{}/command/{}",
@ -60,9 +72,7 @@ impl Vehicle {
Self { vin, client }
}
pub async fn charge_data(
&self,
) -> Result<crate::types::ChargeState, crate::errors::TeslaError> {
pub async fn charge_data(&self) -> eyre::Result<ChargeState> {
log::trace!("getting charge data...");
let data = self
.client
@ -77,17 +87,14 @@ impl Vehicle {
let response = response.response;
if !response.result {
return Err(crate::errors::TeslaError::Tesla(response));
return Err(eyre::eyre!("got error response from API: {response:#?}"));
}
let state: ChargeStateOuter = serde_json::from_value(response.response)?;
Ok(state.charge_state)
}
pub async fn set_charging_amps(
&self,
charging_amps: i64,
) -> Result<(), Box<dyn std::error::Error>> {
pub async fn set_charging_amps(&self, charging_amps: i64) -> eyre::Result<()> {
self.client
.post(format!(
"{API_URL}/vehicles/{}/command/set_charging_amps",

View file

@ -1,28 +1,90 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Serialize, Deserialize, Debug)]
pub struct CarState {
pub charge_state: Option<ChargeState>,
mod http;
// pub enum CarState {
// Unavailable,
// Available {
// charge_state: ChargeState,
// data_received: std::time::Instant,
// },
// }
pub struct Car {
vehicle: http::Vehicle,
state: tokio::sync::RwLock<CarState>,
}
impl CarState {
pub fn is_charging(&self) -> bool {
self.charge_state
.as_ref()
.is_some_and(|v| v.charging_state == ChargingState::Charging)
pub struct CarState {
charge_state: Option<ChargeState>,
data_received: std::time::Instant,
}
impl Car {
pub fn new(vin: &str) -> Self {
let state = CarState {
charge_state: None,
data_received: std::time::Instant::now(),
};
let state = tokio::sync::RwLock::new(state);
Self {
vehicle: http::Vehicle::new(vin),
state,
}
}
pub async fn update(&self) {
let mut state = self.state.write().await;
if let Ok(data) = self.vehicle.charge_data().await {
state.data_received = std::time::Instant::now();
state.charge_state = Some(data);
}
}
pub fn vehicle(&self) -> &http::Vehicle {
&self.vehicle
}
pub fn state(&self) -> &tokio::sync::RwLock<CarState> {
&self.state
}
}
#[allow(dead_code)]
#[derive(serde::Deserialize, Debug)]
pub struct ApiResponse {
pub command: String,
pub reason: String,
pub response: serde_json::Value,
pub result: bool,
pub vin: String,
impl CarState {
pub fn charge_state(&self) -> Option<&ChargeState> {
if self.is_outdated() {
None
} else {
self.charge_state.as_ref()
}
}
pub fn is_charging(&self) -> bool {
self.charge_state().is_some_and(ChargeState::is_charging)
}
pub fn data_age(&self) -> std::time::Duration {
std::time::Instant::now().duration_since(self.data_received)
}
fn is_outdated(&self) -> bool {
self.data_age() > crate::CONFIG.read().car_state_interval
}
}
// impl CarState {
// pub fn charging_state(&self) -> ChargingState {
// match self {
// CarState::Unavailable => ChargingState::Unavailable,
// CarState::Available {
// charge_state,
// data_received: _,
// } => charge_state.charging_state,
// }
// }
// }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ChargeState {
pub battery_heater_on: bool,
@ -83,6 +145,12 @@ pub struct ChargeState {
pub user_charge_enable_request: Option<bool>,
}
impl ChargeState {
pub fn is_charging(&self) -> bool {
self.charging_state == ChargingState::Charging
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum ChargingState {
Charging,

View file

@ -0,0 +1,69 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default)]
pub struct Config {
pub car_vin: String,
pub control_enable: bool,
pub tesla_update_interval_seconds: u64,
pub baud_rate: u32,
pub shutoff_voltage: f64,
pub shutoff_voltage_time_seconds: u64,
pub min_rate: i64,
pub max_rate: i64,
pub pid_controls: PidControls,
pub car_state_interval: std::time::Duration,
pub car_state_expiry: std::time::Duration,
}
impl Default for Config {
fn default() -> Self {
Self {
car_vin: String::from("unknown"),
control_enable: false,
tesla_update_interval_seconds: 120,
baud_rate: 9600,
shutoff_voltage: 50.,
shutoff_voltage_time_seconds: 15,
min_rate: 5,
max_rate: 10,
pid_controls: Default::default(),
car_state_interval: std::time::Duration::from_secs(20),
car_state_expiry: std::time::Duration::from_secs(30),
}
}
}
impl common::config::ConfigTrait for Config {
fn load_and_save_defaults(path: impl AsRef<std::path::Path>) -> Self {
todo!()
}
fn save(&self) -> eyre::Result<()> {
todo!()
}
fn is_valid(&self) -> bool {
todo!()
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
#[serde(default)]
pub struct PidControls {
pub proportional_gain: f64,
pub derivative_gain: f64,
pub loop_time_seconds: u64,
pub load_divisor: f64,
}
impl Default for PidControls {
fn default() -> Self {
Self {
proportional_gain: 1.,
derivative_gain: 1.,
loop_time_seconds: 5,
load_divisor: 30.,
}
}
}

View file

@ -0,0 +1,58 @@
pub struct VehicleController {
car: std::sync::Arc<crate::api::Car>,
requests: tokio::sync::mpsc::UnboundedReceiver<InterfaceRequest>,
control_state: ChargeRateControllerState,
}
pub enum ChargeRateControllerState {
Inactive,
Charging { rate_amps: i64 },
}
pub enum InterfaceRequest {
FlashLights,
}
impl VehicleController {
pub fn new(
car: std::sync::Arc<crate::api::Car>,
requests: tokio::sync::mpsc::UnboundedReceiver<InterfaceRequest>,
) -> Self {
Self {
car,
requests,
control_state: ChargeRateControllerState::Inactive,
}
}
pub async fn run_cycle(&mut self) {
let age = self.car.state().read().await.data_age();
if age >= crate::CONFIG.read().car_state_interval {
self.car.update().await;
}
match self.control_state {
ChargeRateControllerState::Inactive => {
if let Some(state) = self.car.state().read().await.charge_state() {
if state.is_charging() {
self.control_state = ChargeRateControllerState::Charging {
rate_amps: state.charge_amps,
}
}
}
}
ChargeRateControllerState::Charging { rate_amps } => todo!(),
}
}
pub async fn process_requests(&mut self) -> eyre::Result<()> {
while let Some(req) = self.requests.recv().await {
match req {
InterfaceRequest::FlashLights => {
self.car.vehicle().flash_lights().await?;
}
}
}
Ok(())
}
}

View file

@ -0,0 +1,54 @@
use std::path::PathBuf;
use clap::Parser;
mod api;
mod config;
mod control;
mod server;
static CONFIG: common::config::ConfigSingleton<config::Config> =
common::config::ConfigSingleton::new();
#[derive(clap::Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[command(subcommand)]
command: Commands,
#[clap(long, default_value = "/etc/tesla-charge-controller")]
config_dir: PathBuf,
}
#[derive(clap::Subcommand, Debug, Clone)]
enum Commands {
/// Run charge controller server
Watch,
/// Print the default config file
GenerateConfig,
}
#[tokio::main]
async fn main() {
common::init_log();
if let Err(e) = run().await {
log::error!("{e:?}");
std::process::exit(1);
}
}
async fn run() -> eyre::Result<()> {
let args = Args::parse();
CONFIG.load(args.config_dir.join("tesla.json"))?;
let car = api::Car::new(&CONFIG.read().car_vin);
let car = std::sync::Arc::new(car);
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut vehicle_controller = control::VehicleController::new(car.clone(), rx);
let server = server::launch_server(server::ServerState::new(car, tx));
server.await;
vehicle_controller.run_cycle().await;
Ok(())
}

View file

@ -1,47 +1,30 @@
use std::sync::{Arc, RwLock};
use std::sync::Arc;
use rocket::{
fairing::{Fairing, Info, Kind},
get,
http::Header,
request::FromParam,
post, routes,
serde::json::Json,
Request, Response, State,
};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::UnboundedSender;
use crate::{
api_interface::InterfaceRequest,
charge_controllers::pl::{PlState, PliRequest, RegulatorState},
config::{access_config, write_to_config},
errors::{PrintErrors, ServerError},
types::CarState,
};
use crate::{api::Car, control::InterfaceRequest};
use self::static_handler::UiStatic;
mod static_handler;
pub struct ServerState {
pub car_state: Arc<RwLock<CarState>>,
pub pl_state: Option<Arc<RwLock<PlState>>>,
pub car: Arc<Car>,
pub api_requests: UnboundedSender<InterfaceRequest>,
pub pli_requests: UnboundedSender<PliRequest>,
}
impl ServerState {
pub fn new(
car_state: Arc<RwLock<CarState>>,
pl_state: Option<Arc<RwLock<PlState>>>,
api_requests: UnboundedSender<InterfaceRequest>,
pli_requests: UnboundedSender<PliRequest>,
) -> Self {
Self {
car_state,
pl_state,
api_requests,
pli_requests,
}
pub fn new(car: Arc<Car>, api_requests: UnboundedSender<InterfaceRequest>) -> Self {
Self { car, api_requests }
}
}
@ -70,7 +53,6 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
"/",
routes![
car_state,
regulator_state,
control_state,
flash,
disable_control,
@ -86,17 +68,24 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
set_load_divisor,
pid_settings,
metrics,
sync_time,
read_ram,
read_eeprom,
set_regulator_state
],
)
}
#[get("/car-state")]
fn car_state(state: &State<ServerState>) -> Result<Json<CarState>, ServerError> {
Ok(Json(state.car_state.read()?.clone()))
async fn car_state(
state: &State<ServerState>,
) -> Result<Json<crate::api::ChargeState>, ServerError> {
Ok(Json(
state
.car
.state()
.read()
.await
.charge_state()
.ok_or(ServerError::NoData)?
.clone(),
))
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
@ -108,9 +97,9 @@ struct ControlState {
}
#[get("/control-state")]
fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> {
let is_charging = state.car_state.read()?.is_charging();
let config = access_config();
async fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> {
let is_charging = state.car.state().read().await.is_charging();
let config = crate::CONFIG.read();
let control_enable = config.control_enable;
let max_rate = config.max_rate;
let min_rate = config.min_rate;
@ -132,27 +121,27 @@ fn flash(state: &State<ServerState>, remote_addr: std::net::IpAddr) {
#[post("/disable-control")]
fn disable_control(remote_addr: std::net::IpAddr) {
log::warn!("disabling control: {remote_addr:?}");
crate::config::write_to_config().control_enable = false;
crate::CONFIG.write().control_enable = false;
}
#[post("/enable-control")]
fn enable_control(remote_addr: std::net::IpAddr) {
log::warn!("enabling control: {remote_addr:?}");
crate::config::write_to_config().control_enable = true;
crate::CONFIG.write().control_enable = true;
}
#[post("/shutoff/voltage/<voltage>")]
fn set_shutoff(voltage: f64, remote_addr: std::net::IpAddr) {
log::warn!("setting shutoff voltage: {remote_addr:?}");
let voltage = voltage.clamp(40., 60.);
write_to_config().shutoff_voltage = voltage;
crate::CONFIG.write().shutoff_voltage = voltage;
}
#[post("/shutoff/time/<time>")]
fn set_shutoff_time(time: u64, remote_addr: std::net::IpAddr) {
log::warn!("setting shutoff time: {remote_addr:?}");
let time = time.clamp(5, 120);
write_to_config().shutoff_voltage_time_seconds = time;
crate::CONFIG.write().shutoff_voltage_time_seconds = time;
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
@ -163,7 +152,7 @@ struct ShutoffStatus {
#[get("/shutoff/status")]
fn shutoff_status() -> Json<ShutoffStatus> {
let config = access_config();
let config = crate::CONFIG.read();
Json(ShutoffStatus {
voltage: config.shutoff_voltage,
time: config.shutoff_voltage_time_seconds,
@ -173,44 +162,44 @@ fn shutoff_status() -> Json<ShutoffStatus> {
#[post("/set-max/<limit>")]
fn set_max(limit: i64, remote_addr: std::net::IpAddr) {
log::warn!("setting max: {remote_addr:?}");
let limit = limit.clamp(access_config().min_rate, 32);
write_to_config().max_rate = limit;
let limit = limit.clamp(crate::CONFIG.read().min_rate, 32);
crate::CONFIG.write().max_rate = limit;
}
#[post("/set-min/<limit>")]
fn set_min(limit: i64, remote_addr: std::net::IpAddr) {
log::warn!("setting min: {remote_addr:?}");
let limit = limit.clamp(0, access_config().max_rate);
write_to_config().min_rate = limit;
let limit = limit.clamp(0, crate::CONFIG.read().max_rate);
crate::CONFIG.write().min_rate = limit;
}
#[post("/pid-settings/proportional/<gain>")]
fn set_proportional_gain(gain: f64, remote_addr: std::net::IpAddr) {
log::warn!("setting proportional gain: {remote_addr:?}");
write_to_config().pid_controls.proportional_gain = gain;
crate::CONFIG.write().pid_controls.proportional_gain = gain;
}
#[post("/pid-settings/derivative/<gain>")]
fn set_derivative_gain(gain: f64, remote_addr: std::net::IpAddr) {
log::warn!("setting derivative gain: {remote_addr:?}");
write_to_config().pid_controls.derivative_gain = gain;
crate::CONFIG.write().pid_controls.derivative_gain = gain;
}
#[post("/pid-settings/loop_time_seconds/<time>")]
fn set_pid_loop_length(time: u64, remote_addr: std::net::IpAddr) {
log::warn!("setting pid loop interval: {remote_addr:?}");
write_to_config().pid_controls.loop_time_seconds = time;
crate::CONFIG.write().pid_controls.loop_time_seconds = time;
}
#[post("/pid-settings/load_divisor/<divisor>")]
fn set_load_divisor(divisor: f64, remote_addr: std::net::IpAddr) {
log::warn!("setting load divisor interval: {remote_addr:?}");
write_to_config().pid_controls.load_divisor = divisor;
crate::CONFIG.write().pid_controls.load_divisor = divisor;
}
#[get("/pid-settings/status")]
fn pid_settings() -> Json<crate::config::PidControls> {
Json(access_config().pid_controls)
Json(crate::CONFIG.read().pid_controls)
}
#[get("/metrics")]
@ -221,63 +210,63 @@ fn metrics() -> Result<String, ServerError> {
)
}
#[get("/sync-time")]
fn sync_time(state: &State<ServerState>) -> String {
let _ = state.pli_requests.send(PliRequest::SyncTime);
String::from("syncing time...")
}
// #[get("/sync-time")]
// fn sync_time(state: &State<ServerState>) -> String {
// let _ = state.pli_requests.send(PliRequest::SyncTime);
// String::from("syncing time...")
// }
#[get("/read-ram/<address>")]
fn read_ram(address: u8, state: &State<ServerState>) -> String {
let _ = state.pli_requests.send(PliRequest::ReadRam(address));
format!("reading at ram address {address}")
}
// #[get("/read-ram/<address>")]
// fn read_ram(address: u8, state: &State<ServerState>) -> String {
// let _ = state.pli_requests.send(PliRequest::ReadRam(address));
// format!("reading at ram address {address}")
// }
#[get("/read-eeprom/<address>")]
fn read_eeprom(address: u8, state: &State<ServerState>) -> String {
let _ = state.pli_requests.send(PliRequest::ReadEeprom(address));
format!("reading at eeprom address {address}")
}
// #[get("/read-eeprom/<address>")]
// fn read_eeprom(address: u8, state: &State<ServerState>) -> String {
// let _ = state.pli_requests.send(PliRequest::ReadEeprom(address));
// format!("reading at eeprom address {address}")
// }
#[get("/regulator-state")]
fn regulator_state(state: &State<ServerState>) -> Result<Json<PlState>, ServerError> {
state
.pl_state
.as_ref()
.ok_or(ServerError::NoData)
.and_then(|v| Ok(Json(*(v.read()?))))
}
// #[get("/regulator-state")]
// fn regulator_state(state: &State<ServerState>) -> Result<Json<PlState>, ServerError> {
// state
// .pl_state
// .as_ref()
// .ok_or(ServerError::NoData)
// .and_then(|v| Ok(Json(*(v.read()?))))
// }
#[post("/set-regulator-state/<regulator_state>")]
fn set_regulator_state(
state: &State<ServerState>,
regulator_state: Option<RegulatorState>,
) -> Result<(), ServerError> {
if let Some(regulator_state) = regulator_state {
log::info!("Requesting regulator state set to {regulator_state:?}");
state
.pli_requests
.send(PliRequest::SetRegulatorState(regulator_state))
.some_or_print_with("requesting new pl regulator state");
Ok(())
} else {
Err(ServerError::InvalidParameters)
}
}
// #[post("/set-regulator-state/<regulator_state>")]
// fn set_regulator_state(
// state: &State<ServerState>,
// regulator_state: Option<RegulatorState>,
// ) -> Result<(), ServerError> {
// if let Some(regulator_state) = regulator_state {
// log::info!("Requesting regulator state set to {regulator_state:?}");
// state
// .pli_requests
// .send(PliRequest::SetRegulatorState(regulator_state))
// .some_or_print_with("requesting new pl regulator state");
// Ok(())
// } else {
// Err(ServerError::InvalidParameters)
// }
// }
impl FromParam<'_> for RegulatorState {
type Error = ();
// impl FromParam<'_> for RegulatorState {
// type Error = ();
fn from_param(param: &'_ str) -> Result<Self, Self::Error> {
match param.to_lowercase().as_str() {
"boost" => Ok(Self::Boost),
"equalise" => Ok(Self::Equalise),
"absorption" => Ok(Self::Absorption),
"float" => Ok(Self::Float),
_ => Err(()),
}
}
}
// fn from_param(param: &'_ str) -> Result<Self, Self::Error> {
// match param.to_lowercase().as_str() {
// "boost" => Ok(Self::Boost),
// "equalise" => Ok(Self::Equalise),
// "absorption" => Ok(Self::Absorption),
// "float" => Ok(Self::Float),
// _ => Err(()),
// }
// }
// }
pub struct Cors;
@ -300,3 +289,43 @@ impl Fairing for Cors {
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
}
#[derive(thiserror::Error, Debug)]
pub enum ServerError {
#[error("rwlock error")]
// 500
Lock,
#[error("no data")]
// 503
NoData,
#[error("prometheus")]
Prometheus(#[from] prometheus::Error),
#[error("uri")]
Uri,
#[error(transparent)]
Channel(#[from] tokio::sync::mpsc::error::SendError<InterfaceRequest>),
}
impl From<rocket::http::uri::Error<'_>> for ServerError {
fn from(_: rocket::http::uri::Error<'_>) -> Self {
Self::Uri
}
}
impl<T> From<std::sync::PoisonError<T>> for ServerError {
fn from(_: std::sync::PoisonError<T>) -> Self {
Self::Lock
}
}
impl<'a> rocket::response::Responder<'a, 'a> for ServerError {
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
Err(match self {
ServerError::NoData => rocket::http::Status::ServiceUnavailable,
ServerError::Channel(_)
| ServerError::Uri
| ServerError::Lock
| ServerError::Prometheus(_) => rocket::http::Status::InternalServerError,
})
}
}