variable tesla refresh period + low voltage timeout + better valid rate checking
This commit is contained in:
parent
79454ff95a
commit
e7a07b30ae
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -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 !interface.state.read().unwrap().is_charging_at_home() {
|
||||||
|
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() {
|
if let Some(request) = tesla_charge_rate_controller.control_charge_rate() {
|
||||||
interface.process_request(request).await;
|
interface.process_request(request).await;
|
||||||
}
|
}
|
||||||
interface.refresh().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:?}")
|
||||||
|
|
|
@ -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,15 +59,27 @@ 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 {
|
||||||
|
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);
|
return Some(InterfaceRequest::StopCharge);
|
||||||
} else if self.tcrc_state.read().is_ok_and(|v| v.control_enable) {
|
}
|
||||||
|
} 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);
|
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
|
||||||
self.tcrc_state
|
self.tcrc_state
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue