change max/min rate from webpage
All checks were successful
Build .deb on release / Build-Deb (push) Successful in 1m51s
All checks were successful
Build .deb on release / Build-Deb (push) Successful in 1m51s
This commit is contained in:
parent
76bc9708d3
commit
1cec64dc62
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2567,7 +2567,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tesla-charge-controller"
|
name = "tesla-charge-controller"
|
||||||
version = "1.0.13"
|
version = "1.0.14"
|
||||||
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.13"
|
version = "1.0.14"
|
||||||
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,16 +1,43 @@
|
||||||
use std::{path::PathBuf, sync::OnceLock};
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::types::Coords;
|
use crate::{
|
||||||
|
errors::{ConfigError, PrintErrors},
|
||||||
|
types::Coords,
|
||||||
|
};
|
||||||
|
|
||||||
pub(super) static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
|
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 {
|
pub fn access_config<'a>() -> RwLockReadGuard<'a, Config> {
|
||||||
CONFIG.get_or_init(|| {
|
CONFIG
|
||||||
serde_json::from_str(&std::fs::read_to_string(CONFIG_PATH.get().unwrap()).unwrap()).unwrap()
|
.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)]
|
#[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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -103,3 +103,11 @@ impl From<libmodbus_rs::prelude::Error> for TristarError {
|
||||||
Self::Modbus(value)
|
Self::Modbus(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("json")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error("io")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
50
src/main.rs
50
src/main.rs
|
@ -69,7 +69,6 @@ async fn main() {
|
||||||
.await
|
.await
|
||||||
.some_or_print_with("loading tesla interface")
|
.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
|
// build the channel that takes messages from the webserver thread to the api thread
|
||||||
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
|
||||||
|
@ -78,17 +77,21 @@ async fn main() {
|
||||||
let (tcrc_requests, tcrc_receiver) = async_channel::unbounded();
|
let (tcrc_requests, tcrc_receiver) = async_channel::unbounded();
|
||||||
|
|
||||||
// try to spawn the pli loop
|
// try to spawn the pli loop
|
||||||
let pl_state = match Pli::new(
|
let pl_state = match {
|
||||||
config.serial_port.clone(),
|
let config = access_config();
|
||||||
config.baud_rate,
|
Pli::new(
|
||||||
config.pl_timeout_milliseconds,
|
config.serial_port.clone(),
|
||||||
) {
|
config.baud_rate,
|
||||||
|
config.pl_timeout_milliseconds,
|
||||||
|
)
|
||||||
|
} {
|
||||||
Ok(mut pli) => {
|
Ok(mut pli) => {
|
||||||
let pl_state = pli.state.clone();
|
let pl_state = pli.state.clone();
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(
|
let mut interval =
|
||||||
std::time::Duration::from_secs(config.pl_watch_interval_seconds),
|
tokio::time::interval(std::time::Duration::from_secs(
|
||||||
);
|
access_config().pl_watch_interval_seconds,
|
||||||
|
));
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = interval.tick() => pli.refresh(),
|
_ = 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
|
.additional_charge_controllers
|
||||||
.iter()
|
.clone()
|
||||||
|
.into_iter()
|
||||||
.filter_map(|v| match v {
|
.filter_map(|v| match v {
|
||||||
ChargeControllerConfig::Pl {
|
ChargeControllerConfig::Pl {
|
||||||
serial_port,
|
serial_port,
|
||||||
baud_rate,
|
baud_rate,
|
||||||
timeout_milliseconds,
|
timeout_milliseconds,
|
||||||
watch_interval_seconds,
|
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")
|
.some_or_print_with("Failed to connect to additional PLI")
|
||||||
.map(|mut pli| {
|
.map(|mut pli| {
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(
|
let mut interval = tokio::time::interval(
|
||||||
std::time::Duration::from_secs(*watch_interval_seconds),
|
std::time::Duration::from_secs(watch_interval_seconds),
|
||||||
);
|
);
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
@ -133,12 +137,12 @@ async fn main() {
|
||||||
serial_port,
|
serial_port,
|
||||||
baud_rate,
|
baud_rate,
|
||||||
watch_interval_seconds,
|
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")
|
.some_or_print_with("Failed to connect to additional Tristar")
|
||||||
.map(|mut tristar| {
|
.map(|mut tristar| {
|
||||||
tokio::task::spawn_local(async move {
|
tokio::task::spawn_local(async move {
|
||||||
let mut interval = tokio::time::interval(
|
let mut interval = tokio::time::interval(
|
||||||
std::time::Duration::from_secs(*watch_interval_seconds),
|
std::time::Duration::from_secs(watch_interval_seconds),
|
||||||
);
|
);
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
@ -163,16 +167,18 @@ async fn main() {
|
||||||
|
|
||||||
// spawn the api loop
|
// spawn the api loop
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let mut normal_data_update_interval = tokio::time::interval(
|
let mut normal_data_update_interval =
|
||||||
std::time::Duration::from_secs(config.tesla_update_interval_seconds),
|
tokio::time::interval(std::time::Duration::from_secs(
|
||||||
);
|
access_config().tesla_update_interval_seconds,
|
||||||
|
));
|
||||||
let mut charge_data_update_interval =
|
let mut charge_data_update_interval =
|
||||||
tokio::time::interval(std::time::Duration::from_secs(
|
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;
|
let mut was_connected = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
|
@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
api_interface::InterfaceRequest,
|
api_interface::InterfaceRequest,
|
||||||
charge_controllers::pl::{PlState, PliRequest},
|
charge_controllers::pl::{PlState, PliRequest},
|
||||||
|
config::{access_config, write_to_config},
|
||||||
errors::ServerError,
|
errors::ServerError,
|
||||||
tesla_charge_rate::{TcrcRequest, TcrcState},
|
tesla_charge_rate::{TcrcRequest, TcrcState},
|
||||||
types::CarState,
|
types::CarState,
|
||||||
|
@ -61,6 +62,8 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
|
||||||
flash,
|
flash,
|
||||||
disable_control,
|
disable_control,
|
||||||
enable_control,
|
enable_control,
|
||||||
|
set_max,
|
||||||
|
set_min,
|
||||||
metrics,
|
metrics,
|
||||||
sync_time,
|
sync_time,
|
||||||
read_ram,
|
read_ram,
|
||||||
|
@ -88,16 +91,23 @@ async fn car_state(state: &State<ServerState>) -> Result<Json<CarState>, ServerE
|
||||||
struct ControlState {
|
struct ControlState {
|
||||||
control_enable: bool,
|
control_enable: bool,
|
||||||
is_charging_at_home: bool,
|
is_charging_at_home: bool,
|
||||||
|
max_rate: i64,
|
||||||
|
min_rate: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/control-state")]
|
#[get("/control-state")]
|
||||||
async fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> {
|
async fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> {
|
||||||
let control_enable = state.tcrc_state.read()?.control_enable;
|
let control_enable = state.tcrc_state.read()?.control_enable;
|
||||||
let is_charging_at_home = state.car_state.read()?.is_charging_at_home();
|
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 {
|
Ok(Json(ControlState {
|
||||||
control_enable,
|
control_enable,
|
||||||
is_charging_at_home,
|
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")]
|
#[get("/metrics")]
|
||||||
fn metrics() -> Result<String, ServerError> {
|
fn metrics() -> Result<String, ServerError> {
|
||||||
Ok(
|
Ok(
|
||||||
|
|
|
@ -105,11 +105,53 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
var is_automatic_control;
|
var is_automatic_control;
|
||||||
|
var current_min_rate;
|
||||||
|
var current_max_rate;
|
||||||
|
|
||||||
const delay = () => {
|
const delay = (time) => {
|
||||||
return new Promise(resolve => setTimeout(resolve, 1000));
|
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() {
|
function disable_automatic_control() {
|
||||||
if (is_automatic_control) {
|
if (is_automatic_control) {
|
||||||
document.getElementById('control-disabled').checked = true;
|
document.getElementById('control-disabled').checked = true;
|
||||||
|
@ -117,7 +159,7 @@
|
||||||
|
|
||||||
fetch(api_url + "/disable-control", { method: "POST" })
|
fetch(api_url + "/disable-control", { method: "POST" })
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
let delayres = await delay();
|
let delayres = await delay(1000);
|
||||||
refresh_buttons();
|
refresh_buttons();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -129,13 +171,30 @@
|
||||||
document.body.classList.add("loading");
|
document.body.classList.add("loading");
|
||||||
fetch(api_url + "/enable-control", { method: "POST" })
|
fetch(api_url + "/enable-control", { method: "POST" })
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
let delayres = await delay();
|
let delayres = await delay(1000);
|
||||||
refresh_buttons();
|
refresh_buttons();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_control_buttons(data) {
|
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");
|
document.body.classList.remove("loading");
|
||||||
is_automatic_control = data.control_enable;
|
is_automatic_control = data.control_enable;
|
||||||
if (data.control_enable) {
|
if (data.control_enable) {
|
||||||
|
@ -157,12 +216,6 @@
|
||||||
}
|
}
|
||||||
document.getElementsByName('control').disable();
|
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() {
|
function refresh_buttons() {
|
||||||
|
@ -220,6 +273,15 @@
|
||||||
<input id="control-disabled" type="radio" name="control" onclick="disable_automatic_control()" disabled>
|
<input id="control-disabled" type="radio" name="control" onclick="disable_automatic_control()" disabled>
|
||||||
<label for="control-disabled">disabled</label>
|
<label for="control-disabled">disabled</label>
|
||||||
</div>
|
</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>
|
<br>
|
||||||
<button onclick="flash()">flash</button>
|
<button onclick="flash()">flash</button>
|
||||||
<div id="info"></div>
|
<div id="info"></div>
|
||||||
|
|
Loading…
Reference in a new issue