From f0621c8bc844752f3a1a404a4b24bfa37b3ed4f4 Mon Sep 17 00:00:00 2001 From: Maxime Augier Date: Wed, 21 Aug 2024 16:04:32 +0200 Subject: [PATCH] Continue implementing regulation --- config.example.json | 27 ++++++++++++++ src/config.rs | 11 ++++-- src/control.rs | 88 ++++++++++++++++++++++++++++++++++++++------- src/main.rs | 2 +- src/prom.rs | 13 +++---- 5 files changed, 119 insertions(+), 22 deletions(-) create mode 100644 config.example.json diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..5e5d62c --- /dev/null +++ b/config.example.json @@ -0,0 +1,27 @@ +{ + "regulator": { + "power_bias_watts": 3000, + "monophase_volts": 235 + }, + "mattermost": { + "base": "https://mattermost.example.com", + "token": "" + }, + "prometheus": { + "base": "http://prometheus.example.com:9090/" + }, + "easee_token_path": "./.easee_token", + "chargers": [ + + ], + "regulator": { + "site_id": 0, + "circuit_id": 0, + "power_bias_watts": 3000.0, + "monophase_volts": 240, + "polling_interval": 15, + "p": 0.2, + "i": 0.1, + "d": 0 + } +} diff --git a/src/config.rs b/src/config.rs index d3886e4..dadc3ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,12 +26,19 @@ pub struct Config { pub prometheus: Prometheus, pub mattermost: Mattermost, pub chargers: Vec, + pub regulator: Option, } #[derive(Debug,Deserialize)] pub struct Regulator { - power_bias_watts: f64, - monophase_volts: f64, + pub site_id: u32, + pub circuit_id: i64, + pub power_bias_watts: f64, + pub monophase_volts: f64, + pub polling_interval: u64, + pub p: f64, + pub i: f64, + pub d: f64, } pub fn load_config(path: &str) -> Result { diff --git a/src/control.rs b/src/control.rs index 9f5e9bb..515260e 100644 --- a/src/control.rs +++ b/src/control.rs @@ -1,9 +1,11 @@ -use std::collections::HashMap; + use std::collections::HashMap; use std::convert::Infallible; use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; -use anyhow::Result; -use easee::api::{self, ChargerOpMode, Context}; +use anyhow::{anyhow, Result}; +use easee::api::{self, ChargerOpMode, Circuit, Context, SetCurrent, Triphase}; use easee::observation::{self, Event, ObservationError}; use tracing::{error, info, warn}; @@ -19,6 +21,7 @@ struct Charger { current: Option<(f64, f64, f64)> } + impl Charger { pub fn from_api(inner: api::Charger, configs: &[config::Charger]) -> Self { let owners = configs.iter() @@ -36,11 +39,15 @@ pub fn start(mut ctx: Context, config: Config, mut chargers: Vec) let mattermost = mattermost::Context::new(config.mattermost.base, &config.mattermost.token)?; let mut stream = observation::Stream::from_context(&mut ctx)?; - // TODO let chargers: HashMap = chargers.into_iter() .filter_map(|c| { - stream.subscribe(&c.id).ok()?; + stream.subscribe(&c.id) + .map_err(|e| error!("Cannot subscribe {}: {e}", &c.id)) + .ok()?; let name = c.name.clone(); + c.enable_smart_charging(&mut ctx) + .map_err(|e| error!("Cannot enable smart charging on {}: {e}", &c.id)) + .ok()?; Some((name, Charger::from_api(c, &config.chargers))) }) .collect(); @@ -50,11 +57,33 @@ pub fn start(mut ctx: Context, config: Config, mut chargers: Vec) // TODO get channel on a per-charger basis let channel = mattermost.channel("9d9o1a5qf7fofk3wqfa493gkfe"); - - info!("Controller started"); mattermost.send_to_channel(&channel, "Easee Controller started")?; + if let Some(reg) = config.regulator { + + let circuit = ctx.sites_details()? + .into_iter().find(|s| s.site.id == reg.site_id) + .ok_or(anyhow!("Invalid site id {}", reg.site_id))? + .circuits.into_iter() + .find(|c| c.id == reg.circuit_id) + .ok_or(anyhow!("Invalid circuit id {}", reg.circuit_id))?; + + let controller = Controller { + bias: Triphase::from(reg.power_bias_watts), + ctx, + prom: PromClient::new(config.prometheus.base), + circuit, + delay: Duration::from_secs(reg.polling_interval), + p: reg.p, + i: reg.i, + d: reg.d, + }; + + let _ctrl = thread::spawn(move || controller.adjust_power()); + + } + loop { let evt = match stream.recv() { @@ -108,14 +137,47 @@ fn handle_event(evt: Event, charger: &mut Charger, ctx: &mattermost::Context, ch } } -pub fn adjust_power(prom: PromClient) -> Result<()> { +struct Controller { + /// Power bias in W + bias: Triphase, + ctx: Context, + prom: PromClient, + circuit: Circuit, + delay: Duration, + p: f64, + i: f64, + d: f64, +} - loop { - let export_power = prom.current_power()?; +impl Controller { - + pub fn adjust_power(mut self) -> Result<()> { + let voltage = 240f64; + let time_to_live = Some(self.delay.as_secs() as i32); + + let mut current = self.circuit.dynamic_current(&mut self.ctx)?; + let mut integrated = Triphase::default(); + let mut prev_available_current = Triphase::default(); + + loop { + let export_power = self.prom.current_power()?; + let available_power = export_power - self.bias; + let available_current = available_power * (1.0/voltage); + + integrated = available_current * 0.3 + integrated * (1.0 - 0.3); + let differentiated = available_current - prev_available_current; + prev_available_current = available_current; + + let delta = available_current * self.p + + differentiated * self.d + + integrated * self.i; + + current = current + delta; + self.circuit.set_dynamic_current(&mut self.ctx, SetCurrent { time_to_live, current })?; + + thread::sleep(self.delay); + + } } - - } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 0b28595..9023ef0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -183,7 +183,7 @@ fn main() -> Result<()> { Mode::Stream => stream(&args.charger_id)?, Mode::Power => { let pow = prom::PromClient::new(config.prometheus.base).current_power()?; - println!("P1:{}W P2:{}W P3:{}W", pow.0, pow.1, pow.2); + println!("P1:{}W P2:{}W P3:{}W", pow.phase1, pow.phase2, pow.phase3); }, Mode::Control => { let chargers = load_chargers(&mut ctx, &args.charger_id)?; diff --git a/src/prom.rs b/src/prom.rs index c24549e..b77ecfd 100644 --- a/src/prom.rs +++ b/src/prom.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use anyhow::{anyhow, bail, Result}; +use easee::api::Triphase; use ureq::serde::Deserialize; use tracing::warn; @@ -52,7 +53,7 @@ impl PromClient { PromClient { base, power_query_url } } - pub fn current_power(&self) -> Result<(f64, f64, f64)> { + pub fn current_power(&self) -> Result { let reply: PromReply = ureq::get(&self.power_query_url).call()?.into_json()?; let PromReply::Success { @@ -74,11 +75,11 @@ impl PromClient { } } - Ok(( - r.0.ok_or_else(|| anyhow!("Missing phase a"))?, - r.1.ok_or_else(|| anyhow!("Missing phase b"))?, - r.2.ok_or_else(|| anyhow!("Missing phase c"))?, - )) + Ok(Triphase { + phase1: r.0.ok_or_else(|| anyhow!("Missing phase a"))?, + phase2: r.1.ok_or_else(|| anyhow!("Missing phase b"))?, + phase3: r.2.ok_or_else(|| anyhow!("Missing phase c"))?, + }) } }