diff --git a/Cargo.toml b/Cargo.toml index 2b8a16d..38ca557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,5 +30,6 @@ default-trait-access = { level = "allow", priority = 1 } missing-errors-doc = { level = "allow", priority = 1 } missing-panics-doc = { level = "allow", priority = 1 } module-name-repetitions = { level = "allow", priority = 1 } +similar-names = { level = "allow", priority = 1 } struct-excessive-bools = { level = "allow", priority = 1 } too-many-lines = { level = "allow", priority = 1 } diff --git a/charge-controller-supervisor/src/controller.rs b/charge-controller-supervisor/src/controller.rs index 345fa90..90352fa 100644 --- a/charge-controller-supervisor/src/controller.rs +++ b/charge-controller-supervisor/src/controller.rs @@ -43,6 +43,12 @@ impl ControllerState { } } +#[derive(serde::Serialize, Clone)] +#[serde(tag = "model")] +pub enum ControllerSettings { + Tristar(tristar::TristarSettings), +} + #[derive(Clone, Copy, Debug)] pub enum VoltageCommand { Set(f64), @@ -55,7 +61,7 @@ impl Controller { Self, Option>, )> { - let inner = match config.variant { + let mut inner = match config.variant { crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar( tristar::Tristar::new(&config.name, &config.transport).await?, ), @@ -73,6 +79,15 @@ impl Controller { let data = std::sync::Arc::new(ControllerData::new()); + if let ControllerInner::Tristar(t) = &mut inner { + match t.read_settings().await { + Ok(v) => { + *data.write_settings().await = Some(ControllerSettings::Tristar(v)); + } + Err(e) => log::error!("couldn't read config from tristar {}: {e:?}", config.name), + } + } + let (voltage_tx, voltage_rx) = if config.follow_primary { let (a, b) = tokio::sync::mpsc::unbounded_channel(); (Some(a), Some(b)) diff --git a/charge-controller-supervisor/src/controller/tristar.rs b/charge-controller-supervisor/src/controller/tristar.rs index bd6e830..bc4ce10 100644 --- a/charge-controller-supervisor/src/controller/tristar.rs +++ b/charge-controller-supervisor/src/controller/tristar.rs @@ -80,6 +80,286 @@ impl ModbusTimeout { Ok(v) } } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct TristarSettings { + network: NetworkSettings, + charge: ChargeSettings, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct NetworkSettings { + http_port: u16, + modbus_port: u16, + ip_bridging_enabled: bool, + snmp_trap_port: u16, + ethernet_power_saving: bool, +} + +const NETWORK_DATA_ADDR_START: u16 = 0x151b; +const NETWORK_DATA_ADDR_END: u16 = 0x1521; +const NETWORK_DATA_LENGTH: u16 = NETWORK_DATA_ADDR_END - NETWORK_DATA_ADDR_START; + +impl NetworkSettings { + fn from_buf(buf: &[u16]) -> eyre::Result { + fn get(buf: &[u16], addr: TristarEepromAddress) -> eyre::Result { + let addr = (addr as u16) + .checked_sub(NETWORK_DATA_ADDR_START) + .ok_or_else(|| eyre::eyre!("wrong address range!"))?; + + let word = buf + .get::(addr.into()) + .ok_or_else(|| eyre::eyre!("address out of bounds!"))?; + Ok(*word) + } + + let http_port = get(buf, TristarEepromAddress::HTTPPort)?; + let modbus_port = get(buf, TristarEepromAddress::MBIPPort)?; + let ip_bridging_enabled = get(buf, TristarEepromAddress::NetRules)? != 0; + let snmp_trap_port = get(buf, TristarEepromAddress::SNMPTrapRecPort)?; + let ethernet_power_saving = get(buf, TristarEepromAddress::EthernetPowerSaveMode)? == 1; + + Ok(Self { + http_port, + modbus_port, + ip_bridging_enabled, + snmp_trap_port, + ethernet_power_saving, + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct ChargeSettings { + absorption_voltage: f64, + float_voltage: f64, + absorption_time_seconds: u16, + absorption_extension_time_seconds: u16, + absorption_extension_voltage: f64, + float_cancel_voltage: f64, + float_exit_timer_seconds: u16, + equalize_voltage: f64, + equalize_interval_days: u16, + equalize_time_limit_above_vreg: u16, + equalize_time_limit_at_veq: u16, + battery_service_days: u16, + temperature_compensation_raw: u16, + high_voltage_disconnect: f64, + high_voltage_reconnect: f64, + battery_charge_reference_limit: f64, + max_temp_comp_raw: u16, + min_temp_comp_raw: u16, + modbus_slave_address: u16, + meterbus_address: u16, + battery_current_limit: f64, + array_target_voltage: f64, + array_target_voltage_percentage_raw: u16, + led_thresholds: LedThresholds, + read_only: ReadOnly, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct LedThresholds { + green_to_greenyellow: f64, + greenyellow_to_yellow: f64, + yellow_to_yellowred: f64, + yellowred_to_red: f64, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct ReadOnly { + hourmeter: u32, + charge_ah_resetable: f64, + charge_ah_total: f64, + charge_kwh_resetable: u16, + charge_kwh_total: u16, + vb_min: f64, + vb_max: f64, + va_max: f64, + days_since_last_equalize: u16, + battery_service_timer_days: u16, + serial: u64, + model: Model, + hardware_version: u16, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum Model { + Tristar45A, + Tristar60A, +} + +const CHARGE_DATA_ADDR_START: u16 = 0xE000; + +impl ChargeSettings { + fn from_buf(buf: &[u16], scaling: &Scaling) -> eyre::Result { + fn get(buf: &[u16], address: TristarEepromAddress) -> eyre::Result { + let addr = (address as u16) + .checked_sub(CHARGE_DATA_ADDR_START) + .ok_or_else(|| eyre::eyre!("wrong address range!"))?; + + let word = buf + .get::(addr.into()) + .ok_or_else(|| eyre::eyre!("address {address:?} out of bounds at {addr:#X?}!"))?; + Ok(*word) + } + + let absorption_voltage = scaling.get_voltage(get(buf, TristarEepromAddress::EvAbsorp)?); + let float_voltage = scaling.get_voltage(get(buf, TristarEepromAddress::EvFloat)?); + + let absorption_time_seconds = get(buf, TristarEepromAddress::EtAbsorp)?; + let absorption_extension_time_seconds = get(buf, TristarEepromAddress::EtAbsorp)?; + + let absorption_extension_voltage = + scaling.get_voltage(get(buf, TristarEepromAddress::EvAbsorpExt)?); + let float_cancel_voltage = + scaling.get_voltage(get(buf, TristarEepromAddress::EvFloatCancel)?); + + let float_exit_timer_seconds = get(buf, TristarEepromAddress::EtFloatExitCum)?; + + let equalize_voltage = scaling.get_voltage(get(buf, TristarEepromAddress::EvEq)?); + let equalize_interval_days = get(buf, TristarEepromAddress::EtEqcalendar)?; + let equalize_time_limit_above_vreg = get(buf, TristarEepromAddress::EtEqAbove)?; + let equalize_time_limit_at_veq = get(buf, TristarEepromAddress::EtEqReg)?; + + let battery_service_days = get(buf, TristarEepromAddress::EtBattService)?; + + let temperature_compensation_raw = get(buf, TristarEepromAddress::EvTempcomp)?; + + let high_voltage_disconnect = scaling.get_voltage(get(buf, TristarEepromAddress::EvHvd)?); + let high_voltage_reconnect = scaling.get_voltage(get(buf, TristarEepromAddress::EvHvr)?); + + let battery_charge_reference_limit = + scaling.get_voltage(get(buf, TristarEepromAddress::EvbRefLim)?); + + let max_temp_comp_raw = get(buf, TristarEepromAddress::EtbMax)?; + let min_temp_comp_raw = get(buf, TristarEepromAddress::EtbMin)?; + + let led_thresholds = { + let green_to_greenyellow = + scaling.get_voltage(get(buf, TristarEepromAddress::EvSocGGy)?); + let greenyellow_to_yellow = + scaling.get_voltage(get(buf, TristarEepromAddress::EvSocGyY)?); + let yellow_to_yellowred = + scaling.get_voltage(get(buf, TristarEepromAddress::EvSocYYr)?); + let yellowred_to_red = scaling.get_voltage(get(buf, TristarEepromAddress::EvSocYrR)?); + + LedThresholds { + green_to_greenyellow, + greenyellow_to_yellow, + yellow_to_yellowred, + yellowred_to_red, + } + }; + + let modbus_slave_address = get(buf, TristarEepromAddress::EmodbusId)?; + let meterbus_address = get(buf, TristarEepromAddress::EmeterbusId)?; + + let battery_current_limit = scaling.get_current(get(buf, TristarEepromAddress::EibLim)?); + + let array_target_voltage = + scaling.get_voltage(get(buf, TristarEepromAddress::EvaRefFixedInit)?); + let array_target_voltage_percentage_raw = + get(buf, TristarEepromAddress::EvaRefFixedPctInit)?; + + let read_only = { + let hourmeter = { + let hourmeter_lo = get(buf, TristarEepromAddress::EhourmeterLo)?; + let hourmeter_hi = get(buf, TristarEepromAddress::EhourmeterHi)?; + u32::from(hourmeter_lo) | (u32::from(hourmeter_hi) << 16) + }; + + let charge_ah_resetable = { + let charge_ah_resetable_lo = get(buf, TristarEepromAddress::EahcRLo)?; + let charge_ah_resetable_hi = get(buf, TristarEepromAddress::EahcRHi)?; + let tenths = + u32::from(charge_ah_resetable_lo) | (u32::from(charge_ah_resetable_hi) << 16); + f64::from(tenths) * 0.1 + }; + + let charge_ah_total = { + let charge_ah_total_lo = get(buf, TristarEepromAddress::EahcTLo)?; + let charge_ah_total_hi = get(buf, TristarEepromAddress::EahcTHi)?; + let tenths = u32::from(charge_ah_total_lo) | (u32::from(charge_ah_total_hi) << 16); + f64::from(tenths) * 0.1 + }; + + let charge_kwh_resetable = get(buf, TristarEepromAddress::EkWhcR)?; + let charge_kwh_total = get(buf, TristarEepromAddress::EkWhcT)?; + + let vb_min = scaling.get_voltage(get(buf, TristarEepromAddress::EvbMin)?); + let vb_max = scaling.get_voltage(get(buf, TristarEepromAddress::EvbMax)?); + let va_max = scaling.get_voltage(get(buf, TristarEepromAddress::EvaMax)?); + + let days_since_last_equalize = get(buf, TristarEepromAddress::EtmrEqcalendar)?; + let battery_service_timer_days = get(buf, TristarEepromAddress::EtmrBattService)?; + + let serial = { + let s0 = get(buf, TristarEepromAddress::Eserial0)?; + let s1 = get(buf, TristarEepromAddress::Eserial1)?; + let s2 = get(buf, TristarEepromAddress::Eserial2)?; + let s3 = get(buf, TristarEepromAddress::Eserial3)?; + + let mut serial = u64::from(s0); + serial |= u64::from(s1) << 16; + serial |= u64::from(s2) << 32; + serial |= u64::from(s3) << 48; + serial + }; + + let model = if get(buf, TristarEepromAddress::Emodel)? == 0 { + Model::Tristar45A + } else { + Model::Tristar60A + }; + + let hardware_version = get(buf, TristarEepromAddress::EhwVersion)?; + + ReadOnly { + hourmeter, + charge_ah_resetable, + charge_ah_total, + charge_kwh_resetable, + charge_kwh_total, + vb_min, + vb_max, + va_max, + days_since_last_equalize, + battery_service_timer_days, + serial, + model, + hardware_version, + } + }; + + Ok(Self { + absorption_voltage, + float_voltage, + absorption_time_seconds, + absorption_extension_time_seconds, + absorption_extension_voltage, + float_cancel_voltage, + float_exit_timer_seconds, + equalize_voltage, + equalize_interval_days, + equalize_time_limit_above_vreg, + equalize_time_limit_at_veq, + battery_service_days, + temperature_compensation_raw, + high_voltage_disconnect, + high_voltage_reconnect, + battery_charge_reference_limit, + max_temp_comp_raw, + min_temp_comp_raw, + modbus_slave_address, + meterbus_address, + battery_current_limit, + array_target_voltage, + array_target_voltage_percentage_raw, + led_thresholds, + read_only, + }) + } +} #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct TristarState { @@ -370,6 +650,33 @@ impl Tristar { Ok(new_state) } + pub async fn read_settings(&mut self) -> eyre::Result { + let network_data = self + .modbus + .read_holding_registers(NETWORK_DATA_ADDR_START, NETWORK_DATA_LENGTH) + .await?; + + let network = NetworkSettings::from_buf(&network_data)?; + + let charge_data_1 = self + .modbus + .read_holding_registers(CHARGE_DATA_ADDR_START, 0x22) + .await?; + + let charge_data_2 = self + .modbus + .read_holding_registers(CHARGE_DATA_ADDR_START + 0x80, 0x4e) + .await?; + + let mut charge_data = vec![0; 0xCE]; + charge_data[..0x22].copy_from_slice(&charge_data_1); + charge_data[0x80..].copy_from_slice(&charge_data_2); + + let charge = ChargeSettings::from_buf(&charge_data, &self.scaling)?; + + Ok(TristarSettings { network, charge }) + } + pub async fn set_target_voltage(&mut self, target_voltage: f64) -> eyre::Result<()> { let scaled_voltage: u16 = self.scale_voltage(target_voltage); self.modbus @@ -397,6 +704,7 @@ impl Tristar { } } +#[repr(u16)] pub enum TristarRamAddress { AdcVbFMed = 0x0018, AdcVbtermF = 0x0019, @@ -457,6 +765,72 @@ pub enum TristarRamAddress { VaRefFixedPc = 0x005B, } +#[repr(u16)] +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub enum TristarEepromAddress { + // eeprom + // tcp network settings + HTTPPort = 0x151B, + MBIPPort = 0x151C, + NetRules = 0x151D, + SNMPTrapRecPort = 0x151E, + EthernetPowerSaveMode = 0x151F, + // beta 8.21 + VLANEnable = 0x1520, + VLANParameters = 0x1521, + + // charge settings + EvAbsorp = 0xE000, + EvFloat = 0xE001, + EtAbsorp = 0xE002, + EtAbsorpExt = 0xE003, + EvAbsorpExt = 0xE004, + EvFloatCancel = 0xE005, + EtFloatExitCum = 0xE006, + EvEq = 0xE007, + EtEqcalendar = 0xE008, + EtEqAbove = 0xE009, + EtEqReg = 0xE00A, + EtBattService = 0xE00B, + EvTempcomp = 0xE00D, + EvHvd = 0xE00E, + EvHvr = 0xE00F, + EvbRefLim = 0xE010, + EtbMax = 0xE011, + EtbMin = 0xE012, + EvSocGGy = 0xE015, + EvSocGyY = 0xE016, + EvSocYYr = 0xE017, + EvSocYrR = 0xE018, + EmodbusId = 0xE019, + EmeterbusId = 0xE01A, + EibLim = 0xE01D, + EvaRefFixedInit = 0xE020, + EvaRefFixedPctInit = 0xE021, + + // read only + EhourmeterLo = 0xE080, + EhourmeterHi = 0xE081, + EahcRLo = 0xE082, + EahcRHi = 0xE083, + EahcTLo = 0xE084, + EahcTHi = 0xE085, + EkWhcR = 0xE086, + EkWhcT = 0xE087, + EvbMin = 0xE088, + EvbMax = 0xE089, + EvaMax = 0xE08A, + EtmrEqcalendar = 0xE08B, + EtmrBattService = 0xE08C, + Eserial0 = 0xE0C0, + Eserial1 = 0xE0C1, + Eserial2 = 0xE0C2, + Eserial3 = 0xE0C3, + Emodel = 0xE0CC, + EhwVersion = 0xE0CD, +} + impl std::ops::Index for [u16] { type Output = u16; diff --git a/charge-controller-supervisor/src/storage.rs b/charge-controller-supervisor/src/storage.rs index 48eb84a..e3bb9f2 100644 --- a/charge-controller-supervisor/src/storage.rs +++ b/charge-controller-supervisor/src/storage.rs @@ -24,12 +24,14 @@ impl AllControllers { pub struct ControllerData { state: tokio::sync::RwLock>, + settings: tokio::sync::RwLock>, } impl ControllerData { pub const fn new() -> Self { Self { state: tokio::sync::RwLock::const_new(None), + settings: tokio::sync::RwLock::const_new(None), } } @@ -44,4 +46,16 @@ impl ControllerData { ) -> tokio::sync::RwLockReadGuard> { self.state.read().await } + + pub async fn write_settings( + &self, + ) -> tokio::sync::RwLockWriteGuard> { + self.settings.write().await + } + + pub async fn read_settings( + &self, + ) -> tokio::sync::RwLockReadGuard> { + self.settings.read().await + } } diff --git a/charge-controller-supervisor/src/web.rs b/charge-controller-supervisor/src/web.rs index 30add36..d90a297 100644 --- a/charge-controller-supervisor/src/web.rs +++ b/charge-controller-supervisor/src/web.rs @@ -49,6 +49,7 @@ pub fn rocket(state: ServerState) -> rocket::Rocket { interfaces, all_interfaces, all_interfaces_full, + all_interfaces_settings, primary_interface, interface, interface_full, @@ -110,6 +111,22 @@ async fn all_interfaces_full( Json(data) } +#[get("/interfaces/settings")] +async fn all_interfaces_settings( + state: &State, +) -> Json> { + let mut data = Vec::new(); + + for (k, v) in state.data.all_data() { + let v = v.read_settings().await; + if let Some(v) = v.as_ref() { + data.push((k.clone(), v.clone())); + } + } + + Json(data) +} + #[get("/interface/")] async fn interface( name: &str,