matc/devman/
device.rs

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