Continue implementing regulation
This commit is contained in:
parent
769544b608
commit
f0621c8bc8
27
config.example.json
Normal file
27
config.example.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -26,12 +26,19 @@ pub struct Config {
|
|||||||
pub prometheus: Prometheus,
|
pub prometheus: Prometheus,
|
||||||
pub mattermost: Mattermost,
|
pub mattermost: Mattermost,
|
||||||
pub chargers: Vec<Charger>,
|
pub chargers: Vec<Charger>,
|
||||||
|
pub regulator: Option<Regulator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug,Deserialize)]
|
#[derive(Debug,Deserialize)]
|
||||||
pub struct Regulator {
|
pub struct Regulator {
|
||||||
power_bias_watts: f64,
|
pub site_id: u32,
|
||||||
monophase_volts: f64,
|
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<Config> {
|
pub fn load_config(path: &str) -> Result<Config> {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use easee::api::{self, ChargerOpMode, Context};
|
use easee::api::{self, ChargerOpMode, Circuit, Context, SetCurrent, Triphase};
|
||||||
use easee::observation::{self, Event, ObservationError};
|
use easee::observation::{self, Event, ObservationError};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
@ -19,6 +21,7 @@ struct Charger {
|
|||||||
current: Option<(f64, f64, f64)>
|
current: Option<(f64, f64, f64)>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Charger {
|
impl Charger {
|
||||||
pub fn from_api(inner: api::Charger, configs: &[config::Charger]) -> Self {
|
pub fn from_api(inner: api::Charger, configs: &[config::Charger]) -> Self {
|
||||||
let owners = configs.iter()
|
let owners = configs.iter()
|
||||||
@ -36,11 +39,15 @@ pub fn start(mut ctx: Context, config: Config, mut chargers: Vec<api::Charger>)
|
|||||||
let mattermost = mattermost::Context::new(config.mattermost.base, &config.mattermost.token)?;
|
let mattermost = mattermost::Context::new(config.mattermost.base, &config.mattermost.token)?;
|
||||||
let mut stream = observation::Stream::from_context(&mut ctx)?;
|
let mut stream = observation::Stream::from_context(&mut ctx)?;
|
||||||
|
|
||||||
// TODO
|
|
||||||
let chargers: HashMap<String, Charger> = chargers.into_iter()
|
let chargers: HashMap<String, Charger> = chargers.into_iter()
|
||||||
.filter_map(|c| {
|
.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();
|
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)))
|
Some((name, Charger::from_api(c, &config.chargers)))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -50,11 +57,33 @@ pub fn start(mut ctx: Context, config: Config, mut chargers: Vec<api::Charger>)
|
|||||||
// TODO get channel on a per-charger basis
|
// TODO get channel on a per-charger basis
|
||||||
let channel = mattermost.channel("9d9o1a5qf7fofk3wqfa493gkfe");
|
let channel = mattermost.channel("9d9o1a5qf7fofk3wqfa493gkfe");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
info!("Controller started");
|
info!("Controller started");
|
||||||
mattermost.send_to_channel(&channel, "Easee 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 {
|
loop {
|
||||||
|
|
||||||
let evt = match stream.recv() {
|
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 {
|
impl Controller {
|
||||||
let export_power = prom.current_power()?;
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -183,7 +183,7 @@ fn main() -> Result<()> {
|
|||||||
Mode::Stream => stream(&args.charger_id)?,
|
Mode::Stream => stream(&args.charger_id)?,
|
||||||
Mode::Power => {
|
Mode::Power => {
|
||||||
let pow = prom::PromClient::new(config.prometheus.base).current_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 => {
|
Mode::Control => {
|
||||||
let chargers = load_chargers(&mut ctx, &args.charger_id)?;
|
let chargers = load_chargers(&mut ctx, &args.charger_id)?;
|
||||||
|
13
src/prom.rs
13
src/prom.rs
@ -1,6 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use easee::api::Triphase;
|
||||||
use ureq::serde::Deserialize;
|
use ureq::serde::Deserialize;
|
||||||
|
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
@ -52,7 +53,7 @@ impl PromClient {
|
|||||||
PromClient { base, power_query_url }
|
PromClient { base, power_query_url }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_power(&self) -> Result<(f64, f64, f64)> {
|
pub fn current_power(&self) -> Result<Triphase> {
|
||||||
let reply: PromReply = ureq::get(&self.power_query_url).call()?.into_json()?;
|
let reply: PromReply = ureq::get(&self.power_query_url).call()?.into_json()?;
|
||||||
|
|
||||||
let PromReply::Success {
|
let PromReply::Success {
|
||||||
@ -74,11 +75,11 @@ impl PromClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((
|
Ok(Triphase {
|
||||||
r.0.ok_or_else(|| anyhow!("Missing phase a"))?,
|
phase1: r.0.ok_or_else(|| anyhow!("Missing phase a"))?,
|
||||||
r.1.ok_or_else(|| anyhow!("Missing phase b"))?,
|
phase2: r.1.ok_or_else(|| anyhow!("Missing phase b"))?,
|
||||||
r.2.ok_or_else(|| anyhow!("Missing phase c"))?,
|
phase3: r.2.ok_or_else(|| anyhow!("Missing phase c"))?,
|
||||||
))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user