From 2c7aa8641c24976ad6519d6ae1f12873ae51f5c3 Mon Sep 17 00:00:00 2001
From: Alex Janka <alex@alexjanka.com>
Date: Sat, 28 Dec 2024 18:54:21 +1100
Subject: [PATCH] split: initial (and bad)

---
 .gitea/workflows/deb.yaml                     |   4 +-
 Cargo.lock                                    | 578 ++----------------
 Cargo.toml                                    |  64 +-
 charge-controller-supervisor/Cargo.toml       |  32 +
 .../debian/service                            |   0
 charge-controller-supervisor/src/config.rs    |  41 ++
 .../src}/gauges.rs                            |   0
 charge-controller-supervisor/src/main.rs      | 160 +++++
 .../src}/pl.rs                                | 119 ++--
 .../src}/tristar.rs                           | 103 ++--
 charge-controller-supervisor/src/web.rs       |  31 +
 pkg/debian/.empty                             |   0
 src/bins/controllers/main.rs                  |   1 -
 src/bins/tesla/main.rs                        |   1 -
 src/lib/api_interface.rs                      | 263 --------
 src/lib/charge_controllers/mod.rs             |   7 -
 src/lib/config.rs                             | 223 -------
 src/lib/errors.rs                             | 117 ----
 src/lib/lib.rs                                | 218 -------
 src/lib/tesla_charge_rate.rs                  | 138 -----
 tesla-charge-controller/Cargo.toml            |  40 ++
 .../debian/service                            |   0
 .../src/api}/http.rs                          |  29 +-
 .../src/api/mod.rs                            | 100 ++-
 tesla-charge-controller/src/config.rs         |  69 +++
 tesla-charge-controller/src/control.rs        |  58 ++
 tesla-charge-controller/src/main.rs           |  54 ++
 .../src}/server/mod.rs                        | 227 ++++---
 .../src}/server/static_handler.rs             |   0
 29 files changed, 869 insertions(+), 1808 deletions(-)
 create mode 100644 charge-controller-supervisor/Cargo.toml
 rename pkg/systemd/charge-controller-supervisor.service => charge-controller-supervisor/debian/service (100%)
 create mode 100644 charge-controller-supervisor/src/config.rs
 rename {src/lib/charge_controllers => charge-controller-supervisor/src}/gauges.rs (100%)
 create mode 100644 charge-controller-supervisor/src/main.rs
 rename {src/lib/charge_controllers => charge-controller-supervisor/src}/pl.rs (67%)
 rename {src/lib/charge_controllers => charge-controller-supervisor/src}/tristar.rs (77%)
 create mode 100644 charge-controller-supervisor/src/web.rs
 delete mode 100644 pkg/debian/.empty
 delete mode 100644 src/bins/controllers/main.rs
 delete mode 100644 src/bins/tesla/main.rs
 delete mode 100644 src/lib/api_interface.rs
 delete mode 100644 src/lib/charge_controllers/mod.rs
 delete mode 100644 src/lib/config.rs
 delete mode 100644 src/lib/errors.rs
 delete mode 100644 src/lib/lib.rs
 delete mode 100644 src/lib/tesla_charge_rate.rs
 create mode 100644 tesla-charge-controller/Cargo.toml
 rename pkg/systemd/tesla-charge-controller.service => tesla-charge-controller/debian/service (100%)
 rename {src/lib/api_interface => tesla-charge-controller/src/api}/http.rs (78%)
 rename src/lib/types.rs => tesla-charge-controller/src/api/mod.rs (56%)
 create mode 100644 tesla-charge-controller/src/config.rs
 create mode 100644 tesla-charge-controller/src/control.rs
 create mode 100644 tesla-charge-controller/src/main.rs
 rename {src/lib => tesla-charge-controller/src}/server/mod.rs (54%)
 rename {src/lib => tesla-charge-controller/src}/server/static_handler.rs (100%)

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