ccs: tristar: expose settings

This commit is contained in:
Alex Janka 2025-01-10 13:57:23 +11:00
parent ed82c3444e
commit a908490bb0
5 changed files with 422 additions and 1 deletions

View file

@ -30,5 +30,6 @@ default-trait-access = { level = "allow", priority = 1 }
missing-errors-doc = { level = "allow", priority = 1 } missing-errors-doc = { level = "allow", priority = 1 }
missing-panics-doc = { level = "allow", priority = 1 } missing-panics-doc = { level = "allow", priority = 1 }
module-name-repetitions = { level = "allow", priority = 1 } module-name-repetitions = { level = "allow", priority = 1 }
similar-names = { level = "allow", priority = 1 }
struct-excessive-bools = { level = "allow", priority = 1 } struct-excessive-bools = { level = "allow", priority = 1 }
too-many-lines = { level = "allow", priority = 1 } too-many-lines = { level = "allow", priority = 1 }

View file

@ -43,6 +43,12 @@ impl ControllerState {
} }
} }
#[derive(serde::Serialize, Clone)]
#[serde(tag = "model")]
pub enum ControllerSettings {
Tristar(tristar::TristarSettings),
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum VoltageCommand { pub enum VoltageCommand {
Set(f64), Set(f64),
@ -55,7 +61,7 @@ impl Controller {
Self, Self,
Option<tokio::sync::mpsc::UnboundedSender<VoltageCommand>>, Option<tokio::sync::mpsc::UnboundedSender<VoltageCommand>>,
)> { )> {
let inner = match config.variant { let mut inner = match config.variant {
crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar( crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar(
tristar::Tristar::new(&config.name, &config.transport).await?, tristar::Tristar::new(&config.name, &config.transport).await?,
), ),
@ -73,6 +79,15 @@ impl Controller {
let data = std::sync::Arc::new(ControllerData::new()); 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 (voltage_tx, voltage_rx) = if config.follow_primary {
let (a, b) = tokio::sync::mpsc::unbounded_channel(); let (a, b) = tokio::sync::mpsc::unbounded_channel();
(Some(a), Some(b)) (Some(a), Some(b))

View file

@ -80,6 +80,286 @@ impl ModbusTimeout {
Ok(v) 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<Self> {
fn get(buf: &[u16], addr: TristarEepromAddress) -> eyre::Result<u16> {
let addr = (addr as u16)
.checked_sub(NETWORK_DATA_ADDR_START)
.ok_or_else(|| eyre::eyre!("wrong address range!"))?;
let word = buf
.get::<usize>(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<Self> {
fn get(buf: &[u16], address: TristarEepromAddress) -> eyre::Result<u16> {
let addr = (address as u16)
.checked_sub(CHARGE_DATA_ADDR_START)
.ok_or_else(|| eyre::eyre!("wrong address range!"))?;
let word = buf
.get::<usize>(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)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct TristarState { pub struct TristarState {
@ -370,6 +650,33 @@ impl Tristar {
Ok(new_state) Ok(new_state)
} }
pub async fn read_settings(&mut self) -> eyre::Result<TristarSettings> {
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<()> { pub async fn set_target_voltage(&mut self, target_voltage: f64) -> eyre::Result<()> {
let scaled_voltage: u16 = self.scale_voltage(target_voltage); let scaled_voltage: u16 = self.scale_voltage(target_voltage);
self.modbus self.modbus
@ -397,6 +704,7 @@ impl Tristar {
} }
} }
#[repr(u16)]
pub enum TristarRamAddress { pub enum TristarRamAddress {
AdcVbFMed = 0x0018, AdcVbFMed = 0x0018,
AdcVbtermF = 0x0019, AdcVbtermF = 0x0019,
@ -457,6 +765,72 @@ pub enum TristarRamAddress {
VaRefFixedPc = 0x005B, 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<TristarRamAddress> for [u16] { impl std::ops::Index<TristarRamAddress> for [u16] {
type Output = u16; type Output = u16;

View file

@ -24,12 +24,14 @@ impl AllControllers {
pub struct ControllerData { pub struct ControllerData {
state: tokio::sync::RwLock<Option<crate::controller::ControllerState>>, state: tokio::sync::RwLock<Option<crate::controller::ControllerState>>,
settings: tokio::sync::RwLock<Option<crate::controller::ControllerSettings>>,
} }
impl ControllerData { impl ControllerData {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
state: tokio::sync::RwLock::const_new(None), state: tokio::sync::RwLock::const_new(None),
settings: tokio::sync::RwLock::const_new(None),
} }
} }
@ -44,4 +46,16 @@ impl ControllerData {
) -> tokio::sync::RwLockReadGuard<Option<crate::controller::ControllerState>> { ) -> tokio::sync::RwLockReadGuard<Option<crate::controller::ControllerState>> {
self.state.read().await self.state.read().await
} }
pub async fn write_settings(
&self,
) -> tokio::sync::RwLockWriteGuard<Option<crate::controller::ControllerSettings>> {
self.settings.write().await
}
pub async fn read_settings(
&self,
) -> tokio::sync::RwLockReadGuard<Option<crate::controller::ControllerSettings>> {
self.settings.read().await
}
} }

View file

@ -49,6 +49,7 @@ pub fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
interfaces, interfaces,
all_interfaces, all_interfaces,
all_interfaces_full, all_interfaces_full,
all_interfaces_settings,
primary_interface, primary_interface,
interface, interface,
interface_full, interface_full,
@ -110,6 +111,22 @@ async fn all_interfaces_full(
Json(data) Json(data)
} }
#[get("/interfaces/settings")]
async fn all_interfaces_settings(
state: &State<ServerState>,
) -> Json<Vec<(String, crate::controller::ControllerSettings)>> {
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/<name>")] #[get("/interface/<name>")]
async fn interface( async fn interface(
name: &str, name: &str,