change max/min rate from webpage
All checks were successful
Build .deb on release / Build-Deb (push) Successful in 1m51s

This commit is contained in:
Alex Janka 2024-01-21 15:25:28 +11:00
parent 76bc9708d3
commit 1cec64dc62
7 changed files with 190 additions and 41 deletions

2
Cargo.lock generated
View file

@ -2567,7 +2567,7 @@ dependencies = [
[[package]]
name = "tesla-charge-controller"
version = "1.0.13"
version = "1.0.14"
dependencies = [
"async-channel",
"chrono",

View file

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

View file

@ -1,16 +1,43 @@
use std::{path::PathBuf, sync::OnceLock};
use std::{
path::PathBuf,
sync::{OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
};
use serde::{Deserialize, Serialize};
use crate::types::Coords;
use crate::{
errors::{ConfigError, PrintErrors},
types::Coords,
};
pub(super) static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
static CONFIG: OnceLock<Config> = OnceLock::new();
static CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
pub fn access_config<'a>() -> &'a Config {
CONFIG.get_or_init(|| {
serde_json::from_str(&std::fs::read_to_string(CONFIG_PATH.get().unwrap()).unwrap()).unwrap()
})
pub fn access_config<'a>() -> RwLockReadGuard<'a, Config> {
CONFIG
.get_or_init(|| RwLock::new(Config::load_and_save_defaults()))
.read()
.unwrap()
}
pub struct ConfigHandle<'a> {
pub handle: RwLockWriteGuard<'a, Config>,
}
impl<'a> Drop for ConfigHandle<'a> {
fn drop(&mut self) {
log::warn!("saving config...");
let _ = self.handle.save().some_or_print_with("saving config");
}
}
pub fn write_to_config<'a>() -> ConfigHandle<'a> {
ConfigHandle {
handle: CONFIG
.get_or_init(|| RwLock::new(Config::load_and_save_defaults()))
.write()
.unwrap(),
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -72,3 +99,25 @@ impl Default for Config {
}
}
}
impl Config {
fn load() -> Self {
serde_json::from_str(&std::fs::read_to_string(CONFIG_PATH.get().unwrap()).unwrap()).unwrap()
}
fn save(&self) -> Result<(), ConfigError> {
Ok(serde_json::ser::to_writer_pretty(
std::io::BufWriter::new(std::fs::File::create(CONFIG_PATH.get().unwrap())?),
self,
)?)
}
fn load_and_save_defaults() -> Self {
let config = Self::load();
let result = config.save();
if let Err(e) = result {
log::error!("Failed to save config: {e:#?}",);
}
config
}
}

View file

@ -103,3 +103,11 @@ impl From<libmodbus_rs::prelude::Error> for TristarError {
Self::Modbus(value)
}
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("json")]
Json(#[from] serde_json::Error),
#[error("io")]
Io(#[from] std::io::Error),
}

View file

@ -69,7 +69,6 @@ async fn main() {
.await
.some_or_print_with("loading tesla interface")
{
let config = access_config();
// build the channel that takes messages from the webserver thread to the api thread
let (api_requests, api_receiver) = async_channel::unbounded();
// and to the pli thread
@ -78,17 +77,21 @@ async fn main() {
let (tcrc_requests, tcrc_receiver) = async_channel::unbounded();
// try to spawn the pli loop
let pl_state = match Pli::new(
config.serial_port.clone(),
config.baud_rate,
config.pl_timeout_milliseconds,
) {
let pl_state = match {
let config = access_config();
Pli::new(
config.serial_port.clone(),
config.baud_rate,
config.pl_timeout_milliseconds,
)
} {
Ok(mut pli) => {
let pl_state = pli.state.clone();
tokio::task::spawn(async move {
let mut interval = tokio::time::interval(
std::time::Duration::from_secs(config.pl_watch_interval_seconds),
);
let mut interval =
tokio::time::interval(std::time::Duration::from_secs(
access_config().pl_watch_interval_seconds,
));
loop {
tokio::select! {
_ = interval.tick() => pli.refresh(),
@ -107,21 +110,22 @@ async fn main() {
}
};
let _additional_controllers: Vec<_> = config
let _additional_controllers: Vec<_> = access_config()
.additional_charge_controllers
.iter()
.clone()
.into_iter()
.filter_map(|v| match v {
ChargeControllerConfig::Pl {
serial_port,
baud_rate,
timeout_milliseconds,
watch_interval_seconds,
} => Pli::new(serial_port.clone(), *baud_rate, *timeout_milliseconds)
} => Pli::new(serial_port.clone(), baud_rate, timeout_milliseconds)
.some_or_print_with("Failed to connect to additional PLI")
.map(|mut pli| {
tokio::task::spawn(async move {
let mut interval = tokio::time::interval(
std::time::Duration::from_secs(*watch_interval_seconds),
std::time::Duration::from_secs(watch_interval_seconds),
);
loop {
interval.tick().await;
@ -133,12 +137,12 @@ async fn main() {
serial_port,
baud_rate,
watch_interval_seconds,
} => Tristar::new(serial_port.clone(), *baud_rate)
} => Tristar::new(serial_port.clone(), baud_rate)
.some_or_print_with("Failed to connect to additional Tristar")
.map(|mut tristar| {
tokio::task::spawn_local(async move {
let mut interval = tokio::time::interval(
std::time::Duration::from_secs(*watch_interval_seconds),
std::time::Duration::from_secs(watch_interval_seconds),
);
loop {
interval.tick().await;
@ -163,16 +167,18 @@ async fn main() {
// spawn the api loop
tokio::task::spawn(async move {
let mut normal_data_update_interval = tokio::time::interval(
std::time::Duration::from_secs(config.tesla_update_interval_seconds),
);
let mut normal_data_update_interval =
tokio::time::interval(std::time::Duration::from_secs(
access_config().tesla_update_interval_seconds,
));
let mut charge_data_update_interval =
tokio::time::interval(std::time::Duration::from_secs(
config.tesla_update_interval_while_charging_seconds,
access_config().tesla_update_interval_while_charging_seconds,
));
let mut charge_rate_update_interval =
tokio::time::interval(std::time::Duration::from_secs(
access_config().charge_rate_update_interval_seconds,
));
let mut charge_rate_update_interval = tokio::time::interval(
std::time::Duration::from_secs(config.charge_rate_update_interval_seconds),
);
let mut was_connected = false;
loop {

View file

@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
use crate::{
api_interface::InterfaceRequest,
charge_controllers::pl::{PlState, PliRequest},
config::{access_config, write_to_config},
errors::ServerError,
tesla_charge_rate::{TcrcRequest, TcrcState},
types::CarState,
@ -61,6 +62,8 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
flash,
disable_control,
enable_control,
set_max,
set_min,
metrics,
sync_time,
read_ram,
@ -88,16 +91,23 @@ async fn car_state(state: &State<ServerState>) -> Result<Json<CarState>, ServerE
struct ControlState {
control_enable: bool,
is_charging_at_home: bool,
max_rate: i64,
min_rate: i64,
}
#[get("/control-state")]
async fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> {
let control_enable = state.tcrc_state.read()?.control_enable;
let is_charging_at_home = state.car_state.read()?.is_charging_at_home();
let config = access_config();
let max_rate = config.max_rate;
let min_rate = config.min_rate;
Ok(Json(ControlState {
control_enable,
is_charging_at_home,
max_rate,
min_rate,
}))
}
@ -130,6 +140,20 @@ async fn enable_control(state: &State<ServerState>) {
}
}
#[post("/set-max/<limit>")]
async fn set_max(limit: i64) -> String {
let limit = limit.clamp(access_config().min_rate, 15);
write_to_config().handle.max_rate = limit;
format!("set upper limit to {limit}")
}
#[post("/set-min/<limit>")]
async fn set_min(limit: i64) -> String {
let limit = limit.clamp(3, access_config().max_rate);
write_to_config().handle.min_rate = limit;
format!("set lower limit to {limit}")
}
#[get("/metrics")]
fn metrics() -> Result<String, ServerError> {
Ok(

View file

@ -105,11 +105,53 @@
}
var is_automatic_control;
var current_min_rate;
var current_max_rate;
const delay = () => {
return new Promise(resolve => setTimeout(resolve, 1000));
const delay = (time) => {
return new Promise(resolve => setTimeout(resolve, time));
};
function set_minimum() {
var set_button = document.getElementById("set-minimum");
var number_input = document.getElementById("min-rate");
if (!isNaN(number_input.value)) {
set_button.disabled = true;
number_input.disabled = true;
fetch(api_url + "/set-min/" + number_input.value, { method: "POST" })
.then(async (response) => {
let delayres = await delay(100);
refresh_buttons();
});
}
}
function change_min() {
var set_button = document.getElementById("set-minimum");
var number_input = document.getElementById("min-rate");
set_button.disabled = (number_input.value == current_min_rate);
}
function set_maximum() {
var set_button = document.getElementById("set-maximum");
var number_input = document.getElementById("max-rate");
if (!isNaN(number_input.value)) {
set_button.disabled = true;
number_input.disabled = true;
fetch(api_url + "/set-max/" + number_input.value, { method: "POST" })
.then(async (response) => {
let delayres = await delay(100);
refresh_buttons();
});
}
}
function change_max() {
var set_button = document.getElementById("set-maximum");
var number_input = document.getElementById("max-rate");
set_button.disabled = (number_input.value == current_max_rate);
}
function disable_automatic_control() {
if (is_automatic_control) {
document.getElementById('control-disabled').checked = true;
@ -117,7 +159,7 @@
fetch(api_url + "/disable-control", { method: "POST" })
.then(async (response) => {
let delayres = await delay();
let delayres = await delay(1000);
refresh_buttons();
});
}
@ -129,13 +171,30 @@
document.body.classList.add("loading");
fetch(api_url + "/enable-control", { method: "POST" })
.then(async (response) => {
let delayres = await delay();
let delayres = await delay(1000);
refresh_buttons();
});
}
}
function update_control_buttons(data) {
current_max_rate = data.max_rate;
current_min_rate = data.min_rate;
var number_input_min = document.getElementById("min-rate");
if (number_input_min.disabled || number_input_min.value == "") {
number_input_min.value = data.min_rate;
number_input_min.disabled = false;
}
document.getElementById("set-minimum").disabled = (number_input_min.value == current_min_rate);
var number_input_max = document.getElementById("max-rate");
if (number_input_max.disabled || number_input_max.value == "") {
number_input_max.value = data.max_rate;
number_input_max.disabled = false;
}
document.getElementById("set-maximum").disabled = (number_input_max.value == current_max_rate);
document.body.classList.remove("loading");
is_automatic_control = data.control_enable;
if (data.control_enable) {
@ -157,12 +216,6 @@
}
document.getElementsByName('control').disable();
}
// for (let i = 0; i < elements.length; i++) {
// console.log(elements[i]);
// elements[i].disabled = data.is_charging_at_home;
// }
}
function refresh_buttons() {
@ -220,6 +273,15 @@
<input id="control-disabled" type="radio" name="control" onclick="disable_automatic_control()" disabled>
<label for="control-disabled">disabled</label>
</div>
<div id="rate-control">
<h3>Charge rate:</h3>
<input type="number" id="max-rate" max="15" min="3" onchange="change_max()" autocomplete="off" />
<button id="set-maximum" onclick="set_maximum()" disabled>Set maximum</button>
<br><br>
<input type="number" id="min-rate" max="15" min="3" onchange="change_min()" autocomplete="off" />
<button id="set-minimum" onclick="set_minimum()" disabled>Set minimum</button>
</div>
<br>
<button onclick="flash()">flash</button>
<div id="info"></div>