tesla-charge-controller/src/charge_controllers/tristar.rs

448 lines
13 KiB
Rust
Raw Normal View History

2024-01-18 16:26:58 +11:00
use libmodbus_rs::{Modbus, ModbusClient, ModbusRTU};
use metrics::{gauge, Gauge, Label};
use crate::errors::TristarError;
use super::{gauge_names, register_tristar_metrics};
const DEVICE_ID: u8 = 0x01;
const RAM_DATA_SIZE: u16 = 0x005B;
const RAM_ARRAY_SIZE: usize = RAM_DATA_SIZE as usize + 1;
#[derive(Debug, Clone)]
pub struct Scaling {
pub v_scale: f64,
pub i_scale: f64,
}
impl Scaling {
fn from(data: &[u16]) -> Self {
Self::from_internal(data[0], data[1], data[2], data[3])
}
fn from_internal(v_pu_hi: u16, v_pu_lo: u16, i_pu_hi: u16, i_pu_lo: u16) -> Self {
Self {
v_scale: v_pu_hi as f64 + (v_pu_lo as f64 / f64::powf(2., 16.)),
i_scale: i_pu_hi as f64 + (i_pu_lo as f64 / f64::powf(2., 16.)),
}
}
fn get_voltage(&self, data: u16) -> f64 {
data as f64 * self.v_scale * f64::powf(2., -15.)
}
fn get_current(&self, data: u16) -> f64 {
data as f64 * self.i_scale * f64::powf(2., -15.)
}
fn get_power(&self, data: u16) -> f64 {
data as f64 * self.v_scale * self.i_scale * f64::powf(2., -17.)
}
}
pub struct Tristar {
state: TristarState,
gauges: TristarGauges,
modbus: Modbus,
data_in: [u16; RAM_ARRAY_SIZE],
}
pub struct TristarGauges {
battery_voltage: Gauge,
target_voltage: Gauge,
input_current: Gauge,
battery_temp: Gauge,
charge_state: ChargeStateGauges,
tristar_input_voltage: Gauge,
tristar_charge_current: Gauge,
tristar_power_out: Gauge,
tristar_power_in: Gauge,
tristar_max_array_power: Gauge,
tristar_max_array_voltage: Gauge,
tristar_open_circuit_voltage: Gauge,
}
impl TristarGauges {
fn new(serial_port: String) -> Self {
let device_labels = vec![
Label::new(gauge_names::CHARGE_CONTROLLER_LABEL, serial_port.clone()),
Label::new(gauge_names::TRISTAR_LABEL, serial_port),
];
let battery_voltage = gauge!(gauge_names::BATTERY_VOLTAGE, device_labels.clone());
let target_voltage = gauge!(gauge_names::TARGET_VOLTAGE, device_labels.clone());
let input_current = gauge!(gauge_names::INPUT_CURRENT, device_labels.clone());
let battery_temp = gauge!(gauge_names::BATTERY_TEMP, device_labels.clone());
let charge_state = ChargeStateGauges::new(device_labels.clone());
let tristar_input_voltage =
gauge!(gauge_names::TRISTAR_INPUT_VOLTAGE, device_labels.clone());
let tristar_charge_current =
gauge!(gauge_names::TRISTAR_CHARGE_CURRENT, device_labels.clone());
let tristar_power_out = gauge!(gauge_names::TRISTAR_POWER_OUT, device_labels.clone());
let tristar_power_in = gauge!(gauge_names::TRISTAR_POWER_IN, device_labels.clone());
let tristar_max_array_power =
gauge!(gauge_names::TRISTAR_MAX_ARRAY_POWER, device_labels.clone());
let tristar_max_array_voltage = gauge!(
gauge_names::TRISTAR_MAX_ARRAY_VOLTAGE,
device_labels.clone()
);
let tristar_open_circuit_voltage = gauge!(
gauge_names::TRISTAR_OPEN_CIRCUIT_VOLTAGE,
device_labels.clone()
);
Self {
battery_voltage,
target_voltage,
input_current,
battery_temp,
charge_state,
tristar_input_voltage,
tristar_charge_current,
tristar_power_out,
tristar_power_in,
tristar_max_array_power,
tristar_max_array_voltage,
tristar_open_circuit_voltage,
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct TristarState {
battery_voltage: f64,
target_voltage: f64,
input_current: f64,
battery_temp: u16,
charge_state: ChargeState,
tristar_input_voltage: f64,
tristar_charge_current: f64,
tristar_power_out: f64,
tristar_power_in: f64,
tristar_max_array_power: f64,
tristar_max_array_voltage: f64,
tristar_open_circuit_voltage: f64,
}
impl TristarState {
fn from_ram(scaling: Scaling, ram: &[u16]) -> Self {
Self {
battery_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVbFMed]),
target_voltage: scaling.get_voltage(ram[TristarRamAddress::VbRef]),
input_current: scaling.get_current(ram[TristarRamAddress::AdcIaFShadow]),
battery_temp: ram[TristarRamAddress::Tbatt],
charge_state: ChargeState::from(ram[TristarRamAddress::ChargeState]),
tristar_input_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVaF]),
tristar_charge_current: scaling.get_current(ram[TristarRamAddress::AdcIbFShadow]),
tristar_power_out: scaling.get_power(ram[TristarRamAddress::PowerOutShadow]),
tristar_power_in: scaling.get_power(ram[TristarRamAddress::PowerInShadow]),
tristar_max_array_power: scaling.get_power(ram[TristarRamAddress::SweepPinMax]),
tristar_max_array_voltage: scaling.get_voltage(ram[TristarRamAddress::SweepVmp]),
tristar_open_circuit_voltage: scaling.get_voltage(ram[TristarRamAddress::SweepVoc]),
}
}
}
#[derive(Debug, Clone, Copy)]
enum ChargeState {
Start,
NightCheck,
Disconnect,
Night,
Fault,
Mppt,
Absorption,
Float,
Equalize,
Slave,
Unknown,
}
impl Default for ChargeState {
fn default() -> Self {
Self::Unknown
}
}
impl From<u16> for ChargeState {
fn from(value: u16) -> Self {
match value {
0 => Self::Start,
1 => Self::NightCheck,
2 => Self::Disconnect,
3 => Self::Night,
4 => Self::Fault,
5 => Self::Mppt,
6 => Self::Absorption,
7 => Self::Float,
8 => Self::Equalize,
9 => Self::Slave,
_ => Self::Unknown,
}
}
}
struct ChargeStateGauges {
start: Gauge,
night_check: Gauge,
disconnect: Gauge,
night: Gauge,
fault: Gauge,
mppt: Gauge,
absorption: Gauge,
float: Gauge,
equalize: Gauge,
slave: Gauge,
unknown: Gauge,
}
impl ChargeStateGauges {
fn new(labels: Vec<Label>) -> Self {
let start = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("start"))]
]
.concat()
);
let night_check = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("night_check"))]
]
.concat()
);
let disconnect = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("disconnect"))]
]
.concat()
);
let night = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("night"))]
]
.concat()
);
let fault = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("fault"))]
]
.concat()
);
let mppt = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("mppt"))]
]
.concat()
);
let absorption = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("absorption"))]
]
.concat()
);
let float = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("float"))]
]
.concat()
);
let equalize = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("equalize"))]
]
.concat()
);
let slave = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("slave"))]
]
.concat()
);
let unknown = gauge!(
gauge_names::CHARGE_STATE,
[
labels.clone(),
vec![Label::new("state", String::from("unknown"))]
]
.concat()
);
Self {
start,
night_check,
disconnect,
night,
fault,
mppt,
absorption,
float,
equalize,
slave,
unknown,
}
}
fn zero_all(&mut self) {
self.start.set(0.);
self.night_check.set(0.);
self.disconnect.set(0.);
self.night.set(0.);
self.fault.set(0.);
self.mppt.set(0.);
self.absorption.set(0.);
self.float.set(0.);
self.equalize.set(0.);
self.slave.set(0.);
self.unknown.set(0.);
}
fn set(&mut self, state: ChargeState) {
match state {
ChargeState::Start => {
self.zero_all();
self.start.set(1.);
}
ChargeState::NightCheck => {
self.zero_all();
self.night_check.set(1.);
}
ChargeState::Disconnect => {
self.zero_all();
self.disconnect.set(1.);
}
ChargeState::Night => {
self.zero_all();
self.night.set(1.);
}
ChargeState::Fault => {
self.zero_all();
self.fault.set(1.);
}
ChargeState::Mppt => {
self.zero_all();
self.mppt.set(1.);
}
ChargeState::Absorption => {
self.zero_all();
self.absorption.set(1.);
}
ChargeState::Float => {
self.zero_all();
self.float.set(1.);
}
ChargeState::Equalize => {
self.zero_all();
self.equalize.set(1.);
}
ChargeState::Slave => {
self.zero_all();
self.slave.set(1.);
}
ChargeState::Unknown => {
self.zero_all();
self.unknown.set(1.);
}
}
}
}
impl Tristar {
pub fn new(serial_port: String, baud_rate: i32) -> Result<Self, TristarError> {
let parity = 'N';
let data_bit = 8;
let stop_bit = 2;
let mut modbus = Modbus::new_rtu(&serial_port, baud_rate, parity, data_bit, stop_bit)?;
modbus.set_slave(DEVICE_ID)?;
modbus.connect()?;
register_tristar_metrics();
Ok(Self {
state: Default::default(),
gauges: TristarGauges::new(serial_port.clone()),
modbus,
data_in: [0; RAM_ARRAY_SIZE],
})
}
pub fn refresh(&mut self) {
if let Ok(new_state) = self
.get_data()
.map(|scaling| TristarState::from_ram(scaling, &self.data_in))
{
self.gauges.battery_voltage.set(new_state.battery_voltage);
self.gauges.target_voltage.set(new_state.target_voltage);
self.gauges.input_current.set(new_state.input_current);
self.gauges.battery_temp.set(new_state.battery_temp as f64);
self.gauges.charge_state.set(new_state.charge_state);
self.gauges
.tristar_input_voltage
.set(new_state.tristar_input_voltage);
self.gauges
.tristar_charge_current
.set(new_state.tristar_charge_current);
self.gauges
.tristar_power_out
.set(new_state.tristar_power_out);
self.gauges.tristar_power_in.set(new_state.tristar_power_in);
self.gauges
.tristar_max_array_power
.set(new_state.tristar_max_array_power);
self.gauges
.tristar_max_array_voltage
.set(new_state.tristar_max_array_voltage);
self.gauges
.tristar_open_circuit_voltage
.set(new_state.tristar_open_circuit_voltage);
self.state = new_state;
}
}
fn get_data(&mut self) -> Result<Scaling, TristarError> {
self.modbus
.read_registers(0x0000, RAM_DATA_SIZE + 1, &mut self.data_in)?;
let scaling = Scaling::from(&self.data_in);
Ok(scaling)
}
}
enum TristarRamAddress {
AdcVbFMed = 25,
AdcVaF = 28,
Tbatt = 38,
AdcIbFShadow = 29,
AdcIaFShadow = 30,
ChargeState = 51,
VbRef = 52,
PowerOutShadow = 59,
PowerInShadow = 60,
SweepPinMax = 61,
SweepVmp = 62,
SweepVoc = 63,
}
impl std::ops::Index<TristarRamAddress> for [u16] {
type Output = u16;
fn index(&self, index: TristarRamAddress) -> &Self::Output {
&self[index as usize]
}
}