tesla charge rate controller: builds
This commit is contained in:
parent
786701794e
commit
59994c75c1
14 changed files with 259 additions and 75 deletions
|
@ -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"
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
Loading…
Add table
Reference in a new issue