ccs: tristar: expose settings
This commit is contained in:
parent
ed82c3444e
commit
a908490bb0
5 changed files with 422 additions and 1 deletions
|
@ -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 }
|
||||
|
|
|
@ -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<tokio::sync::mpsc::UnboundedSender<VoltageCommand>>,
|
||||
)> {
|
||||
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))
|
||||
|
|
|
@ -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<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)]
|
||||
pub struct TristarState {
|
||||
|
@ -370,6 +650,33 @@ impl Tristar {
|
|||
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<()> {
|
||||
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<TristarRamAddress> for [u16] {
|
||||
type Output = u16;
|
||||
|
||||
|
|
|
@ -24,12 +24,14 @@ impl AllControllers {
|
|||
|
||||
pub struct ControllerData {
|
||||
state: tokio::sync::RwLock<Option<crate::controller::ControllerState>>,
|
||||
settings: tokio::sync::RwLock<Option<crate::controller::ControllerSettings>>,
|
||||
}
|
||||
|
||||
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<Option<crate::controller::ControllerState>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ pub fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
|
|||
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<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>")]
|
||||
async fn interface(
|
||||
name: &str,
|
||||
|
|
Loading…
Add table
Reference in a new issue