From 23fe7acbb0d011fc66c098e18efd047d43cbf441 Mon Sep 17 00:00:00 2001
From: Alex Janka <alex@alexjanka.com>
Date: Wed, 8 Jan 2025 22:02:40 +1100
Subject: [PATCH] ccs: modbus tcp

---
 charge-controller-supervisor/src/config.rs    | 33 ++++++---
 .../src/config/outdated.rs                    | 72 +++++++++++++++++++
 .../src/controller.rs                         | 17 ++---
 charge-controller-supervisor/src/main.rs      | 27 ++++++-
 charge-controller-supervisor/src/tristar.rs   | 22 ++++--
 5 files changed, 145 insertions(+), 26 deletions(-)
 create mode 100644 charge-controller-supervisor/src/config/outdated.rs

diff --git a/charge-controller-supervisor/src/config.rs b/charge-controller-supervisor/src/config.rs
index 1792fe9..51c48b6 100644
--- a/charge-controller-supervisor/src/config.rs
+++ b/charge-controller-supervisor/src/config.rs
@@ -121,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())
@@ -131,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> {
@@ -149,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)]
@@ -181,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)]
@@ -194,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 },
+}
diff --git a/charge-controller-supervisor/src/config/outdated.rs b/charge-controller-supervisor/src/config/outdated.rs
new file mode 100644
index 0000000..320cf72
--- /dev/null
+++ b/charge-controller-supervisor/src/config/outdated.rs
@@ -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(),
+            }
+        }
+    }
+}
diff --git a/charge-controller-supervisor/src/controller.rs b/charge-controller-supervisor/src/controller.rs
index c4b5ece..c89da12 100644
--- a/charge-controller-supervisor/src/controller.rs
+++ b/charge-controller-supervisor/src/controller.rs
@@ -28,17 +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)
-                    .await?,
+                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();
diff --git a/charge-controller-supervisor/src/main.rs b/charge-controller-supervisor/src/main.rs
index bf0b3cd..fc0c4db 100644
--- a/charge-controller-supervisor/src/main.rs
+++ b/charge-controller-supervisor/src/main.rs
@@ -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(())
diff --git a/charge-controller-supervisor/src/tristar.rs b/charge-controller-supervisor/src/tristar.rs
index 4ec9b1a..e0bce38 100644
--- a/charge-controller-supervisor/src/tristar.rs
+++ b/charge-controller-supervisor/src/tristar.rs
@@ -280,13 +280,23 @@ impl ChargeStateGauges {
 }
 
 impl Tristar {
-    pub async 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))?;
+                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 mut modbus = ModbusTimeout(modbus);
 
         let scaling = {