tesla charge rate controller: builds

This commit is contained in:
Alex Janka 2024-12-29 16:20:59 +11:00
parent 786701794e
commit 59994c75c1
14 changed files with 259 additions and 75 deletions

View file

@ -33,6 +33,7 @@ lazy_static = "1.5.0"
rand = "0.8.5"
reqwest = { version = "0.12.9", default-features = false, features = [
"rustls-tls",
"json",
] }
eyre = "0.6.12"

View file

@ -83,6 +83,7 @@ impl Vehicle {
.timeout(std::time::Duration::from_secs(5))
.send()
.await?;
let response: ApiResponseOuter = data.json().await?;
let response = response.response;

View file

@ -52,24 +52,26 @@ impl Car {
}
impl CarState {
pub fn charge_state(&self) -> Option<&ChargeState> {
if self.is_outdated() {
pub async fn charge_state(&self) -> Option<&ChargeState> {
if self.is_outdated().await {
None
} else {
self.charge_state.as_ref()
}
}
pub fn is_charging(&self) -> bool {
self.charge_state().is_some_and(ChargeState::is_charging)
pub async fn is_charging(&self) -> bool {
self.charge_state()
.await
.is_some_and(ChargeState::is_charging)
}
pub fn data_age(&self) -> std::time::Duration {
std::time::Instant::now().duration_since(self.data_received)
}
fn is_outdated(&self) -> bool {
self.data_age() > crate::CONFIG.read().car_state_interval
async fn is_outdated(&self) -> bool {
self.data_age() > crate::config::access_config().await.car_state_interval
}
}

View file

@ -1,5 +1,114 @@
use serde::{Deserialize, Serialize};
static CONFIG_PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
pub(super) static CONFIG: std::sync::OnceLock<tokio::sync::RwLock<Config>> =
std::sync::OnceLock::new();
pub async fn access_config<'a>() -> tokio::sync::RwLockReadGuard<'a, Config> {
CONFIG.get().unwrap().read().await
}
pub(super) struct ConfigWatcher {
_debouncer: notify_debouncer_mini::Debouncer<notify_debouncer_mini::notify::RecommendedWatcher>,
_handle: tokio::task::JoinHandle<()>,
}
impl ConfigWatcher {
pub fn new(path: impl AsRef<std::path::Path>) -> Option<Self> {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let mut debouncer =
notify_debouncer_mini::new_debouncer(std::time::Duration::from_secs(1), move |v| {
tx.send(v).unwrap();
})
.ok()?;
debouncer
.watcher()
.watch(
path.as_ref(),
notify_debouncer_mini::notify::RecursiveMode::NonRecursive,
)
.ok()?;
let config_path = std::path::PathBuf::from(path.as_ref());
let handle = tokio::task::spawn(async move {
loop {
match rx.recv().await {
Some(Ok(_event)) => {
let mut config = Config::load(&config_path);
config.validate();
if let Err(e) = overwrite_config(config).await {
log::error!("{e:?}");
}
}
Some(Err(e)) => log::error!("Error {e:?} from watcher"),
None => {}
}
}
});
Some(Self {
_debouncer: debouncer,
_handle: handle,
})
}
}
async fn overwrite_config(config: Config) -> eyre::Result<()> {
*CONFIG
.get()
.ok_or(eyre::eyre!("could not get config"))?
.write()
.await = config;
Ok(())
}
pub fn init_config(path: impl AsRef<std::path::Path>) -> Option<ConfigWatcher> {
log::trace!("loading config...");
let config = Config::load_and_save_defaults(&path);
log::trace!("watching config for changes...");
let config_watcher = ConfigWatcher::new(&path);
let _ = CONFIG_PATH.get_or_init(|| path.as_ref().to_path_buf());
CONFIG.set(tokio::sync::RwLock::new(config)).unwrap();
config_watcher
}
pub struct ConfigHandle<'a> {
handle: tokio::sync::RwLockWriteGuard<'a, Config>,
}
impl<'a> core::ops::Deref for ConfigHandle<'a> {
type Target = tokio::sync::RwLockWriteGuard<'a, Config>;
fn deref(&self) -> &Self::Target {
&self.handle
}
}
impl std::ops::DerefMut for ConfigHandle<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.handle
}
}
impl Drop for ConfigHandle<'_> {
fn drop(&mut self) {
if let Err(e) = self.save() {
log::error!("error saving config on drop of handle: {e:?}");
}
}
}
pub async fn write_to_config<'a>() -> ConfigHandle<'a> {
ConfigHandle {
handle: CONFIG.get().unwrap().write().await,
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default)]
pub struct Config {
@ -34,20 +143,6 @@ impl Default for Config {
}
}
impl common::config::ConfigTrait for Config {
fn load_and_save_defaults(path: impl AsRef<std::path::Path>) -> Self {
todo!()
}
fn save(&self) -> eyre::Result<()> {
todo!()
}
fn is_valid(&self) -> bool {
todo!()
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
#[serde(default)]
pub struct PidControls {
@ -67,3 +162,41 @@ impl Default for PidControls {
}
}
}
impl Config {
fn load(path: impl AsRef<std::path::Path>) -> Self {
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
}
fn save_to(&self, path: impl AsRef<std::path::Path>) -> eyre::Result<()> {
Ok(serde_json::ser::to_writer_pretty(
std::io::BufWriter::new(std::fs::File::create(path)?),
self,
)?)
}
fn save(&self) -> eyre::Result<()> {
self.save_to(CONFIG_PATH.get().unwrap())
}
fn load_and_save_defaults(path: impl AsRef<std::path::Path>) -> Self {
let mut config = Self::load(&path);
config.validate();
let result = config.save_to(path);
if let Err(e) = result {
log::error!("Failed to save config: {e:#?}",);
}
config
}
fn validate(&mut self) {
self.shutoff_voltage = self.shutoff_voltage.clamp(40.0, 60.0);
self.max_rate = self.max_rate.clamp(0, 30);
self.min_rate = self.min_rate.clamp(0, self.max_rate);
// self.duty_cycle_too_high = self.duty_cycle_too_high.clamp(0.0, 1.0);
// self.duty_cycle_too_low = self.duty_cycle_too_low.clamp(0.0, 1.0);
self.pid_controls.proportional_gain = self.pid_controls.proportional_gain.clamp(0.0, 50.0);
self.pid_controls.derivative_gain = self.pid_controls.derivative_gain.clamp(0.0, 50.0);
self.pid_controls.load_divisor = self.pid_controls.load_divisor.clamp(1.0, 50.0);
}
}

View file

@ -25,14 +25,32 @@ impl VehicleController {
}
}
pub async fn run(&mut self) -> ! {
let (mut car_state_tick, mut pid_loop_tick) = {
let config = crate::config::access_config().await;
let car_state_tick = tokio::time::interval(config.car_state_interval);
let pid_loop_tick = tokio::time::interval(std::time::Duration::from_secs(
config.pid_controls.loop_time_seconds,
));
(car_state_tick, pid_loop_tick)
};
loop {
tokio::select! {
_ = car_state_tick.tick() => { self.car.update().await; },
_ = pid_loop_tick.tick() => { self.run_cycle().await; }
Some(req) = self.requests.recv() => { self.process_requests(req).await; }
}
}
}
pub async fn run_cycle(&mut self) {
let age = self.car.state().read().await.data_age();
if age >= crate::CONFIG.read().car_state_interval {
if age >= crate::config::access_config().await.car_state_interval {
self.car.update().await;
}
match self.control_state {
ChargeRateControllerState::Inactive => {
if let Some(state) = self.car.state().read().await.charge_state() {
if let Some(state) = self.car.state().read().await.charge_state().await {
if state.is_charging() {
self.control_state = ChargeRateControllerState::Charging {
rate_amps: state.charge_amps,
@ -44,15 +62,11 @@ impl VehicleController {
}
}
pub async fn process_requests(&mut self) -> eyre::Result<()> {
while let Some(req) = self.requests.recv().await {
match req {
InterfaceRequest::FlashLights => {
self.car.vehicle().flash_lights().await?;
}
}
pub async fn process_requests(&mut self, req: InterfaceRequest) {
if let Err(e) = match req {
InterfaceRequest::FlashLights => self.car.vehicle().flash_lights().await,
} {
log::error!("failed to execute request: {e:?}");
}
Ok(())
}
}

View file

@ -7,16 +7,13 @@ mod config;
mod control;
mod server;
static CONFIG: common::config::ConfigSingleton<config::Config> =
common::config::ConfigSingleton::new();
#[derive(clap::Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[command(subcommand)]
command: Commands,
#[clap(long, default_value = "/etc/tesla-charge-controller")]
config_dir: PathBuf,
#[clap(long, default_value = "/etc/tesla-charge-controller/config.json")]
config: PathBuf,
}
#[derive(clap::Subcommand, Debug, Clone)]
@ -29,26 +26,55 @@ enum Commands {
#[tokio::main]
async fn main() {
common::init_log();
env_logger::builder()
.format_module_path(false)
.format_timestamp(
if std::env::var("LOG_TIMESTAMP").is_ok_and(|v| v == "false") {
None
} else {
Some(env_logger::TimestampPrecision::Seconds)
},
)
.init();
if let Err(e) = run().await {
log::error!("{e:?}");
std::process::exit(1);
let args = Args::parse();
match args.command {
Commands::Watch => {
if let Err(e) = run(args).await {
log::error!("{e:?}");
std::process::exit(1);
}
}
Commands::GenerateConfig => {
let config = config::Config::default();
match serde_json::to_string_pretty(&config) {
Ok(config_json) => {
println!("{config_json}");
}
Err(e) => {
eprintln!("error serializing config: {e:?}");
}
}
}
}
}
async fn run() -> eyre::Result<()> {
let args = Args::parse();
CONFIG.load(args.config_dir.join("tesla.json"))?;
async fn run(args: Args) -> eyre::Result<()> {
let _config_watcher = config::init_config(args.config);
let car = api::Car::new(&CONFIG.read().car_vin);
let car = {
let config = config::access_config().await;
api::Car::new(&config.car_vin)
};
let car = std::sync::Arc::new(car);
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut vehicle_controller = control::VehicleController::new(car.clone(), rx);
let server = server::launch_server(server::ServerState::new(car, tx));
server.await;
vehicle_controller.run_cycle().await;
tokio::select! {
() = server => {}
() = vehicle_controller.run() => {}
}
Ok(())
}

View file

@ -11,7 +11,11 @@ use rocket::{
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::UnboundedSender;
use crate::{api::Car, control::InterfaceRequest};
use crate::{
api::Car,
config::{access_config, write_to_config},
control::InterfaceRequest,
};
use self::static_handler::UiStatic;
@ -83,6 +87,7 @@ async fn car_state(
.read()
.await
.charge_state()
.await
.ok_or(ServerError::NoData)?
.clone(),
))
@ -98,8 +103,8 @@ struct ControlState {
#[get("/control-state")]
async fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> {
let is_charging = state.car.state().read().await.is_charging();
let config = crate::CONFIG.read();
let is_charging = state.car.state().read().await.is_charging().await;
let config = access_config().await;
let control_enable = config.control_enable;
let max_rate = config.max_rate;
let min_rate = config.min_rate;
@ -119,29 +124,29 @@ fn flash(state: &State<ServerState>, remote_addr: std::net::IpAddr) {
}
#[post("/disable-control")]
fn disable_control(remote_addr: std::net::IpAddr) {
async fn disable_control(remote_addr: std::net::IpAddr) {
log::warn!("disabling control: {remote_addr:?}");
crate::CONFIG.write().control_enable = false;
write_to_config().await.control_enable = false;
}
#[post("/enable-control")]
fn enable_control(remote_addr: std::net::IpAddr) {
async fn enable_control(remote_addr: std::net::IpAddr) {
log::warn!("enabling control: {remote_addr:?}");
crate::CONFIG.write().control_enable = true;
write_to_config().await.control_enable = true;
}
#[post("/shutoff/voltage/<voltage>")]
fn set_shutoff(voltage: f64, remote_addr: std::net::IpAddr) {
async fn set_shutoff(voltage: f64, remote_addr: std::net::IpAddr) {
log::warn!("setting shutoff voltage: {remote_addr:?}");
let voltage = voltage.clamp(40., 60.);
crate::CONFIG.write().shutoff_voltage = voltage;
write_to_config().await.shutoff_voltage = voltage;
}
#[post("/shutoff/time/<time>")]
fn set_shutoff_time(time: u64, remote_addr: std::net::IpAddr) {
async fn set_shutoff_time(time: u64, remote_addr: std::net::IpAddr) {
log::warn!("setting shutoff time: {remote_addr:?}");
let time = time.clamp(5, 120);
crate::CONFIG.write().shutoff_voltage_time_seconds = time;
write_to_config().await.shutoff_voltage_time_seconds = time;
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
@ -151,8 +156,8 @@ struct ShutoffStatus {
}
#[get("/shutoff/status")]
fn shutoff_status() -> Json<ShutoffStatus> {
let config = crate::CONFIG.read();
async fn shutoff_status() -> Json<ShutoffStatus> {
let config = access_config().await;
Json(ShutoffStatus {
voltage: config.shutoff_voltage,
time: config.shutoff_voltage_time_seconds,
@ -160,46 +165,48 @@ fn shutoff_status() -> Json<ShutoffStatus> {
}
#[post("/set-max/<limit>")]
fn set_max(limit: i64, remote_addr: std::net::IpAddr) {
async fn set_max(limit: i64, remote_addr: std::net::IpAddr) {
log::warn!("setting max: {remote_addr:?}");
let limit = limit.clamp(crate::CONFIG.read().min_rate, 32);
crate::CONFIG.write().max_rate = limit;
let mut config = write_to_config().await;
let limit = limit.clamp(config.min_rate, 32);
config.max_rate = limit;
}
#[post("/set-min/<limit>")]
fn set_min(limit: i64, remote_addr: std::net::IpAddr) {
async fn set_min(limit: i64, remote_addr: std::net::IpAddr) {
log::warn!("setting min: {remote_addr:?}");
let limit = limit.clamp(0, crate::CONFIG.read().max_rate);
crate::CONFIG.write().min_rate = limit;
let mut config = write_to_config().await;
let limit = limit.clamp(0, config.max_rate);
config.min_rate = limit;
}
#[post("/pid-settings/proportional/<gain>")]
fn set_proportional_gain(gain: f64, remote_addr: std::net::IpAddr) {
async fn set_proportional_gain(gain: f64, remote_addr: std::net::IpAddr) {
log::warn!("setting proportional gain: {remote_addr:?}");
crate::CONFIG.write().pid_controls.proportional_gain = gain;
write_to_config().await.pid_controls.proportional_gain = gain;
}
#[post("/pid-settings/derivative/<gain>")]
fn set_derivative_gain(gain: f64, remote_addr: std::net::IpAddr) {
async fn set_derivative_gain(gain: f64, remote_addr: std::net::IpAddr) {
log::warn!("setting derivative gain: {remote_addr:?}");
crate::CONFIG.write().pid_controls.derivative_gain = gain;
write_to_config().await.pid_controls.derivative_gain = gain;
}
#[post("/pid-settings/loop_time_seconds/<time>")]
fn set_pid_loop_length(time: u64, remote_addr: std::net::IpAddr) {
async fn set_pid_loop_length(time: u64, remote_addr: std::net::IpAddr) {
log::warn!("setting pid loop interval: {remote_addr:?}");
crate::CONFIG.write().pid_controls.loop_time_seconds = time;
write_to_config().await.pid_controls.loop_time_seconds = time;
}
#[post("/pid-settings/load_divisor/<divisor>")]
fn set_load_divisor(divisor: f64, remote_addr: std::net::IpAddr) {
async fn set_load_divisor(divisor: f64, remote_addr: std::net::IpAddr) {
log::warn!("setting load divisor interval: {remote_addr:?}");
crate::CONFIG.write().pid_controls.load_divisor = divisor;
write_to_config().await.pid_controls.load_divisor = divisor;
}
#[get("/pid-settings/status")]
fn pid_settings() -> Json<crate::config::PidControls> {
Json(crate::CONFIG.read().pid_controls)
async fn pid_settings() -> Json<crate::config::PidControls> {
Json(access_config().await.pid_controls)
}
#[get("/metrics")]