diff --git a/.gitea/workflows/deb.yaml b/.gitea/workflows/deb.yaml index f0bc944..d789a24 100644 --- a/.gitea/workflows/deb.yaml +++ b/.gitea/workflows/deb.yaml @@ -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}}" diff --git a/Cargo.lock b/Cargo.lock index 0af99af..c7b9cde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index d185013..4a46273 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/charge-controller-supervisor/Cargo.toml b/charge-controller-supervisor/Cargo.toml new file mode 100644 index 0000000..a5bebc2 --- /dev/null +++ b/charge-controller-supervisor/Cargo.toml @@ -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 diff --git a/pkg/systemd/charge-controller-supervisor.service b/charge-controller-supervisor/debian/service similarity index 100% rename from pkg/systemd/charge-controller-supervisor.service rename to charge-controller-supervisor/debian/service diff --git a/charge-controller-supervisor/src/config.rs b/charge-controller-supervisor/src/config.rs new file mode 100644 index 0000000..7a9432c --- /dev/null +++ b/charge-controller-supervisor/src/config.rs @@ -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, + } + } +} diff --git a/src/lib/charge_controllers/gauges.rs b/charge-controller-supervisor/src/gauges.rs similarity index 100% rename from src/lib/charge_controllers/gauges.rs rename to charge-controller-supervisor/src/gauges.rs diff --git a/charge-controller-supervisor/src/main.rs b/charge-controller-supervisor/src/main.rs new file mode 100644 index 0000000..c538874 --- /dev/null +++ b/charge-controller-supervisor/src/main.rs @@ -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()); + } + } +} diff --git a/src/lib/charge_controllers/pl.rs b/charge-controller-supervisor/src/pl.rs similarity index 67% rename from src/lib/charge_controllers/pl.rs rename to charge-controller-supervisor/src/pl.rs index 106be0e..e5a456e 100644 --- a/src/lib/charge_controllers/pl.rs +++ b/charge-controller-supervisor/src/pl.rs @@ -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 { diff --git a/src/lib/charge_controllers/tristar.rs b/charge-controller-supervisor/src/tristar.rs similarity index 77% rename from src/lib/charge_controllers/tristar.rs rename to charge-controller-supervisor/src/tristar.rs index 301ea6e..4439080 100644 --- a/src/lib/charge_controllers/tristar.rs +++ b/charge-controller-supervisor/src/tristar.rs @@ -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) diff --git a/charge-controller-supervisor/src/web.rs b/charge-controller-supervisor/src/web.rs new file mode 100644 index 0000000..2767397 --- /dev/null +++ b/charge-controller-supervisor/src/web.rs @@ -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, + }) + } +} diff --git a/pkg/debian/.empty b/pkg/debian/.empty deleted file mode 100644 index e69de29..0000000 diff --git a/src/bins/controllers/main.rs b/src/bins/controllers/main.rs deleted file mode 100644 index f328e4d..0000000 --- a/src/bins/controllers/main.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/src/bins/tesla/main.rs b/src/bins/tesla/main.rs deleted file mode 100644 index f328e4d..0000000 --- a/src/bins/tesla/main.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/src/lib/api_interface.rs b/src/lib/api_interface.rs deleted file mode 100644 index 987552b..0000000 --- a/src/lib/api_interface.rs +++ /dev/null @@ -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) -} diff --git a/src/lib/charge_controllers/mod.rs b/src/lib/charge_controllers/mod.rs deleted file mode 100644 index f319cdc..0000000 --- a/src/lib/charge_controllers/mod.rs +++ /dev/null @@ -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"; diff --git a/src/lib/config.rs b/src/lib/config.rs deleted file mode 100644 index 2beed5a..0000000 --- a/src/lib/config.rs +++ /dev/null @@ -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); - } -} diff --git a/src/lib/errors.rs b/src/lib/errors.rs deleted file mode 100644 index ca9e1f7..0000000 --- a/src/lib/errors.rs +++ /dev/null @@ -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), -} diff --git a/src/lib/lib.rs b/src/lib/lib.rs deleted file mode 100644 index d73b97c..0000000 --- a/src/lib/lib.rs +++ /dev/null @@ -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); - } - } -} diff --git a/src/lib/tesla_charge_rate.rs b/src/lib/tesla_charge_rate.rs deleted file mode 100644 index 50141d0..0000000 --- a/src/lib/tesla_charge_rate.rs +++ /dev/null @@ -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) - } -} diff --git a/tesla-charge-controller/Cargo.toml b/tesla-charge-controller/Cargo.toml new file mode 100644 index 0000000..69c3254 --- /dev/null +++ b/tesla-charge-controller/Cargo.toml @@ -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 diff --git a/pkg/systemd/tesla-charge-controller.service b/tesla-charge-controller/debian/service similarity index 100% rename from pkg/systemd/tesla-charge-controller.service rename to tesla-charge-controller/debian/service diff --git a/src/lib/api_interface/http.rs b/tesla-charge-controller/src/api/http.rs similarity index 78% rename from src/lib/api_interface/http.rs rename to tesla-charge-controller/src/api/http.rs index f42bbe6..30153ff 100644 --- a/src/lib/api_interface/http.rs +++ b/tesla-charge-controller/src/api/http.rs @@ -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", diff --git a/src/lib/types.rs b/tesla-charge-controller/src/api/mod.rs similarity index 56% rename from src/lib/types.rs rename to tesla-charge-controller/src/api/mod.rs index 1655905..01e7bf5 100644 --- a/src/lib/types.rs +++ b/tesla-charge-controller/src/api/mod.rs @@ -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, diff --git a/tesla-charge-controller/src/config.rs b/tesla-charge-controller/src/config.rs new file mode 100644 index 0000000..2c8c811 --- /dev/null +++ b/tesla-charge-controller/src/config.rs @@ -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., + } + } +} diff --git a/tesla-charge-controller/src/control.rs b/tesla-charge-controller/src/control.rs new file mode 100644 index 0000000..bae5be1 --- /dev/null +++ b/tesla-charge-controller/src/control.rs @@ -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(()) + } +} diff --git a/tesla-charge-controller/src/main.rs b/tesla-charge-controller/src/main.rs new file mode 100644 index 0000000..08071e1 --- /dev/null +++ b/tesla-charge-controller/src/main.rs @@ -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(()) +} diff --git a/src/lib/server/mod.rs b/tesla-charge-controller/src/server/mod.rs similarity index 54% rename from src/lib/server/mod.rs rename to tesla-charge-controller/src/server/mod.rs index 5b60234..fb74bc2 100644 --- a/src/lib/server/mod.rs +++ b/tesla-charge-controller/src/server/mod.rs @@ -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, + }) + } +} diff --git a/src/lib/server/static_handler.rs b/tesla-charge-controller/src/server/static_handler.rs similarity index 100% rename from src/lib/server/static_handler.rs rename to tesla-charge-controller/src/server/static_handler.rs