Compare commits

...

3 commits

Author SHA1 Message Date
11b5845182 release v1.9.9-pre-16
All checks were successful
Build and release .deb / Release (push) Successful in 54s
2025-01-09 11:38:53 +11:00
20a7ad6ddd ccs: tristar: remove useless Option around scaling 2025-01-09 11:23:33 +11:00
9d8b4b23b1 ccs: modbus tcp 2025-01-09 10:22:35 +11:00
7 changed files with 161 additions and 35 deletions

4
Cargo.lock generated
View file

@ -239,7 +239,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "charge-controller-supervisor"
version = "1.9.9-pre-15"
version = "1.9.9-pre-16"
dependencies = [
"chrono",
"clap",
@ -2203,7 +2203,7 @@ dependencies = [
[[package]]
name = "tesla-charge-controller"
version = "1.9.9-pre-15"
version = "1.9.9-pre-16"
dependencies = [
"chrono",
"clap",

View file

@ -4,7 +4,7 @@ default-members = ["charge-controller-supervisor"]
resolver = "2"
[workspace.package]
version = "1.9.9-pre-15"
version = "1.9.9-pre-16"
[workspace.lints.clippy]
pedantic = "warn"

View file

@ -14,11 +14,14 @@ pub(super) struct ConfigWatcher {
}
pub fn init_config(path: impl AsRef<std::path::Path>) -> Option<ConfigWatcher> {
let _ = CONFIG_PATH.get_or_init(|| path.as_ref().to_path_buf());
log::trace!("loading config...");
let config = Config::load(&path).unwrap();
if let Err(e) = config.save() {
log::warn!("couldn't save updated config: {e:?}");
}
log::trace!("watching config for changes...");
let config_watcher = ConfigWatcher::new(&path);
let _ = CONFIG_PATH.get_or_init(|| path.as_ref().to_path_buf());
CONFIG.set(tokio::sync::RwLock::new(config)).unwrap();
config_watcher
@ -118,9 +121,13 @@ pub async fn write_to_config<'a>() -> ConfigHandle<'a> {
#[serde(tag = "version")]
pub enum ConfigStorage {
#[serde(rename = "1")]
V1(Config),
V1(outdated::ConfigV1),
#[serde(rename = "2")]
V2(Config),
}
mod outdated;
impl Default for ConfigStorage {
fn default() -> Self {
Self::from_latest(Default::default())
@ -128,8 +135,15 @@ impl Default for ConfigStorage {
}
impl ConfigStorage {
const fn from_latest(config: Config) -> Self {
Self::V1(config)
pub const fn from_latest(config: Config) -> Self {
Self::V2(config)
}
pub fn into_latest(self) -> Config {
match self {
ConfigStorage::V1(v1) => v1.into(),
ConfigStorage::V2(config) => config,
}
}
fn load(path: impl AsRef<std::path::Path>) -> eyre::Result<Self> {
@ -146,12 +160,6 @@ impl ConfigStorage {
fn save(&self) -> eyre::Result<()> {
self.save_to(CONFIG_PATH.get().unwrap())
}
fn into_latest(self) -> Config {
match self {
ConfigStorage::V1(config) => config,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
@ -178,12 +186,11 @@ impl Config {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct ChargeControllerConfig {
pub name: String,
pub serial_port: String,
pub baud_rate: u32,
pub watch_interval_seconds: u64,
pub variant: ChargeControllerVariant,
#[serde(default)]
pub follow_primary: bool,
pub transport: Transport,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
@ -191,3 +198,10 @@ pub enum ChargeControllerVariant {
Tristar,
Pl { timeout_milliseconds: u64 },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Transport {
Serial { port: String, baud_rate: u32 },
Tcp { ip: std::net::IpAddr, port: u16 },
}

View file

@ -0,0 +1,72 @@
pub use v1::ConfigV1;
mod v1 {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
#[serde(default)]
pub struct ConfigV1 {
primary_charge_controller: String,
enable_secondary_control: bool,
charge_controllers: Vec<ChargeControllerConfigV1>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct ChargeControllerConfigV1 {
name: String,
serial_port: String,
baud_rate: u32,
watch_interval_seconds: u64,
variant: ChargeControllerVariantV1,
#[serde(default)]
follow_primary: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
enum ChargeControllerVariantV1 {
Tristar,
Pl { timeout_milliseconds: u64 },
}
impl From<ChargeControllerConfigV1> for crate::config::ChargeControllerConfig {
fn from(value: ChargeControllerConfigV1) -> Self {
Self {
name: value.name,
transport: crate::config::Transport::Serial {
port: value.serial_port,
baud_rate: value.baud_rate,
},
watch_interval_seconds: value.watch_interval_seconds,
variant: value.variant.into(),
follow_primary: value.follow_primary,
}
}
}
impl From<ChargeControllerVariantV1> for crate::config::ChargeControllerVariant {
fn from(value: ChargeControllerVariantV1) -> Self {
match value {
ChargeControllerVariantV1::Tristar => Self::Tristar,
ChargeControllerVariantV1::Pl {
timeout_milliseconds,
} => Self::Pl {
timeout_milliseconds,
},
}
}
}
impl From<ConfigV1> for crate::config::Config {
fn from(value: ConfigV1) -> Self {
Self {
primary_charge_controller: value.primary_charge_controller,
enable_secondary_control: value.enable_secondary_control,
charge_controllers: value
.charge_controllers
.into_iter()
.map(Into::into)
.collect(),
}
}
}
}

View file

@ -20,7 +20,7 @@ pub enum VoltageCommand {
}
impl Controller {
pub fn new(
pub async fn new(
config: crate::config::ChargeControllerConfig,
) -> eyre::Result<(
Self,
@ -28,16 +28,18 @@ impl Controller {
)> {
let inner = match config.variant {
crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar(
crate::tristar::Tristar::new(&config.serial_port, &config.name, config.baud_rate)?,
crate::tristar::Tristar::new(&config.name, &config.transport).await?,
),
crate::config::ChargeControllerVariant::Pl {
timeout_milliseconds,
} => ControllerInner::Pl(crate::pl::Pli::new(
&config.serial_port,
&config.name,
config.baud_rate,
timeout_milliseconds,
)?),
} => match &config.transport {
crate::config::Transport::Serial { port, baud_rate } => ControllerInner::Pl(
crate::pl::Pli::new(port, &config.name, *baud_rate, timeout_milliseconds)?,
),
crate::config::Transport::Tcp { ip: _, port: _ } => {
return Err(eyre::eyre!("pl doesn't support tcp"))
}
},
};
let data = CommonData::default();

View file

@ -59,7 +59,32 @@ async fn run() -> eyre::Result<()> {
match args.command {
Commands::Watch => watch(args).await,
Commands::GenerateConfig => {
let config = config::ConfigStorage::default();
let mut config = config::Config::default();
config
.charge_controllers
.push(config::ChargeControllerConfig {
name: String::from("tcp"),
transport: config::Transport::Tcp {
ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 102)),
port: 420,
},
watch_interval_seconds: 0,
variant: config::ChargeControllerVariant::Tristar,
follow_primary: false,
});
config
.charge_controllers
.push(config::ChargeControllerConfig {
name: String::from("serial"),
transport: config::Transport::Serial {
port: "/dev/someport".to_string(),
baud_rate: 69,
},
watch_interval_seconds: 0,
variant: config::ChargeControllerVariant::Tristar,
follow_primary: false,
});
let config = config::ConfigStorage::from_latest(config);
let json = serde_json::to_string_pretty(&config)?;
println!("{json}");
Ok(())
@ -87,7 +112,7 @@ async fn watch(args: Args) -> eyre::Result<()> {
for config in &config.charge_controllers {
let n = config.name.clone();
match controller::Controller::new(config.clone()) {
match controller::Controller::new(config.clone()).await {
Ok((v, voltage_tx)) => {
map.insert(n, v.get_data_ptr());
controllers.push(v);

View file

@ -55,9 +55,9 @@ pub struct Tristar {
scaling: Option<Scaling>,
}
#[derive(Default, Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy)]
pub struct TristarState {
scaling: Option<Scaling>,
scaling: Scaling,
battery_voltage: f64,
target_voltage: f64,
input_current: f64,
@ -86,7 +86,7 @@ impl TristarState {
fn from_ram(ram: &[u16]) -> Self {
let scaling = Scaling::from(ram);
Self {
scaling: Some(scaling),
scaling,
battery_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVbFMed]),
target_voltage: scaling.get_voltage(ram[TristarRamAddress::VbRef]),
input_current: scaling.get_current(ram[TristarRamAddress::AdcIaFShadow]),
@ -255,14 +255,27 @@ impl ChargeStateGauges {
}
impl Tristar {
pub fn new(serial_port: &str, friendly_name: &str, baud_rate: u32) -> eyre::Result<Self> {
let modbus_serial = tokio_serial::SerialStream::open(
&tokio_serial::new(serial_port, baud_rate).timeout(std::time::Duration::from_secs(3)),
)?;
pub async fn new(
friendly_name: &str,
transport: &crate::config::Transport,
) -> eyre::Result<Self> {
let slave = tokio_modbus::Slave(DEVICE_ID);
let modbus = tokio_modbus::client::rtu::attach_slave(modbus_serial, slave);
let modbus = match transport {
crate::config::Transport::Serial { port, baud_rate } => {
let modbus_serial = tokio_serial::SerialStream::open(
&tokio_serial::new(port, *baud_rate).timeout(std::time::Duration::from_secs(3)),
)?;
tokio_modbus::client::rtu::attach_slave(modbus_serial, slave)
}
crate::config::Transport::Tcp { ip, port } => {
tokio_modbus::client::tcp::connect((*ip, *port).into()).await?
}
};
let charge_state_gauges = ChargeStateGauges::new(friendly_name);
Ok(Self {
friendly_name: friendly_name.to_owned(),
modbus,
@ -275,7 +288,7 @@ impl Tristar {
pub async fn refresh(&mut self) -> eyre::Result<crate::controller::CommonData> {
let new_state = self.get_data().await?;
self.scaling = new_state.scaling;
self.scaling = Some(new_state.scaling);
self.consecutive_errors = 0;
BATTERY_VOLTAGE
.with_label_values(&[&self.friendly_name])