1use anyhow::{anyhow, Result};
2use base64::{engine::general_purpose, Engine as _};
3use serde::{Deserialize, Serialize};
4use std::collections::VecDeque;
5
6#[derive(Debug, Default, Deserialize, Serialize)]
9pub enum Device {
10 #[default]
11 NotSupported,
12 HeaterThermostat(HeaterThermostat),
13}
14
15#[derive(Debug, Default, Serialize, Deserialize)]
17pub struct HeaterThermostat {
18 pub rf_address: u32,
20 pub serial: String,
22 pub name: String,
24 pub room_id: u8,
26 pub valve_position: u8,
28 pub temperature_set: f64,
30 pub temperature_measured: f64,
33 pub battery_low: bool,
35 pub error: bool,
37 pub valid: bool,
39}
40
41#[derive(Debug, Default, Serialize, Deserialize)]
43pub struct Room {
44 pub room_id: u8,
46 pub name: String,
48 pub rf_address: u32,
50}
51
52pub type Devices = Vec<Device>;
54pub type Rooms = Vec<Room>;
56
57pub(super) fn from_message_m(recv: &str) -> Result<(Rooms, Devices)> {
59 if !recv.starts_with("M:") {
61 return Err(anyhow!(
62 "Message `M` expected, but `{}` received.",
63 recv.chars().next().unwrap()
64 ));
65 }
66
67 for (index, part) in recv.split(",").into_iter().enumerate() {
68 if index == 0 && part != "M:00" {
69 return Err(anyhow!("Chunked M-Message not supported."));
70 } else if index == 1 && part != "01" {
71 return Err(anyhow!("Chunked M-Message not supported."));
72 } else if index == 2 {
73 let mut b = VecDeque::from(general_purpose::STANDARD.decode(part)?);
74 b.pop_front().ok_or(anyhow!("Unexpected data length."))?;
75 b.pop_front().ok_or(anyhow!("Unexpected data length."))?;
76
77 let room_count = b.pop_front().ok_or(anyhow!("Unexpected data length."))? as usize;
79 let mut rooms = Rooms::new();
80 for _ in vec![0; room_count] {
81 let room_id = b.pop_front().ok_or(anyhow!("Unexpected data length."))?;
82 let length = b.pop_front().ok_or(anyhow!("Unexpected data length."))? as usize;
83 let name =
84 String::from_utf8_lossy(&b.drain(..length).into_iter().collect::<Vec<_>>())
85 .to_string();
86 let rf_address = u32::from_be_bytes([
87 0,
88 b.pop_front().ok_or(anyhow!("Unexpected data length."))?,
89 b.pop_front().ok_or(anyhow!("Unexpected data length."))?,
90 b.pop_front().ok_or(anyhow!("Unexpected data length."))?,
91 ]);
92 let room = Room {
93 room_id,
94 name,
95 rf_address,
96 };
97 rooms.push(room);
98 }
99
100 let dev_count = b.pop_front().ok_or(anyhow!("Unexpected data length."))? as usize;
102 let mut devices = Devices::new();
103 for _ in vec![0; dev_count] {
104 let dev_type = b.pop_front().ok_or(anyhow!("Unexpected data length."))?;
105 let rf_address = u32::from_be_bytes([
106 0,
107 b.pop_front().ok_or(anyhow!("Unexpected data length."))?,
108 b.pop_front().ok_or(anyhow!("Unexpected data length."))?,
109 b.pop_front().ok_or(anyhow!("Unexpected data length."))?,
110 ]);
111 let serial =
112 String::from_utf8_lossy(&b.drain(..10).into_iter().collect::<Vec<_>>())
113 .to_string();
114 let length = b.pop_front().ok_or(anyhow!("Unexpected data length."))? as usize;
115 let name =
116 String::from_utf8_lossy(&b.drain(..length).into_iter().collect::<Vec<_>>())
117 .to_string();
118 let room_id = b.pop_front().ok_or(anyhow!("Unexpected data length."))?;
119 let device = match dev_type {
120 1 => Device::HeaterThermostat(HeaterThermostat {
121 rf_address,
122 serial,
123 room_id,
124 name,
125 ..Default::default()
126 }),
127 _ => Device::NotSupported,
128 };
129 devices.push(device);
130 }
131 return Ok((rooms, devices));
132 }
133 }
134
135 Err(anyhow!("Message M not well-formatted."))
136}
137
138pub(super) fn from_message_l(recv: &str, devices: &mut Devices) -> Result<()> {
139 if !recv.starts_with("L:") {
141 return Err(anyhow!(
142 "Message `L` expected, but `{}` received.",
143 recv.chars().next().unwrap()
144 ));
145 }
146
147 let mut b = VecDeque::from(
148 general_purpose::STANDARD.decode(
149 recv.split(":")
150 .last()
151 .ok_or(anyhow!("Message L not well-formatted."))?,
152 )?,
153 );
154
155 while b.len() > 0 {
156 let length = b.pop_front().ok_or(anyhow!("Unexpected data length."))? as usize;
157 let mut sub = b.drain(..length).into_iter().collect::<VecDeque<_>>();
158 let rf_address = u32::from_be_bytes([
159 0,
160 sub.pop_front().ok_or(anyhow!("Unexpected data length."))?,
161 sub.pop_front().ok_or(anyhow!("Unexpected data length."))?,
162 sub.pop_front().ok_or(anyhow!("Unexpected data length."))?,
163 ]);
164 sub.pop_front().ok_or(anyhow!("Unexpected data length."))?; let flags = u16::from_be_bytes([
166 sub.pop_front().ok_or(anyhow!("Unexpected data length."))?,
167 sub.pop_front().ok_or(anyhow!("Unexpected data length."))?,
168 ]);
169
170 devices.iter_mut().for_each(|e| {
172 if let Device::HeaterThermostat(ts) = e {
173 if ts.rf_address == rf_address {
174 ts.battery_low = (flags & 0x80) > 0;
175 ts.error = (flags & 0x800) > 0;
176 ts.valid = (flags & 0x1000) > 0;
177
178 if length > 6 {
179 ts.valve_position = sub.pop_front().unwrap();
180 ts.temperature_set = sub.pop_front().unwrap() as f64 / 2.0;
181 ts.temperature_measured = u16::from_be_bytes([
182 sub.pop_front()
183 .ok_or(anyhow!("Unexpected data length."))
184 .unwrap(),
185 sub.pop_front()
186 .ok_or(anyhow!("Unexpected data length."))
187 .unwrap(),
188 ]) as f64
189 / 10.0;
190 }
191 }
192 }
193 });
194 }
195
196 Ok(())
197}
198
199#[derive(Debug, Default, Copy, Clone)]
201pub enum DeviceMode {
202 Manual = 1,
204 #[default]
206 Auto = 0,
207}
208
209#[derive(Default, Debug)]
211pub struct DeviceConfig {
212 mode: DeviceMode,
213 temperature: f64,
214 rf_address: u32,
215 room_id: u8,
216}
217
218impl DeviceConfig {
219 pub fn new() -> Self {
221 Self {
222 ..Default::default()
223 }
224 }
225
226 pub fn set_mode(mut self, mode: DeviceMode) -> Self {
228 self.mode = mode;
229 self
230 }
231
232 pub fn set_temperature(mut self, temperature: f64) -> Self {
234 self.temperature = temperature;
235 self
236 }
237
238 pub fn set_address(mut self, rf_address: u32) -> Self {
240 self.rf_address = rf_address;
241 self
242 }
243
244 pub fn set_room_id(mut self, room_id: u8) -> Self {
247 self.room_id = room_id;
248 self
249 }
250
251 pub fn build(&self) -> String {
253 let mut data = vec![0x00u8, 0x04, 0x40, 0x00, 0x00, 0x00];
254 data.push((self.rf_address >> 16) as u8);
255 data.push((self.rf_address >> 8) as u8);
256 data.push(self.rf_address as u8);
257 data.push(self.room_id);
258
259 data.push(((self.mode as u8) << 6) | (((self.temperature * 2.0) as u8) & 0x3f));
260 let mut cmd = "s:".to_string();
261 cmd.push_str(&general_purpose::STANDARD.encode(data));
262 cmd.push_str("\r\n");
263 cmd
264 }
265}
266
267#[cfg(test)]
268mod test {
269 use super::*;
270
271 #[test]
272 fn test_message_m_0() {
273 let data = "M:00,01,VgIEAQNCYWQK7WkCBEJ1cm8K8wADCldvaG56aW1tZXIK8wwEDFNjaGxhZnppbW1lcgr1QAUCCu1pS0VRMDM3ODA0MAZIVCBCYWQBAgrzAEtFUTAzNzk1NDQHSFQgQnVybwICCvMMS0VRMDM3OTU1NhlIVCBXb2huemltbWVyIEJhbGtvbnNlaXRlAwIK83lLRVEwMzc5NjY1GkhUIFdvaG56aW1tZXIgRmVuc3RlcnNlaXRlAwIK9UBLRVEwMzgwMTIwD0hUIFNjaGxhZnppbW1lcgQB";
276
277 let (rooms, _) = from_message_m(&data).unwrap();
278
279 assert_eq!(rooms.len(), 4);
281 assert_eq!(rooms[0].name, "Bad");
282 assert_eq!(rooms[0].rf_address, 716137);
283 assert_eq!(rooms[3].name, "Schlafzimmer");
284 assert_eq!(rooms[3].rf_address, 718144);
285 }
286
287 fn extract_message_m_1() -> (Rooms, Devices) {
288 let data = "M:00,01,VgIFAQdCZWRyb29tGuXTAgtMaXZpbmcgcm9vbRrqAQMHS2l0Y2hlbhrnLgQGT2ZmaWNlGun/BQhCYXRocm9vbRrlGAUBGuXTT0VRMjEyMTY0NAdCZWRyb29tAQEa6gFPRVEyMTIyMzU2C0xpdmluZyByb29tAgEa5y5PRVEyMTIxNDc2B0tpdGNoZW4DARrp/09FUTIxMjIzNTMGT2ZmaWNlBAEa5RhPRVEyMTIxNzc0CEJhdGhyb29tBQE=";
289 from_message_m(&data).unwrap()
290 }
291
292 #[test]
293 fn test_message_m_1() {
294 let (rooms, devices) = extract_message_m_1();
295
296 assert_eq!(rooms.len(), 5);
298 assert_eq!(devices.len(), 5);
299 match devices.get(4).unwrap() {
300 Device::HeaterThermostat(st) => {
301 assert_eq!(st.serial, "OEQ2121774");
302 assert_eq!(st.rf_address, 1762584);
303 assert_eq!(st.name, "Bathroom");
304 }
305 _ => {
306 panic!("Wrong device type!");
307 }
308 }
309 }
310
311 #[test]
312 fn test_message_l_1() {
313 let data =
314 "L:CxrnLgkSGQAmAM0ACxrlGAkSGQAKAAAACxrqAQkSGQApAOMACxrp/wkSGRYnAMoACxrl0wkSmQAoAOAA";
315 let (_, mut devices) = extract_message_m_1();
316 from_message_l(data, &mut devices).unwrap();
317 match devices.get(2).unwrap() {
320 Device::HeaterThermostat(ts) => {
321 assert_eq!(ts.name, "Kitchen");
322 assert_eq!(ts.valve_position, 0);
323 assert_eq!(ts.temperature_set, 19.0);
324 }
325 _ => panic!("Wrong device type!"),
326 }
327 }
328
329 #[test]
330 fn test_set_temperature() {
331 let (_, d) = extract_message_m_1();
332 println!("{:?}", d);
333
334 let s = DeviceConfig::new()
335 .set_address(1762771)
336 .set_room_id(1)
337 .set_mode(DeviceMode::Manual)
338 .set_temperature(23.0)
339 .build();
340 assert_eq!(s, "s:AARAAAAAGuXTAW4=\r\n");
341 }
342}