use std::collections::HashMap; use prettytable::{row, Table}; use crate::constants; use crate::devices::device::{Device, DeviceId, DeviceRelay, DeviceRelayID}; use crate::energy::consumption::EnergyConsumption; use crate::utils::time_utils::time_secs; #[derive(Default)] pub struct DeviceState { pub last_ping: u64, } impl DeviceState { pub fn record_ping(&mut self) { self.last_ping = time_secs(); } pub fn is_online(&self) -> bool { (time_secs() - self.last_ping) < constants::DEVICE_MAX_PING_TIME } } #[derive(Default, Clone)] pub struct RelayState { on: bool, since: usize, } impl RelayState { fn is_on(&self) -> bool { self.on } fn is_off(&self) -> bool { !self.on } } type RelaysState = HashMap; #[derive(Default)] pub struct EnergyEngine { devices_state: HashMap, relays_state: RelaysState, } impl DeviceRelay { // Note : this function is not recursive fn has_running_dependencies(&self, s: &RelaysState, devices: &[Device]) -> bool { for d in devices { for r in &d.relays { if r.depends_on.contains(&self.id) && s.get(&r.id).unwrap().is_on() { return true; } } } false } // Note : this function is not recursive fn is_missing_dependencies(&self, s: &RelaysState) -> bool { self.depends_on.iter().any(|id| s.get(id).unwrap().is_off()) } fn is_having_conflict(&self, s: &RelaysState, devices: &[Device]) -> bool { if self .conflicts_with .iter() .any(|id| s.get(id).unwrap().is_on()) { return true; } // Reverse search for device in devices { for r in &device.relays { if s.get(&r.id).unwrap().is_on() && r.conflicts_with.contains(&self.id) { return true; } } } false } } impl EnergyEngine { pub fn device_state(&mut self, dev_id: &DeviceId) -> &mut DeviceState { self.devices_state.entry(dev_id.clone()).or_default(); self.devices_state.get_mut(dev_id).unwrap() } pub fn relay_state(&mut self, relay_id: DeviceRelayID) -> &mut RelayState { self.relays_state.entry(relay_id).or_default(); self.relays_state.get_mut(&relay_id).unwrap() } fn print_summary(&mut self, curr_consumption: EnergyConsumption, devices: &[Device]) { log::info!("Current consumption: {curr_consumption}"); let mut table = Table::new(); table.add_row(row!["Device", "Relay", "On", "Since"]); for d in devices { for r in &d.relays { let status = self.relay_state(r.id); table.add_row(row![ d.name, r.name, status.is_on().to_string(), status.since ]); } } table.printstd(); } pub fn refresh(&mut self, curr_consumption: EnergyConsumption, devices: &[Device]) { // Force creation of missing relays state for d in devices { for r in &d.relays { // Requesting relay state is enough to trigger relay creation self.relay_state(r.id); } } let mut new_relays_state = self.relays_state.clone(); // Forcefully turn off relays that belongs to offline devices for d in devices { if !self.device_state(&d.id).is_online() { for r in &d.relays { new_relays_state.get_mut(&r.id).unwrap().on = false; } } } // Forcefully turn off disabled relays for d in devices { for r in &d.relays { if !r.enabled { new_relays_state.get_mut(&r.id).unwrap().on = false; } } } // Forcefully turn off relays with missing dependency loop { let mut changed = false; for d in devices { for r in &d.relays { if new_relays_state.get(&r.id).unwrap().is_off() { continue; } // Check if any dependency of relay is off if r.is_missing_dependencies(&new_relays_state) { new_relays_state.get_mut(&r.id).unwrap().on = false; changed = true; } } } if !changed { break; } } // Virtually turn off all relays that can be stopped loop { let mut changed = false; for d in devices { for r in &d.relays { let state = new_relays_state.get(&r.id).unwrap(); if state.is_off() { continue; } // Check if minimal runtime has not been reached if (state.since + r.minimal_uptime) as i64 > time_secs() as i64 { continue; } // Check that no relay that depends on this relay are turned on if r.has_running_dependencies(&new_relays_state, devices) { continue; } new_relays_state.get_mut(&r.id).unwrap().on = false; changed = true; } } if !changed { break; } } // TODO Turn on relays with running constraints (only ENABLED) // Order relays let mut ordered_relays = devices .iter() .filter(|d| self.device_state(&d.id).is_online() && d.enabled) .flat_map(|d| &d.relays) .filter(|r| r.enabled) .collect::>(); ordered_relays.sort_by_key(|r| r.priority); ordered_relays.reverse(); loop { let mut changed = false; for relay in &ordered_relays { if new_relays_state.get(&relay.id).unwrap().is_on() { continue; } if !relay.enabled { continue; } let real_relay_state = self.relays_state.get(&relay.id).unwrap(); if real_relay_state.is_off() && (real_relay_state.since + relay.minimal_downtime) as u64 > time_secs() { continue; } if relay.is_missing_dependencies(&new_relays_state) { continue; } if relay.is_having_conflict(&new_relays_state, devices) { continue; } // TODO : check consumption log::info!("Turn on relay {}", relay.name); new_relays_state.get_mut(&relay.id).unwrap().on = true; changed = true; } if !changed { break; } } // Commit changes for (id, new_state) in &new_relays_state { let curr_state = self.relay_state(*id); if curr_state.on != new_state.on { curr_state.on = new_state.on; curr_state.since = time_secs() as usize; log::info!("Changing state of {id:?} to {}", new_state.on); } } self.print_summary(curr_consumption, devices); } }