automatic control beta 1
This commit is contained in:
parent
4f29b3fcf8
commit
560fbf3d7a
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2256,7 +2256,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tesla-charge-controller"
|
name = "tesla-charge-controller"
|
||||||
version = "0.2.8"
|
version = "1.0.0-beta-1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tesla-charge-controller"
|
name = "tesla-charge-controller"
|
||||||
version = "0.2.8"
|
version = "1.0.0-beta-1"
|
||||||
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"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use metrics::{describe_gauge, gauge, Gauge, Unit};
|
use metrics::{describe_gauge, gauge, Gauge, Label, Unit};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
@ -7,7 +7,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use teslatte::{
|
use teslatte::{
|
||||||
auth::{AccessToken, RefreshToken},
|
auth::{AccessToken, RefreshToken},
|
||||||
vehicles::{Endpoint, GetVehicleData},
|
vehicles::{ChargingState, Endpoint, GetVehicleData, SetChargingAmps},
|
||||||
FleetApi, FleetVehicleApi,
|
FleetApi, FleetVehicleApi,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ struct Metrics {
|
||||||
preconditioning: Gauge,
|
preconditioning: Gauge,
|
||||||
remote_heater_control_enabled: Gauge,
|
remote_heater_control_enabled: Gauge,
|
||||||
tesla_online: Gauge,
|
tesla_online: Gauge,
|
||||||
|
charging_state: ChargingStateGauges,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metrics {
|
impl Metrics {
|
||||||
|
@ -57,6 +58,8 @@ impl Metrics {
|
||||||
let remote_heater_control_enabled = gauge!("tesla_remote_heater_control_enabled");
|
let remote_heater_control_enabled = gauge!("tesla_remote_heater_control_enabled");
|
||||||
describe_gauge!("tesla_online", "Tesla online");
|
describe_gauge!("tesla_online", "Tesla online");
|
||||||
let tesla_online = gauge!("tesla_online");
|
let tesla_online = gauge!("tesla_online");
|
||||||
|
describe_gauge!("tesla_charging_state", "Tesla charging state");
|
||||||
|
let charging_state = ChargingStateGauges::new();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
battery_level,
|
battery_level,
|
||||||
|
@ -71,6 +74,70 @@ impl Metrics {
|
||||||
preconditioning,
|
preconditioning,
|
||||||
remote_heater_control_enabled,
|
remote_heater_control_enabled,
|
||||||
tesla_online,
|
tesla_online,
|
||||||
|
charging_state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChargingStateGauges {
|
||||||
|
charging: Gauge,
|
||||||
|
stopped: Gauge,
|
||||||
|
disconnected: Gauge,
|
||||||
|
other: Gauge,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChargingStateGauges {
|
||||||
|
fn new() -> Self {
|
||||||
|
let charging = gauge!(
|
||||||
|
"tesla_charging_state",
|
||||||
|
vec![Label::new("state", String::from("charging"))]
|
||||||
|
);
|
||||||
|
let stopped = gauge!(
|
||||||
|
"tesla_charging_state",
|
||||||
|
vec![Label::new("state", String::from("stopped"))]
|
||||||
|
);
|
||||||
|
let disconnected = gauge!(
|
||||||
|
"tesla_charging_state",
|
||||||
|
vec![Label::new("state", String::from("disconnected"))]
|
||||||
|
);
|
||||||
|
let other = gauge!(
|
||||||
|
"tesla_charging_state",
|
||||||
|
vec![Label::new("state", String::from("other"))]
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
charging,
|
||||||
|
stopped,
|
||||||
|
disconnected,
|
||||||
|
other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, state: ChargingState) {
|
||||||
|
match state {
|
||||||
|
ChargingState::Charging => {
|
||||||
|
self.charging.set(1.);
|
||||||
|
self.stopped.set(0.);
|
||||||
|
self.disconnected.set(0.);
|
||||||
|
self.other.set(0.);
|
||||||
|
}
|
||||||
|
ChargingState::Stopped => {
|
||||||
|
self.charging.set(0.);
|
||||||
|
self.stopped.set(1.);
|
||||||
|
self.disconnected.set(0.);
|
||||||
|
self.other.set(0.);
|
||||||
|
}
|
||||||
|
ChargingState::Disconnected => {
|
||||||
|
self.charging.set(0.);
|
||||||
|
self.stopped.set(0.);
|
||||||
|
self.disconnected.set(1.);
|
||||||
|
self.other.set(0.);
|
||||||
|
}
|
||||||
|
ChargingState::Other => {
|
||||||
|
self.charging.set(0.);
|
||||||
|
self.stopped.set(0.);
|
||||||
|
self.disconnected.set(0.);
|
||||||
|
self.other.set(1.);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +149,6 @@ pub struct TeslaInterface {
|
||||||
last_refresh: Instant,
|
last_refresh: Instant,
|
||||||
auth_path: PathBuf,
|
auth_path: PathBuf,
|
||||||
metrics: Metrics,
|
metrics: Metrics,
|
||||||
last_charging_state: String,
|
|
||||||
last_conn_charge_cable: String,
|
last_conn_charge_cable: String,
|
||||||
last_cop_state: String,
|
last_cop_state: String,
|
||||||
last_climate_keeper: String,
|
last_climate_keeper: String,
|
||||||
|
@ -99,6 +165,8 @@ struct AuthInfo {
|
||||||
// these are the messages that the webserver can send the api thread
|
// these are the messages that the webserver can send the api thread
|
||||||
pub enum InterfaceRequest {
|
pub enum InterfaceRequest {
|
||||||
FlashLights,
|
FlashLights,
|
||||||
|
StopCharge,
|
||||||
|
SetChargeRate(i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEY_REFRESH_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60);
|
const KEY_REFRESH_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60);
|
||||||
|
@ -130,7 +198,6 @@ impl TeslaInterface {
|
||||||
auth_path,
|
auth_path,
|
||||||
vehicle,
|
vehicle,
|
||||||
metrics,
|
metrics,
|
||||||
last_charging_state: String::new(),
|
|
||||||
last_conn_charge_cable: String::new(),
|
last_conn_charge_cable: String::new(),
|
||||||
last_cop_state: String::new(),
|
last_cop_state: String::new(),
|
||||||
last_climate_keeper: String::new(),
|
last_climate_keeper: String::new(),
|
||||||
|
@ -164,6 +231,18 @@ impl TeslaInterface {
|
||||||
InterfaceRequest::FlashLights => {
|
InterfaceRequest::FlashLights => {
|
||||||
let _ = self.api.flash_lights(&self.vehicle.vin).await;
|
let _ = self.api.flash_lights(&self.vehicle.vin).await;
|
||||||
}
|
}
|
||||||
|
InterfaceRequest::StopCharge => match self.api.charge_stop(&self.vehicle.vin).await {
|
||||||
|
Ok(_) => log::warn!("Successfully stopped charge"),
|
||||||
|
Err(e) => log::error!("Error stopping charge: {e:?}"),
|
||||||
|
},
|
||||||
|
InterfaceRequest::SetChargeRate(charging_amps) => match self
|
||||||
|
.api
|
||||||
|
.set_charging_amps(&self.vehicle.vin, &SetChargingAmps { charging_amps })
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => log::warn!("Successfully set charge rate to {charging_amps}"),
|
||||||
|
Err(e) => log::error!("Error setting charge rate: {e:?}"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,6 +269,9 @@ impl TeslaInterface {
|
||||||
self.metrics
|
self.metrics
|
||||||
.charger_connected
|
.charger_connected
|
||||||
.set(bf(new_charge_state.charger_connected));
|
.set(bf(new_charge_state.charger_connected));
|
||||||
|
self.metrics
|
||||||
|
.charging_state
|
||||||
|
.set(new_charge_state.charging_state);
|
||||||
state.charge_state = Some(new_charge_state);
|
state.charge_state = Some(new_charge_state);
|
||||||
}
|
}
|
||||||
if let Some(new_location_data) = new_state.location_data {
|
if let Some(new_location_data) = new_state.location_data {
|
||||||
|
@ -282,10 +364,6 @@ impl TeslaInterface {
|
||||||
log::warn!("Current conn charge cable: \"{}\"", v.conn_charge_cable);
|
log::warn!("Current conn charge cable: \"{}\"", v.conn_charge_cable);
|
||||||
self.last_conn_charge_cable = v.conn_charge_cable.clone();
|
self.last_conn_charge_cable = v.conn_charge_cable.clone();
|
||||||
}
|
}
|
||||||
if self.last_charging_state != v.charging_state {
|
|
||||||
log::warn!("Current charging state: \"{}\"", v.charging_state);
|
|
||||||
self.last_charging_state = v.charging_state.clone();
|
|
||||||
}
|
|
||||||
v.into()
|
v.into()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,18 +15,23 @@ 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_watch_interval_seconds: u64,
|
pub tesla_update_interval_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 min_rate: i64,
|
||||||
|
pub max_rate: i64,
|
||||||
|
pub duty_cycle_too_high: f64,
|
||||||
|
pub duty_cycle_too_low: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
tesla_watch_interval_seconds: 120,
|
tesla_update_interval_seconds: 120,
|
||||||
pl_watch_interval_seconds: 30,
|
pl_watch_interval_seconds: 30,
|
||||||
pl_timeout_milliseconds: 250,
|
pl_timeout_milliseconds: 250,
|
||||||
coords: Coords {
|
coords: Coords {
|
||||||
|
@ -35,6 +40,11 @@ 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.,
|
||||||
|
min_rate: 5,
|
||||||
|
max_rate: 10,
|
||||||
|
duty_cycle_too_high: 0.9,
|
||||||
|
duty_cycle_too_low: 0.7,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
25
src/main.rs
25
src/main.rs
|
@ -9,6 +9,7 @@ use clap::{Parser, Subcommand};
|
||||||
use config::{access_config, CONFIG_PATH};
|
use config::{access_config, CONFIG_PATH};
|
||||||
use errors::PrintErrors;
|
use errors::PrintErrors;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use tesla_charge_rate::TeslaChargeRateController;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ mod charge_controllers;
|
||||||
mod config;
|
mod config;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod tesla_charge_rate;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
@ -73,6 +75,9 @@ async fn main() {
|
||||||
let (api_requests, api_receiver) = async_channel::unbounded();
|
let (api_requests, api_receiver) = async_channel::unbounded();
|
||||||
// and to the pli thread
|
// and to the pli thread
|
||||||
let (pli_requests, pli_receiver) = async_channel::unbounded();
|
let (pli_requests, pli_receiver) = async_channel::unbounded();
|
||||||
|
// and to the charge rate controller thread
|
||||||
|
let (tcrc_requests, tcrc_receiver) = async_channel::unbounded();
|
||||||
|
|
||||||
charge_controllers::register_metrics();
|
charge_controllers::register_metrics();
|
||||||
|
|
||||||
// try to spawn the pli loop
|
// try to spawn the pli loop
|
||||||
|
@ -105,25 +110,39 @@ async fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut tesla_charge_rate_controller =
|
||||||
|
TeslaChargeRateController::new(interface.state.clone(), pl_state.clone());
|
||||||
|
|
||||||
let server_handle = server::launch_server(server::ServerState {
|
let server_handle = server::launch_server(server::ServerState {
|
||||||
car_state: interface.state.clone(),
|
car_state: interface.state.clone(),
|
||||||
pl_state,
|
pl_state,
|
||||||
|
tcrc_state: tesla_charge_rate_controller.tcrc_state.clone(),
|
||||||
api_requests,
|
api_requests,
|
||||||
pli_requests,
|
pli_requests,
|
||||||
|
tcrc_requests,
|
||||||
});
|
});
|
||||||
|
|
||||||
// spawn the api loop
|
// spawn the api loop
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
|
||||||
config.tesla_watch_interval_seconds,
|
config.tesla_update_interval_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() => interface.refresh().await,
|
_ = interval.tick() => {
|
||||||
message = api_receiver.recv() => match message {
|
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,
|
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:?}")
|
||||||
|
},
|
||||||
|
tcrc_message = tcrc_receiver.recv() => match tcrc_message {
|
||||||
|
Ok(message) => tesla_charge_rate_controller.process_request(message),
|
||||||
|
Err(e) => panic!("Error on TCRC receive channel: {e:?}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
||||||
api_interface::InterfaceRequest,
|
api_interface::InterfaceRequest,
|
||||||
charge_controllers::pl::{PlState, PliRequest},
|
charge_controllers::pl::{PlState, PliRequest},
|
||||||
errors::ServerError,
|
errors::ServerError,
|
||||||
|
tesla_charge_rate::{TcrcRequest, TcrcState},
|
||||||
types::CarState,
|
types::CarState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,9 +22,11 @@ mod static_handler;
|
||||||
|
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub car_state: Arc<RwLock<CarState>>,
|
pub car_state: Arc<RwLock<CarState>>,
|
||||||
|
pub tcrc_state: Arc<RwLock<TcrcState>>,
|
||||||
pub pl_state: Option<Arc<RwLock<PlState>>>,
|
pub pl_state: Option<Arc<RwLock<PlState>>>,
|
||||||
pub api_requests: Sender<InterfaceRequest>,
|
pub api_requests: Sender<InterfaceRequest>,
|
||||||
pub pli_requests: Sender<PliRequest>,
|
pub pli_requests: Sender<PliRequest>,
|
||||||
|
pub tcrc_requests: Sender<TcrcRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn launch_server(state: ServerState) {
|
pub async fn launch_server(state: ServerState) {
|
||||||
|
@ -49,7 +52,17 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
|
||||||
.mount("/", fileserver)
|
.mount("/", fileserver)
|
||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
routes![home, car_state, regulator_state, flash, metrics, read_ram,],
|
routes![
|
||||||
|
home,
|
||||||
|
car_state,
|
||||||
|
regulator_state,
|
||||||
|
control_state,
|
||||||
|
flash,
|
||||||
|
disable_control,
|
||||||
|
enable_control,
|
||||||
|
metrics,
|
||||||
|
read_ram
|
||||||
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,11 +81,40 @@ async fn car_state(state: &State<ServerState>) -> Result<Json<CarState>, ServerE
|
||||||
Ok(Json(*state.car_state.read()?))
|
Ok(Json(*state.car_state.read()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/control-state")]
|
||||||
|
async fn control_state(state: &State<ServerState>) -> Result<Json<TcrcState>, ServerError> {
|
||||||
|
Ok(Json(*state.tcrc_state.read()?))
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/flash")]
|
#[post("/flash")]
|
||||||
async fn flash(state: &State<ServerState>) {
|
async fn flash(state: &State<ServerState>) {
|
||||||
let _ = state.api_requests.send(InterfaceRequest::FlashLights).await;
|
let _ = state.api_requests.send(InterfaceRequest::FlashLights).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/disable-control")]
|
||||||
|
async fn disable_control(state: &State<ServerState>) {
|
||||||
|
match state
|
||||||
|
.tcrc_requests
|
||||||
|
.send(TcrcRequest::DisableAutomaticControl)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => log::error!("Error sending stop control request: {e:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/enable-control")]
|
||||||
|
async fn enable_control(state: &State<ServerState>) {
|
||||||
|
match state
|
||||||
|
.tcrc_requests
|
||||||
|
.send(TcrcRequest::EnableAutomaticControl)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => log::error!("Error sending stop control request: {e:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/metrics")]
|
#[get("/metrics")]
|
||||||
fn metrics() -> Result<String, ServerError> {
|
fn metrics() -> Result<String, ServerError> {
|
||||||
Ok(
|
Ok(
|
||||||
|
|
109
src/tesla_charge_rate.rs
Normal file
109
src/tesla_charge_rate.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use metrics::{describe_gauge, gauge, Gauge};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use teslatte::vehicles::ChargingState;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api_interface::InterfaceRequest,
|
||||||
|
charge_controllers::pl::PlState,
|
||||||
|
config::access_config,
|
||||||
|
types::{CarState, ChargeState},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct TeslaChargeRateController {
|
||||||
|
pub car_state: Arc<RwLock<CarState>>,
|
||||||
|
pub pl_state: Option<Arc<RwLock<PlState>>>,
|
||||||
|
pub tcrc_state: Arc<RwLock<TcrcState>>,
|
||||||
|
control_enable_gauge: Gauge,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TcrcRequest {
|
||||||
|
DisableAutomaticControl,
|
||||||
|
EnableAutomaticControl,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct TcrcState {
|
||||||
|
pub control_enable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TeslaChargeRateController {
|
||||||
|
pub fn new(car_state: Arc<RwLock<CarState>>, pl_state: Option<Arc<RwLock<PlState>>>) -> Self {
|
||||||
|
describe_gauge!("tcrc_control_enable", "Enable Tesla charge rate control");
|
||||||
|
Self {
|
||||||
|
car_state,
|
||||||
|
pl_state,
|
||||||
|
tcrc_state: Default::default(),
|
||||||
|
control_enable_gauge: gauge!("tcrc_control_enable"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn control_charge_rate(&mut self) -> Option<InterfaceRequest> {
|
||||||
|
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 {
|
||||||
|
// check if we're charging at home
|
||||||
|
if charge_state.charging_state == ChargingState::Charging
|
||||||
|
&& car_state.location_data.is_some_and(|v| v.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);
|
||||||
|
}
|
||||||
|
} else if charge_state.charging_state == ChargingState::Disconnected {
|
||||||
|
// only disable automatic control until the next time we're unplugged
|
||||||
|
self.tcrc_state
|
||||||
|
.write()
|
||||||
|
.expect("failed to write to tcrc state")
|
||||||
|
.control_enable = true;
|
||||||
|
self.control_enable_gauge.set(1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_request(&mut self, message: TcrcRequest) {
|
||||||
|
match message {
|
||||||
|
TcrcRequest::DisableAutomaticControl => {
|
||||||
|
self.tcrc_state
|
||||||
|
.write()
|
||||||
|
.expect("failed to write to tcrc state")
|
||||||
|
.control_enable = false;
|
||||||
|
self.control_enable_gauge.set(0.);
|
||||||
|
}
|
||||||
|
TcrcRequest::EnableAutomaticControl => {
|
||||||
|
self.tcrc_state
|
||||||
|
.write()
|
||||||
|
.expect("failed to write to tcrc state")
|
||||||
|
.control_enable = true;
|
||||||
|
self.control_enable_gauge.set(1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_control(pl_state: &PlState, charge_state: &ChargeState) -> Option<InterfaceRequest> {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
} 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use teslatte::vehicles::ChargingState;
|
||||||
|
|
||||||
use crate::{config::access_config, errors::TeslaStateParseError};
|
use crate::{config::access_config, errors::TeslaStateParseError};
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ pub struct ChargeState {
|
||||||
pub charge_current_request_max: i64,
|
pub charge_current_request_max: i64,
|
||||||
pub charger_connected: bool,
|
pub charger_connected: bool,
|
||||||
pub charge_enable_request: bool,
|
pub charge_enable_request: bool,
|
||||||
|
pub charging_state: ChargingState,
|
||||||
pub charge_limit_soc: i64,
|
pub charge_limit_soc: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +59,6 @@ impl ChargeState {
|
||||||
|
|
||||||
impl From<teslatte::vehicles::ChargeState> for ChargeState {
|
impl From<teslatte::vehicles::ChargeState> for ChargeState {
|
||||||
fn from(value: teslatte::vehicles::ChargeState) -> Self {
|
fn from(value: teslatte::vehicles::ChargeState) -> Self {
|
||||||
let charger_connected = value.conn_charge_cable != "<invalid>";
|
|
||||||
ChargeState {
|
ChargeState {
|
||||||
battery_level: value.battery_level,
|
battery_level: value.battery_level,
|
||||||
battery_range: value.battery_range,
|
battery_range: value.battery_range,
|
||||||
|
@ -65,8 +66,9 @@ impl From<teslatte::vehicles::ChargeState> for ChargeState {
|
||||||
charge_rate: value.charge_rate,
|
charge_rate: value.charge_rate,
|
||||||
charge_current_request: value.charge_current_request,
|
charge_current_request: value.charge_current_request,
|
||||||
charge_current_request_max: value.charge_current_request_max,
|
charge_current_request_max: value.charge_current_request_max,
|
||||||
charger_connected,
|
charger_connected: value.conn_charge_cable != "<invalid>",
|
||||||
charge_enable_request: value.charge_enable_request,
|
charge_enable_request: value.charge_enable_request,
|
||||||
|
charging_state: value.charging_state,
|
||||||
charge_limit_soc: value.charge_limit_soc,
|
charge_limit_soc: value.charge_limit_soc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 4888ce9d394e651ad051aeea4ac00d0ae2b75068
|
Subproject commit 636c5fc4821286555cdab311dd520cd0d08965ce
|
|
@ -31,6 +31,45 @@
|
||||||
.then((response) => console.log(response));
|
.then((response) => console.log(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disable_automatic_control() {
|
||||||
|
fetch(api_url + "/disable-control", { method: "POST" })
|
||||||
|
.then((response) => {
|
||||||
|
refresh_buttons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function enable_automatic_control() {
|
||||||
|
fetch(api_url + "/enable-control", { method: "POST" })
|
||||||
|
.then((response) => {
|
||||||
|
refresh_buttons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_control_buttons(data) {
|
||||||
|
console.log(data);
|
||||||
|
var button_container = document.getElementById("buttons");
|
||||||
|
while (button_container.childElementCount > 0) { button_container.removeChild(button_container.firstChild) }
|
||||||
|
if (data.control_enable) {
|
||||||
|
// control enabled, so show disable button
|
||||||
|
var button = document.createElement('button');
|
||||||
|
button.textContent = 'Disable automatic control';
|
||||||
|
button.addEventListener('click', disable_automatic_control);
|
||||||
|
button_container.appendChild(button);
|
||||||
|
} else {
|
||||||
|
// control disabled, so show enable button
|
||||||
|
var button = document.createElement('button');
|
||||||
|
button.textContent = 'Enable automatic control';
|
||||||
|
button.addEventListener('click', enable_automatic_control);
|
||||||
|
button_container.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh_buttons() {
|
||||||
|
fetch(api_url + "/control-state")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => update_control_buttons(json));
|
||||||
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
let favicon = document.getElementById("favicon");
|
let favicon = document.getElementById("favicon");
|
||||||
|
|
||||||
|
@ -39,7 +78,7 @@
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => update_state(json));
|
.then((json) => update_state(json));
|
||||||
|
|
||||||
|
refresh_buttons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_state(state) {
|
function update_state(state) {
|
||||||
|
@ -107,6 +146,8 @@
|
||||||
</div>
|
</div>
|
||||||
<button onclick="flash()">flash</button>
|
<button onclick="flash()">flash</button>
|
||||||
|
|
||||||
|
<p id="buttons"></p>
|
||||||
|
|
||||||
<div id="info"></div>
|
<div id="info"></div>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in a new issue