Skip to main content

matc/devman/
device.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5pub struct Device {
6    pub node_id: u64,
7    pub address: String,
8    pub name: String,
9    /// MRP idle interval advertised by the device (SII, milliseconds)
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub sii_ms: Option<u32>,
12    /// MRP active interval advertised by the device (SAI, milliseconds)
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub sai_ms: Option<u32>,
15    /// MRP active threshold advertised by the device (SAT, milliseconds)
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub sat_ms: Option<u32>,
18}
19
20impl Device {
21    /// MRP timing parameters from the stored SII/SAI/SAT values,
22    /// with spec defaults for missing ones.
23    pub fn mrp_params(&self) -> crate::mrp::MrpParameters {
24        crate::mrp::MrpParameters::from_txt_ms(self.sii_ms, self.sai_ms, self.sat_ms)
25    }
26}
27
28pub(crate) struct DeviceRegistry {
29    path: String,
30    devices: Vec<Device>,
31}
32
33impl DeviceRegistry {
34    pub fn load(path: &str) -> Result<Self> {
35        let devices = match std::fs::read_to_string(path) {
36            Ok(data) => serde_json::from_str(&data).context("parsing devices.json")?,
37            Err(_) => Vec::new(),
38        };
39        Ok(Self {
40            path: path.to_owned(),
41            devices,
42        })
43    }
44
45    fn save(&self) -> Result<()> {
46        let data = serde_json::to_string_pretty(&self.devices)?;
47        std::fs::write(&self.path, data).context(format!("writing devices to {}", self.path))
48    }
49
50    pub fn add(&mut self, device: Device) -> Result<()> {
51        // Check for duplicate name on a different node_id
52        if let Some(existing) = self.devices.iter().find(|d| d.name == device.name) {
53            if existing.node_id != device.node_id {
54                anyhow::bail!("device name '{}' already in use by node {}", device.name, existing.node_id);
55            }
56        }
57        // Replace if same node_id, otherwise push
58        if let Some(pos) = self.devices.iter().position(|d| d.node_id == device.node_id) {
59            self.devices[pos] = device;
60        } else {
61            self.devices.push(device);
62        }
63        self.save()
64    }
65
66    pub fn remove(&mut self, node_id: u64) -> Result<()> {
67        self.devices.retain(|d| d.node_id != node_id);
68        self.save()
69    }
70
71    pub fn get(&self, node_id: u64) -> Option<&Device> {
72        self.devices.iter().find(|d| d.node_id == node_id)
73    }
74
75    pub fn get_by_name(&self, name: &str) -> Option<&Device> {
76        self.devices.iter().find(|d| d.name == name)
77    }
78
79    pub fn list(&self) -> &[Device] {
80        &self.devices
81    }
82
83    pub fn update_address(&mut self, node_id: u64, address: &str) -> Result<()> {
84        let dev = self.devices.iter_mut().find(|d| d.node_id == node_id)
85            .context(format!("device {} not found", node_id))?;
86        dev.address = address.to_owned();
87        self.save()
88    }
89
90    pub fn update_mrp(
91        &mut self,
92        node_id: u64,
93        sii_ms: Option<u32>,
94        sai_ms: Option<u32>,
95        sat_ms: Option<u32>,
96    ) -> Result<()> {
97        let dev = self.devices.iter_mut().find(|d| d.node_id == node_id)
98            .context(format!("device {} not found", node_id))?;
99        dev.sii_ms = sii_ms;
100        dev.sai_ms = sai_ms;
101        dev.sat_ms = sat_ms;
102        self.save()
103    }
104
105    pub fn rename(&mut self, node_id: u64, name: &str) -> Result<()> {
106        // Check for duplicate name
107        if let Some(existing) = self.devices.iter().find(|d| d.name == name) {
108            if existing.node_id != node_id {
109                anyhow::bail!("device name '{}' already in use by node {}", name, existing.node_id);
110            }
111        }
112        let dev = self.devices.iter_mut().find(|d| d.node_id == node_id)
113            .context(format!("device {} not found", node_id))?;
114        dev.name = name.to_owned();
115        self.save()
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn test_path(name: &str) -> String {
124        let dir = std::env::temp_dir().join(format!("matc_test_{}", name));
125        let _ = std::fs::remove_dir_all(&dir);
126        std::fs::create_dir_all(&dir).unwrap();
127        println!("Using test directory: {:?}", dir);
128        dir.join("devices.json").to_str().unwrap().to_owned()
129    }
130
131    #[test]
132    fn registry_round_trip() {
133        let path = test_path("reg_rt");
134
135        let mut reg = DeviceRegistry::load(&path).unwrap();
136        assert!(reg.list().is_empty());
137
138        reg.add(Device { node_id: 1, address: "1.2.3.4:5540".into(), name: "light".into(), ..Default::default() }).unwrap();
139        reg.add(Device { node_id: 2, address: "1.2.3.5:5540".into(), name: "switch".into(), ..Default::default() }).unwrap();
140        assert_eq!(reg.list().len(), 2);
141
142        // reload from disk
143        let reg2 = DeviceRegistry::load(&path).unwrap();
144        assert_eq!(reg2.list().len(), 2);
145        assert_eq!(reg2.get(1).unwrap().name, "light");
146        assert_eq!(reg2.get_by_name("switch").unwrap().node_id, 2);
147    }
148
149    #[test]
150    fn registry_replace_by_node_id() {
151        let path = test_path("reg_replace");
152
153        let mut reg = DeviceRegistry::load(&path).unwrap();
154        reg.add(Device { node_id: 1, address: "1.2.3.4:5540".into(), name: "light".into(), ..Default::default() }).unwrap();
155        reg.add(Device { node_id: 1, address: "1.2.3.5:5540".into(), name: "light2".into(), ..Default::default() }).unwrap();
156        assert_eq!(reg.list().len(), 1);
157        assert_eq!(reg.get(1).unwrap().name, "light2");
158    }
159
160    #[test]
161    fn registry_unique_names() {
162        let path = test_path("reg_unique");
163
164        let mut reg = DeviceRegistry::load(&path).unwrap();
165        reg.add(Device { node_id: 1, address: "1.2.3.4:5540".into(), name: "light".into(), ..Default::default() }).unwrap();
166        let err = reg.add(Device { node_id: 2, address: "1.2.3.5:5540".into(), name: "light".into(), ..Default::default() });
167        assert!(err.is_err());
168    }
169
170    #[test]
171    fn registry_rename_and_update_address() {
172        let path = test_path("reg_rename");
173
174        let mut reg = DeviceRegistry::load(&path).unwrap();
175        reg.add(Device { node_id: 1, address: "1.2.3.4:5540".into(), name: "light".into(), ..Default::default() }).unwrap();
176        reg.rename(1, "kitchen light").unwrap();
177        assert_eq!(reg.get(1).unwrap().name, "kitchen light");
178
179        reg.update_address(1, "10.0.0.1:5540").unwrap();
180        assert_eq!(reg.get(1).unwrap().address, "10.0.0.1:5540");
181    }
182
183    #[test]
184    fn registry_remove() {
185        let path = test_path("reg_remove");
186
187        let mut reg = DeviceRegistry::load(&path).unwrap();
188        reg.add(Device { node_id: 1, address: "1.2.3.4:5540".into(), name: "light".into(), ..Default::default() }).unwrap();
189        reg.remove(1).unwrap();
190        assert!(reg.list().is_empty());
191    }
192}