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 #[serde(default, skip_serializing_if = "Option::is_none")]
11 pub sii_ms: Option<u32>,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub sai_ms: Option<u32>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub sat_ms: Option<u32>,
18}
19
20impl Device {
21 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 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 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 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 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}