eq3_max_cube_rs/
messages.rs

1use anyhow::{anyhow, Result};
2use base64::{engine::general_purpose, Engine as _};
3use serde::{Deserialize, Serialize};
4use std::collections::VecDeque;
5
6/// represents a heating system device, e.g. thermostat, shutter contact...
7/// Only thermostat is supported by now.
8#[derive(Debug, Default, Deserialize, Serialize)]
9pub enum Device {
10    #[default]
11    NotSupported,
12    HeaterThermostat(HeaterThermostat),
13}
14
15/// represents a thermostat of heater
16#[derive(Debug, Default, Serialize, Deserialize)]
17pub struct HeaterThermostat {
18    /// RF address of the thermostat
19    pub rf_address: u32,
20    /// Serial number of the thermostat
21    pub serial: String,
22    /// Name of the thermostat
23    pub name: String,
24    /// room id (group id), which the thermostat belongs to
25    pub room_id: u8,
26    /// current valve position, in percent
27    pub valve_position: u8,
28    /// current temperature set point (which is displayed on thermostat)
29    pub temperature_set: f64,
30    /// current temperature, which is measured by the thermostat
31    /// this value is not always available
32    pub temperature_measured: f64,
33    /// if the battery level is low
34    pub battery_low: bool,
35    /// if the thermostat in error state
36    pub error: bool,
37    /// if the values from thermostat are valid
38    pub valid: bool,
39}
40
41/// represents a room/group, which is set up by MAX! software
42#[derive(Debug, Default, Serialize, Deserialize)]
43pub struct Room {
44    /// room id (group id)
45    pub room_id: u8,
46    /// room name
47    pub name: String,
48    /// RF address of the room
49    pub rf_address: u32,
50}
51
52/// List of devices
53pub type Devices = Vec<Device>;
54/// List of rooms
55pub type Rooms = Vec<Room>;
56
57/// the function shall not be called directly
58pub(super) fn from_message_m(recv: &str) -> Result<(Rooms, Devices)> {
59    // assertions
60    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            // decode all rooms
78            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            // decode all devices
101            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    // assertions
140    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."))?; // unknown field
165        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        // get mutable reference from devices
171        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/// Device mode, can be Manual, Auto (other mode, such as Vaccation etc is not supported by now)
200#[derive(Debug, Default, Copy, Clone)]
201pub enum DeviceMode {
202    /// temperature set point is manually set, won't change automatically
203    Manual = 1,
204    /// temperature set point will be changed automatically according the time scheduling
205    #[default]
206    Auto = 0,
207}
208
209/// DeviceConfig is used to change the device configuration, like temperature set point
210#[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    /// returns a instant of DeviceConfig with default values
220    pub fn new() -> Self {
221        Self {
222            ..Default::default()
223        }
224    }
225
226    /// set the mode for the set command
227    pub fn set_mode(mut self, mode: DeviceMode) -> Self {
228        self.mode = mode;
229        self
230    }
231
232    /// set the temperature set point for the set command
233    pub fn set_temperature(mut self, temperature: f64) -> Self {
234        self.temperature = temperature;
235        self
236    }
237
238    /// set the RF address for the set command
239    pub fn set_address(mut self, rf_address: u32) -> Self {
240        self.rf_address = rf_address;
241        self
242    }
243
244    /// set the room id for the set command
245    /// If the roomt id is 0, the configuration will be applied on all devices
246    pub fn set_room_id(mut self, room_id: u8) -> Self {
247        self.room_id = room_id;
248        self
249    }
250
251    /// build the command payload
252    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        // Test data from: https://github.com/Bouni/max-cube-protocol/blob/master/M-Message.md
274
275        let data =  "M:00,01,VgIEAQNCYWQK7WkCBEJ1cm8K8wADCldvaG56aW1tZXIK8wwEDFNjaGxhZnppbW1lcgr1QAUCCu1pS0VRMDM3ODA0MAZIVCBCYWQBAgrzAEtFUTAzNzk1NDQHSFQgQnVybwICCvMMS0VRMDM3OTU1NhlIVCBXb2huemltbWVyIEJhbGtvbnNlaXRlAwIK83lLRVEwMzc5NjY1GkhUIFdvaG56aW1tZXIgRmVuc3RlcnNlaXRlAwIK9UBLRVEwMzgwMTIwD0hUIFNjaGxhZnppbW1lcgQB";
276
277        let (rooms, _) = from_message_m(&data).unwrap();
278
279        // println!("{:?}, {:?}", rooms, devices);
280        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        // println!("{:?}, {:?}", rooms, devices);
297        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        // println!("{:?}", devices);
318
319        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}