diff --git a/Cargo.lock b/Cargo.lock index c58b897..5244310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2256,7 +2256,7 @@ dependencies = [ [[package]] name = "tesla-charge-controller" -version = "1.0.0-beta-2" +version = "1.0.0-beta-3" dependencies = [ "async-channel", "chrono", diff --git a/Cargo.toml b/Cargo.toml index e40bc85..7ba285d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tesla-charge-controller" -version = "1.0.0-beta-2" +version = "1.0.0-beta-3" edition = "2021" license = "MITNFA" description = "Controls Tesla charge rate based on solar charge data" diff --git a/src/config.rs b/src/config.rs index 9464079..8fdcc29 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,12 +16,14 @@ pub fn access_config<'a>() -> &'a Config { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { pub tesla_update_interval_seconds: u64, + pub tesla_update_interval_while_charging_seconds: u64, pub pl_watch_interval_seconds: u64, pub pl_timeout_milliseconds: u64, pub coords: Coords, 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, @@ -32,6 +34,7 @@ impl Default for Config { fn default() -> Self { Self { tesla_update_interval_seconds: 120, + tesla_update_interval_while_charging_seconds: 5, pl_watch_interval_seconds: 30, pl_timeout_milliseconds: 250, coords: Coords { @@ -41,6 +44,7 @@ impl Default for Config { serial_port: String::from("/dev/ttyUSB0"), baud_rate: 9600, shutoff_voltage: 52., + shutoff_voltage_time_seconds: 15, min_rate: 5, max_rate: 10, duty_cycle_too_high: 0.9, diff --git a/src/main.rs b/src/main.rs index d74a648..c998bf1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,15 +127,26 @@ async fn main() { let mut interval = tokio::time::interval(std::time::Duration::from_secs( config.tesla_update_interval_seconds, )); + let mut charge_interval = + tokio::time::interval(std::time::Duration::from_secs( + config.tesla_update_interval_while_charging_seconds, + )); loop { // await either the next interval OR a message from the other thread tokio::select! { _ = interval.tick() => { - if let Some(request) = tesla_charge_rate_controller.control_charge_rate() { - interface.process_request(request).await; + if !interface.state.read().unwrap().is_charging_at_home() { + interface.refresh().await } - interface.refresh().await }, + _ = charge_interval.tick() => { + if interface.state.read().unwrap().is_charging_at_home() { + if let Some(request) = tesla_charge_rate_controller.control_charge_rate() { + interface.process_request(request).await; + } + interface.refresh().await + } + } api_message = api_receiver.recv() => match api_message { Ok(message) => interface.process_request(message).await, Err(e) => panic!("Error on Tesla receive channel: {e:?}") diff --git a/src/tesla_charge_rate.rs b/src/tesla_charge_rate.rs index 85099ec..abc4776 100644 --- a/src/tesla_charge_rate.rs +++ b/src/tesla_charge_rate.rs @@ -1,4 +1,7 @@ -use std::sync::{Arc, RwLock}; +use std::{ + sync::{Arc, RwLock}, + time::{Duration, Instant}, +}; use metrics::{describe_gauge, gauge, Gauge}; use serde::{Deserialize, Serialize}; @@ -15,6 +18,7 @@ pub struct TeslaChargeRateController { pub car_state: Arc>, pub pl_state: Option>>, pub tcrc_state: Arc>, + voltage_low: Option, control_enable_gauge: Gauge, } @@ -26,12 +30,14 @@ pub enum TcrcRequest { #[derive(Clone, Copy, Serialize, Deserialize, Debug)] pub struct TcrcState { pub control_enable: bool, + pub currently_controlling: bool, } impl Default for TcrcState { fn default() -> Self { Self { control_enable: true, + currently_controlling: false, } } } @@ -43,6 +49,7 @@ impl TeslaChargeRateController { car_state, pl_state, tcrc_state: Default::default(), + voltage_low: None, control_enable_gauge: gauge!("tcrc_control_enable"), } } @@ -52,14 +59,26 @@ impl TeslaChargeRateController { if let Ok(car_state) = self.car_state.read() { if let Some(charge_state) = car_state.charge_state { // check if we're charging at home - if charge_state.charging_state == ChargingState::Charging - && car_state.location_data.is_some_and(|v| v.home) - { + if car_state.is_charging_at_home() { // automatic control or not, check if we're below shutoff voltage if pl_state.battery_voltage < access_config().shutoff_voltage { - return Some(InterfaceRequest::StopCharge); - } else if self.tcrc_state.read().is_ok_and(|v| v.control_enable) { - return get_control(&pl_state, &charge_state); + if let Some(low_voltage_time) = self.voltage_low { + // if we've been below shutoff for long enough, stop charging + if Instant::now().duration_since(low_voltage_time) + >= Duration::from_secs( + access_config().shutoff_voltage_time_seconds, + ) + { + return Some(InterfaceRequest::StopCharge); + } + } else { + self.voltage_low = Some(Instant::now()); + } + } else { + self.voltage_low = None; + if self.tcrc_state.read().is_ok_and(|v| v.control_enable) { + return get_control(&pl_state, &charge_state); + } } } else if charge_state.charging_state == ChargingState::Disconnected { // only disable automatic control until the next time we're unplugged @@ -98,20 +117,22 @@ impl TeslaChargeRateController { fn get_control(pl_state: &PlState, charge_state: &ChargeState) -> Option { let config = access_config(); if pl_state.duty_cycle > config.duty_cycle_too_high { - let new_rate = charge_state.charge_amps - 1; - return if new_rate >= config.min_rate { - Some(InterfaceRequest::SetChargeRate(new_rate)) - } else { - None - }; + let rate = charge_state.charge_amps - 2; + return valid_rate(rate, charge_state.charge_amps).map(InterfaceRequest::SetChargeRate); } else if pl_state.duty_cycle < config.duty_cycle_too_low { - let new_rate = charge_state.charge_amps + 1; - return if new_rate <= config.max_rate { - Some(InterfaceRequest::SetChargeRate(new_rate)) - } else { - None - }; + let rate = charge_state.charge_amps + 1; + return valid_rate(rate, charge_state.charge_amps).map(InterfaceRequest::SetChargeRate); } None } + +fn valid_rate(rate: i64, other: i64) -> Option { + let config = access_config(); + let new = rate.min(config.max_rate).max(config.min_rate); + if new == other { + None + } else { + Some(new) + } +} diff --git a/src/types.rs b/src/types.rs index 1767cf9..888411c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -11,6 +11,14 @@ pub struct CarState { pub climate_state: Option, } +impl CarState { + pub fn is_charging_at_home(&self) -> bool { + self.charge_state + .is_some_and(|v| v.charging_state == ChargingState::Charging) + && self.location_data.is_some_and(|v| v.home) + } +} + #[derive(Clone, Copy, Serialize, Deserialize, Debug)] pub struct ClimateState { pub inside_temp: f64,