variable tesla refresh period + low voltage timeout + better valid rate checking

This commit is contained in:
Alex Janka 2024-01-16 12:28:15 +11:00
parent 79454ff95a
commit e7a07b30ae
6 changed files with 68 additions and 24 deletions

2
Cargo.lock generated
View file

@ -2256,7 +2256,7 @@ dependencies = [
[[package]] [[package]]
name = "tesla-charge-controller" name = "tesla-charge-controller"
version = "1.0.0-beta-2" version = "1.0.0-beta-3"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"chrono", "chrono",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tesla-charge-controller" name = "tesla-charge-controller"
version = "1.0.0-beta-2" version = "1.0.0-beta-3"
edition = "2021" edition = "2021"
license = "MITNFA" license = "MITNFA"
description = "Controls Tesla charge rate based on solar charge data" description = "Controls Tesla charge rate based on solar charge data"

View file

@ -16,12 +16,14 @@ pub fn access_config<'a>() -> &'a Config {
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
pub tesla_update_interval_seconds: u64, pub tesla_update_interval_seconds: u64,
pub tesla_update_interval_while_charging_seconds: u64,
pub pl_watch_interval_seconds: u64, pub pl_watch_interval_seconds: u64,
pub pl_timeout_milliseconds: u64, pub pl_timeout_milliseconds: u64,
pub coords: Coords, pub coords: Coords,
pub serial_port: String, pub serial_port: String,
pub baud_rate: u32, pub baud_rate: u32,
pub shutoff_voltage: f64, pub shutoff_voltage: f64,
pub shutoff_voltage_time_seconds: u64,
pub min_rate: i64, pub min_rate: i64,
pub max_rate: i64, pub max_rate: i64,
pub duty_cycle_too_high: f64, pub duty_cycle_too_high: f64,
@ -32,6 +34,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
tesla_update_interval_seconds: 120, tesla_update_interval_seconds: 120,
tesla_update_interval_while_charging_seconds: 5,
pl_watch_interval_seconds: 30, pl_watch_interval_seconds: 30,
pl_timeout_milliseconds: 250, pl_timeout_milliseconds: 250,
coords: Coords { coords: Coords {
@ -41,6 +44,7 @@ impl Default for Config {
serial_port: String::from("/dev/ttyUSB0"), serial_port: String::from("/dev/ttyUSB0"),
baud_rate: 9600, baud_rate: 9600,
shutoff_voltage: 52., shutoff_voltage: 52.,
shutoff_voltage_time_seconds: 15,
min_rate: 5, min_rate: 5,
max_rate: 10, max_rate: 10,
duty_cycle_too_high: 0.9, duty_cycle_too_high: 0.9,

View file

@ -127,15 +127,26 @@ async fn main() {
let mut interval = tokio::time::interval(std::time::Duration::from_secs( let mut interval = tokio::time::interval(std::time::Duration::from_secs(
config.tesla_update_interval_seconds, 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 { loop {
// await either the next interval OR a message from the other thread // await either the next interval OR a message from the other thread
tokio::select! { tokio::select! {
_ = interval.tick() => { _ = interval.tick() => {
if let Some(request) = tesla_charge_rate_controller.control_charge_rate() { if !interface.state.read().unwrap().is_charging_at_home() {
interface.process_request(request).await; 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 { api_message = api_receiver.recv() => match api_message {
Ok(message) => interface.process_request(message).await, Ok(message) => interface.process_request(message).await,
Err(e) => panic!("Error on Tesla receive channel: {e:?}") Err(e) => panic!("Error on Tesla receive channel: {e:?}")

View file

@ -1,4 +1,7 @@
use std::sync::{Arc, RwLock}; use std::{
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use metrics::{describe_gauge, gauge, Gauge}; use metrics::{describe_gauge, gauge, Gauge};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -15,6 +18,7 @@ pub struct TeslaChargeRateController {
pub car_state: Arc<RwLock<CarState>>, pub car_state: Arc<RwLock<CarState>>,
pub pl_state: Option<Arc<RwLock<PlState>>>, pub pl_state: Option<Arc<RwLock<PlState>>>,
pub tcrc_state: Arc<RwLock<TcrcState>>, pub tcrc_state: Arc<RwLock<TcrcState>>,
voltage_low: Option<Instant>,
control_enable_gauge: Gauge, control_enable_gauge: Gauge,
} }
@ -26,12 +30,14 @@ pub enum TcrcRequest {
#[derive(Clone, Copy, Serialize, Deserialize, Debug)] #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct TcrcState { pub struct TcrcState {
pub control_enable: bool, pub control_enable: bool,
pub currently_controlling: bool,
} }
impl Default for TcrcState { impl Default for TcrcState {
fn default() -> Self { fn default() -> Self {
Self { Self {
control_enable: true, control_enable: true,
currently_controlling: false,
} }
} }
} }
@ -43,6 +49,7 @@ impl TeslaChargeRateController {
car_state, car_state,
pl_state, pl_state,
tcrc_state: Default::default(), tcrc_state: Default::default(),
voltage_low: None,
control_enable_gauge: gauge!("tcrc_control_enable"), control_enable_gauge: gauge!("tcrc_control_enable"),
} }
} }
@ -52,14 +59,26 @@ impl TeslaChargeRateController {
if let Ok(car_state) = self.car_state.read() { if let Ok(car_state) = self.car_state.read() {
if let Some(charge_state) = car_state.charge_state { if let Some(charge_state) = car_state.charge_state {
// check if we're charging at home // check if we're charging at home
if charge_state.charging_state == ChargingState::Charging if car_state.is_charging_at_home() {
&& car_state.location_data.is_some_and(|v| v.home)
{
// automatic control or not, check if we're below shutoff voltage // automatic control or not, check if we're below shutoff voltage
if pl_state.battery_voltage < access_config().shutoff_voltage { if pl_state.battery_voltage < access_config().shutoff_voltage {
return Some(InterfaceRequest::StopCharge); if let Some(low_voltage_time) = self.voltage_low {
} else if self.tcrc_state.read().is_ok_and(|v| v.control_enable) { // if we've been below shutoff for long enough, stop charging
return get_control(&pl_state, &charge_state); 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 { } else if charge_state.charging_state == ChargingState::Disconnected {
// only disable automatic control until the next time we're unplugged // 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<InterfaceRequest> { fn get_control(pl_state: &PlState, charge_state: &ChargeState) -> Option<InterfaceRequest> {
let config = access_config(); let config = access_config();
if pl_state.duty_cycle > config.duty_cycle_too_high { if pl_state.duty_cycle > config.duty_cycle_too_high {
let new_rate = charge_state.charge_amps - 1; let rate = charge_state.charge_amps - 2;
return if new_rate >= config.min_rate { return valid_rate(rate, charge_state.charge_amps).map(InterfaceRequest::SetChargeRate);
Some(InterfaceRequest::SetChargeRate(new_rate))
} else {
None
};
} else if pl_state.duty_cycle < config.duty_cycle_too_low { } else if pl_state.duty_cycle < config.duty_cycle_too_low {
let new_rate = charge_state.charge_amps + 1; let rate = charge_state.charge_amps + 1;
return if new_rate <= config.max_rate { return valid_rate(rate, charge_state.charge_amps).map(InterfaceRequest::SetChargeRate);
Some(InterfaceRequest::SetChargeRate(new_rate))
} else {
None
};
} }
None None
} }
fn valid_rate(rate: i64, other: i64) -> Option<i64> {
let config = access_config();
let new = rate.min(config.max_rate).max(config.min_rate);
if new == other {
None
} else {
Some(new)
}
}

View file

@ -11,6 +11,14 @@ pub struct CarState {
pub climate_state: Option<ClimateState>, pub climate_state: Option<ClimateState>,
} }
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)] #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct ClimateState { pub struct ClimateState {
pub inside_temp: f64, pub inside_temp: f64,