diff --git a/Cargo.lock b/Cargo.lock index 1cf1073..8f6ae81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -972,6 +972,16 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "io-kit-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4769cb30e5dcf1710fc6730d3e94f78c47723a014a567de385e113c737394640" +dependencies = [ + "core-foundation-sys", + "mach2", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1022,6 +1032,26 @@ version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linux-raw-sys" version = "0.4.12" @@ -1059,6 +1089,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1215,6 +1254,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1970,6 +2020,25 @@ dependencies = [ "serde", ] +[[package]] +name = "serialport" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5a15d0be940df84846264b09b51b10b931fb2f275becb80934e3568a016828" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix", + "regex", + "scopeguard", + "unescaper", + "winapi", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2193,7 +2262,7 @@ dependencies = [ [[package]] name = "tesla-charge-controller" -version = "0.1.8-prerelease" +version = "0.1.8" dependencies = [ "anyhow", "async-channel", @@ -2208,6 +2277,7 @@ dependencies = [ "rocket", "ron", "serde", + "serialport", "teslatte", "thiserror", "tokio", @@ -2529,6 +2599,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "unescaper" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f0f68e58d297ba8b22b8b5a96a87b863ba6bb46aaf51e19a4b02c5a6dd5b7f" +dependencies = [ + "thiserror", +] + [[package]] name = "unicode-bidi" version = "0.3.14" diff --git a/Cargo.toml b/Cargo.toml index bede720..5663873 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tesla-charge-controller" -version = "0.1.8-prerelease" +version = "0.1.8" edition = "2021" license = "MITNFA" description = "Controls Tesla charge rate based on solar charge data" @@ -29,3 +29,4 @@ metrics-prometheus = "0.6.0" prometheus = "0.13" env_logger = "0.10" log = "0.4" +serialport = "4.3" diff --git a/src/api_interface.rs b/src/api_interface.rs index 22f8696..d504713 100644 --- a/src/api_interface.rs +++ b/src/api_interface.rs @@ -1,6 +1,5 @@ use anyhow::{Context, Result}; use metrics::{describe_gauge, gauge, Gauge, Unit}; -use metrics_prometheus::Recorder; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, @@ -25,9 +24,7 @@ struct Metrics { } impl Metrics { - fn new() -> (Self, Recorder) { - let recorder = metrics_prometheus::install(); - + fn new() -> Self { describe_gauge!("tesla_battery_level", Unit::Percent, "Battery level"); let battery_level = gauge!("tesla_battery_level"); describe_gauge!("tesla_charge_rate", "Charge rate"); @@ -41,17 +38,14 @@ impl Metrics { describe_gauge!("tesla_battery_heater", "Battery heater"); let battery_heater = gauge!("tesla_battery_heater"); - ( - Self { - battery_level, - charge_rate, - charge_request, - inside_temp, - outside_temp, - battery_heater, - }, - recorder, - ) + Self { + battery_level, + charge_rate, + charge_request, + inside_temp, + outside_temp, + battery_heater, + } } } @@ -97,7 +91,7 @@ impl TeslaInterface { .next() .context("No vehicles attached to account!")?; - let (metrics, _recorder) = Metrics::new(); + let metrics = Metrics::new(); let interface = Self { state: Arc::new(RwLock::new(Default::default())), diff --git a/src/config.rs b/src/config.rs index 4506a19..d716c9c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,8 @@ use crate::types::Coords; pub struct Config { pub watch_interval: Duration, pub coords: Coords, + pub serial_port: String, + pub baud_rate: u32, } impl Default for Config { @@ -18,6 +20,8 @@ impl Default for Config { latitude: 0., longitude: 0., }, + serial_port: String::from("/dev/ttyUSB0"), + baud_rate: 9600, } } } diff --git a/src/main.rs b/src/main.rs index 9f259b5..7918285 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ extern crate rocket; use api_interface::TeslaInterface; use clap::{Parser, Subcommand}; +use pl_interface::Pli; use std::path::PathBuf; use crate::config::Config; @@ -12,6 +13,7 @@ use crate::config::Config; mod api_interface; mod config; mod errors; +mod pl_interface; mod server; mod types; @@ -40,6 +42,8 @@ async fn main() { let auth_path = args.config_dir.join("auth"); let config_path = args.config_dir.join("config"); + let _recorder = metrics_prometheus::install(); + match args.command { Commands::GenerateConfig => { println!( @@ -56,7 +60,7 @@ async fn main() { let (api_requests, receiver) = async_channel::unbounded(); let server_handle = server::launch_server(server::ServerState { - config, + config: config.clone(), state: interface.state.clone(), api_requests, }); @@ -76,6 +80,21 @@ async fn main() { } }); + // try to spawn the pli loop + match Pli::new(config.serial_port, config.baud_rate) { + Ok(mut pli) => { + tokio::task::spawn(async move { + let mut interval = + tokio::time::interval(std::time::Duration::from_secs(30)); + loop { + interval.tick().await; + pli.refresh(); + } + }); + } + Err(e) => log::error!("Error connecting to serial device for PLI: {e:?}"), + } + server_handle.await; } Err(e) => error!("{}", e.error_string()), diff --git a/src/pl_interface.rs b/src/pl_interface.rs new file mode 100644 index 0000000..9af7b14 --- /dev/null +++ b/src/pl_interface.rs @@ -0,0 +1,55 @@ +use std::time::Duration; + +use metrics::{describe_gauge, gauge, Gauge}; +use serialport::SerialPort; + +pub struct Pli { + port: Box, + voltage_gauge: Gauge, +} + +impl Pli { + pub fn new(serial_port: String, baud_rate: u32) -> anyhow::Result { + let port = serialport::new(serial_port, baud_rate) + .timeout(Duration::from_millis(250)) + .open()?; + + describe_gauge!("pl_battery_voltage", "Battery voltage"); + let voltage_gauge = gauge!("pl_battery_voltage"); + Ok(Self { + port, + voltage_gauge, + }) + } + + pub fn refresh(&mut self) { + let batv = (self.read_ram(50) as f64) * (4. / 10.); + self.voltage_gauge.set(batv); + } + + fn send_command(&mut self, req: [u8; 4]) { + self.port + .write_all(&req) + .expect("failed to write to serial port"); + } + + fn receive(&mut self) -> [u8; LENGTH] { + let mut buf = [0; LENGTH]; + match self.port.read_exact(&mut buf) { + Ok(_) => { + println!("got buf {buf:?}") + } + Err(e) => println!("read error: {e:#?}"), + } + buf + } + + fn read_ram(&mut self, address: u8) -> u8 { + self.send_command(command(20, address, 0)); + self.receive::<2>()[1] + } +} + +fn command(operation: u8, address: u8, data: u8) -> [u8; 4] { + [operation, address, data, !operation] +}