dsmr5/
lib.rs

1//! A no-std Rust implementation of the [DSMR5 standard](https://www.netbeheernederland.nl/_upload/Files/Slimme_meter_15_a727fce1f1.pdf) (Dutch Smart Meter Requirements).
2
3#![no_std]
4
5pub mod state;
6pub mod types;
7
8mod obis;
9mod reader;
10
11pub use crate::obis::*;
12pub use crate::reader::*;
13
14#[derive(Debug)]
15pub enum Error {
16    InvalidFormat,
17    InvalidChecksum,
18    UnknownObis,
19}
20
21pub type Result<T> = core::result::Result<T, Error>;
22
23/// A data readout message from the metering system as per section 6.2.
24pub struct Readout {
25    pub buffer: [u8; 2048], // Maximum size of a Readout
26}
27
28impl Readout {
29    /// Parse the readout to an actual telegram message.
30    ///
31    /// Checks the integrity of the telegram by the CRC16 checksum included.
32    /// Parses the prefix and identification, and will allow the parsing of the COSEM objects.
33    pub fn to_telegram(&'_ self) -> Result<Telegram<'_>> {
34        let buffer = core::str::from_utf8(&self.buffer).map_err(|_| Error::InvalidFormat)?;
35
36        if buffer.len() < 16 {
37            return Err(Error::InvalidFormat);
38        }
39
40        let data_end = buffer.find('!').ok_or(Error::InvalidFormat)?;
41        let (buffer, postfix) = buffer.split_at(data_end + 1);
42
43        let given_checksum = u16::from_str_radix(postfix.get(..4).ok_or(Error::InvalidFormat)?, 16)
44            .map_err(|_| Error::InvalidFormat)?;
45        let real_checksum = crc16::State::<crc16::ARC>::calculate(buffer.as_bytes());
46
47        if given_checksum != real_checksum {
48            return Err(Error::InvalidChecksum);
49        }
50
51        let data_start = buffer.find("\r\n\r\n").ok_or(Error::InvalidFormat)?;
52        let (header, data) = buffer.split_at(data_start);
53
54        let prefix = header.get(1..4).ok_or(Error::InvalidFormat)?;
55        let identification = header.get(5..).ok_or(Error::InvalidFormat)?;
56
57        Ok(Telegram {
58            checksum: given_checksum,
59            prefix,
60            identification,
61            object_buffer: data.get(4..data.len() - 3).ok_or(Error::InvalidFormat)?,
62        })
63    }
64}
65
66/// A P1 telegram from the metering system as per section 6.12.
67pub struct Telegram<'a> {
68    /// The verified CRC16 checksum of the telegram data.
69    pub checksum: u16,
70
71    /// The first 3 characters of the datagram.
72    pub prefix: &'a str,
73
74    /// Metering system identification.
75    pub identification: &'a str,
76
77    /// String buffer representing the COSEM objects.
78    object_buffer: &'a str,
79}
80
81impl<'a> Telegram<'a> {
82    /// Parse the COSEM objects, yielding them as part of an iterator.
83    pub fn objects(&self) -> impl core::iter::Iterator<Item = Result<OBIS<'a>>> {
84        self.object_buffer.lines().map(OBIS::parse)
85    }
86}
87
88#[cfg(test)]
89#[macro_use]
90extern crate std;
91
92#[cfg(test)]
93mod tests {
94    #[test]
95    fn example_isk() {
96        let mut buffer = [0u8; 2048];
97        let file = std::fs::read("test/isk.txt").unwrap();
98
99        let (left, _right) = buffer.split_at_mut(file.len());
100        left.copy_from_slice(file.as_slice());
101
102        let readout = crate::Readout { buffer };
103        let telegram = readout.to_telegram().unwrap();
104
105        assert_eq!(telegram.prefix, "ISK");
106        assert_eq!(telegram.identification, "\\2M550E-1012");
107
108        telegram.objects().for_each(|o| {
109            println!("{:?}", o); // to see use `$ cargo test -- --nocapture`
110            let o = o.unwrap();
111
112            use crate::OBIS::*;
113            use core::convert::From;
114            match o {
115                Version(v) => {
116                    let b: std::vec::Vec<u8> = v.as_octets().map(|b| b.unwrap()).collect();
117                    assert_eq!(b, [80]);
118                }
119                DateTime(tst) => {
120                    assert_eq!(
121                        tst,
122                        crate::types::TST {
123                            year: 19,
124                            month: 3,
125                            day: 20,
126                            hour: 18,
127                            minute: 14,
128                            second: 3,
129                            dst: false
130                        }
131                    );
132                }
133                EquipmentIdentifier(ei) => {
134                    let b: std::vec::Vec<u8> = ei.as_octets().map(|b| b.unwrap()).collect();
135                    assert_eq!(std::str::from_utf8(&b).unwrap(), "E0043007052870318");
136                }
137                MeterReadingTo(crate::Tariff::Tariff1, mr) => {
138                    assert_eq!(f64::from(&mr), 576.239);
139                }
140                MeterReadingTo(crate::Tariff::Tariff2, mr) => {
141                    assert_eq!(f64::from(&mr), 465.162);
142                }
143                TariffIndicator(ti) => {
144                    let b: std::vec::Vec<u8> = ti.as_octets().map(|b| b.unwrap()).collect();
145                    assert_eq!(b, [0, 2]);
146                }
147                PowerFailures(crate::types::UFixedInteger(pf)) => {
148                    assert_eq!(pf, 9);
149                }
150                _ => (), // Do not test the rest.
151            }
152        });
153    }
154
155    #[test]
156    fn example_kaifa() {
157        let mut buffer = [0u8; 2048];
158        let file = std::fs::read("test/kaifa.txt").unwrap();
159
160        let (left, _right) = buffer.split_at_mut(file.len());
161        left.copy_from_slice(file.as_slice());
162
163        let readout = crate::Readout { buffer };
164        let telegram = readout.to_telegram().unwrap();
165
166        assert_eq!(telegram.prefix, "KFM");
167        assert_eq!(telegram.identification, "KAIFA-METER");
168
169        telegram.objects().for_each(|o| {
170            println!("{:?}", o); // to see use `$ cargo test -- --nocapture`
171            let o = o.unwrap();
172
173            use crate::OBIS::*;
174            use core::convert::From;
175            match o {
176                Version(v) => {
177                    let b: std::vec::Vec<u8> = v.as_octets().map(|b| b.unwrap()).collect();
178                    assert_eq!(b, [66]);
179                }
180                DateTime(tst) => {
181                    assert_eq!(
182                        tst,
183                        crate::types::TST {
184                            year: 22,
185                            month: 9,
186                            day: 1,
187                            hour: 15,
188                            minute: 22,
189                            second: 1,
190                            dst: true
191                        }
192                    );
193                }
194                EquipmentIdentifier(ei) => {
195                    let b: std::vec::Vec<u8> = ei.as_octets().map(|b| b.unwrap()).collect();
196                    assert_eq!(std::str::from_utf8(&b).unwrap(), "E0026000024153615");
197                }
198                MeterReadingTo(crate::Tariff::Tariff1, mr) => {
199                    assert_eq!(f64::from(&mr), 6285.065);
200                }
201                MeterReadingTo(crate::Tariff::Tariff2, mr) => {
202                    assert_eq!(f64::from(&mr), 6758.327);
203                }
204                TariffIndicator(ti) => {
205                    let b: std::vec::Vec<u8> = ti.as_octets().map(|b| b.unwrap()).collect();
206                    assert_eq!(b, [0, 2]);
207                }
208                PowerFailures(crate::types::UFixedInteger(pf)) => {
209                    assert_eq!(pf, 3);
210                }
211                _ => (), // Do not test the rest.
212            }
213        });
214    }
215
216    #[test]
217    fn example_mcs() {
218        let mut buffer = [0u8; 2048];
219        let file = std::fs::read("test/mcs.txt").unwrap();
220
221        let (left, _right) = buffer.split_at_mut(file.len());
222        left.copy_from_slice(file.as_slice());
223
224        let readout = crate::Readout { buffer };
225        let telegram = readout.to_telegram().unwrap();
226
227        assert_eq!(telegram.prefix, "MCS");
228        assert_eq!(telegram.identification, "0000000000000");
229
230        telegram.objects().for_each(|o| {
231            //     println!("{:?}", o); // to see use `$ cargo test -- --nocapture`
232            let o = o.unwrap();
233
234            use crate::OBIS::*;
235            match o {
236                Version(v) => {
237                    let b: std::vec::Vec<u8> = v.as_octets().map(|b| b.unwrap()).collect();
238                    assert_eq!(b, [80]);
239                }
240                SlaveDeviceType(_slave, devide_type) => {
241                    assert_eq!(devide_type.is_none(), true);
242                }
243                SlaveMeterReading(_slave, _tst, value) => {
244                    assert_eq!(value.is_none(), true)
245                }
246                _ => (), // Do not test the rest.
247            }
248        });
249    }
250}