teleinfo_parser/
frame.rs

1/*
2 * teleinfo-parser
3 * Copyright (c) 2018, 2021 Nicolas PENINGUY.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use ::std::io::{self, Read};
20
21const STX: char = '\u{0002}';
22const ETX: char = '\u{0003}';
23const EOT: char = '\u{0004}';
24
25const LF: char = '\u{000A}';
26const CR: char = '\u{000D}';
27
28const SP: char = '\u{0020}';
29// const HT: char = '\u{0009}';
30
31const SEPARATOR: char = SP;
32
33/// The subscribed tariff option
34#[derive(Debug)]
35pub enum OptionTarifaire {
36    Base,
37    HC,
38    EJP,
39    UNKNOWN(String)
40}
41
42/// The current period for the frame. Valid values depends on the subscribed plan.
43#[derive(Debug)]
44pub enum PeriodeTarifaire {
45    /// Toutes Heures
46    TH,
47    /// Heures Creuses
48    HC,
49    /// Heures Pleines
50    HP,
51    /// Autre / non géré
52    UNKNOWN(String)
53}
54
55/// The different tags that can compose a frame.
56#[derive(Debug)]
57pub enum Tag {
58    /// Adresse du compteur
59    ADCO(String),
60    /// Option tarifaire choisie
61    OPTARIF(OptionTarifaire),
62    /// Intensité souscrite
63    ISOUSC(i32),
64    /// Index option Base
65    BASE(i32),
66    /// Index option Heures Creuses
67    /// Heures Creuses
68    HCHC(i32),
69    /// Heures Pleines
70    HCHP(i32),
71    /// Période tarifaire en cours
72    PTEC(PeriodeTarifaire),
73    /// Intensité Instantanée
74    IINST(i32),
75    /// Avertissement de Dépassement de Puissance Souscrite
76    ADPS(i32),
77    /// Intensité maximale appelée
78    IMAX(i32),
79    /// Puissance apparente
80    PAPP(i32),
81    /// Horaire Heures Pleines Heures Creuses
82    HHPHC(char),
83    /// Mot d'état du compteur
84    MOTDETAT(String),
85    /// Groupe d'information inconnu ou non géré
86    UNKNOWN(String, String)
87}
88
89/// A parsed and valid frame received from the TeleInfo line.
90#[derive(Debug)]
91pub struct Frame {
92    pub tags: Vec<Tag>
93}
94
95impl Frame {
96    fn new() -> Frame {
97        Frame {
98            tags: Vec::new()
99        }
100    }
101
102    pub fn next_frame<T: Read>(mut input: &mut T) -> Result<Frame, TeleinfoError> {
103
104        skip_to(&mut input, STX)?;
105
106        return read_frame(&mut input);
107    }
108}
109
110/// Possible fatal or non-fatal errors when reading frames from the serial port.
111#[derive(Debug)]
112pub enum TeleinfoError {
113    /// Channel is closed.
114    EndOfFile,
115    /// A input/output error was encountered.
116    IoError(io::Error),
117    /// The transmission was interupted, for example because of external reading of the meter
118    /// current value. Reading a new frame can be tried.
119    EndOfTransmission,
120    /// One tag had unexpected form, maybe because of transmission error. Try reading a new frame.
121    FrameError(String),
122    /// One tag had wrong checksum, maybe because of transmission error. Try reading a new frame.
123    ChecksumError
124}
125
126impl From<io::Error> for TeleinfoError {
127
128    fn from(err: io::Error) -> TeleinfoError {
129        TeleinfoError::IoError(err)
130    }
131}
132
133fn read_frame<T: Read>(mut input: &mut T) -> Result<Frame, TeleinfoError> {
134
135    let mut frame = Frame::new();
136
137    loop {
138        let c = read_char(&mut input)?;
139
140        if c == ETX {
141            return Ok(frame);
142        }
143
144        if c != LF {
145            return Err(TeleinfoError::FrameError(format!("Expected LF but found {}", c)));
146        }
147
148        let lbl = read_to_sep(&mut input)?;
149        let val = read_to_sep(&mut input)?;
150        let c = read_char(&mut input)?;
151
152        if c != checksum(&lbl, &val) {
153            return Err(TeleinfoError::ChecksumError);
154        }
155
156        let tag = parse_tag(&lbl, &val)?;
157
158        frame.tags.push(tag);
159
160        expect_char(&mut input, CR)?;
161    }
162}
163
164fn checksum(lbl: &str, val: &str) -> char {
165
166    let mut sum = 0u8;
167
168    for c in lbl.chars() {
169        sum = sum.wrapping_add(c as u8);
170    }
171
172    sum = sum.wrapping_add(SEPARATOR as u8);
173
174    for c in val.chars() {
175        sum = sum.wrapping_add(c as u8);
176    }
177
178    ((sum & 0x3F) + 0x20) as char
179}
180
181fn parse_tag(lbl: &str, val: &str) -> Result<Tag, TeleinfoError> {
182
183    let tag = match lbl {
184
185        "ADCO" => Tag::ADCO(val.to_string()),
186
187        "OPTARIF" => {
188            Tag::OPTARIF(match val {
189                "Base" => OptionTarifaire::Base,
190                "HC.." => OptionTarifaire::HC,
191                "EJP." => OptionTarifaire::EJP,
192                _ => OptionTarifaire::UNKNOWN(val.to_string())
193            })
194        },
195
196        "ISOUSC" => {
197            let p = val.parse::<i32>()
198                .map_err(|_| TeleinfoError::FrameError(format!("Number parse error on {}", val)))?;
199            Tag::ISOUSC(p)
200        },
201
202        "BASE" => {
203            let v = val.parse::<i32>()
204                .map_err(|_| TeleinfoError::FrameError(format!("Number parse error on {}", val)))?;
205            Tag::BASE(v)
206        },
207
208        "HCHC" => {
209            let v = val.parse::<i32>()
210                .map_err(|_| TeleinfoError::FrameError(format!("Number parse error on {}", val)))?;
211            Tag::HCHC(v)
212        },
213
214        "HCHP" => {
215            let v = val.parse::<i32>()
216                .map_err(|_| TeleinfoError::FrameError(format!("Number parse error on {}", val)))?;
217            Tag::HCHP(v)
218        },
219
220        "PTEC" => {
221            Tag::PTEC(match val {
222                "TH.." => PeriodeTarifaire::TH,
223                "HC.." => PeriodeTarifaire::HC,
224                "HP.." => PeriodeTarifaire::HP,
225                _ => PeriodeTarifaire::UNKNOWN(val.to_string())
226            })
227        },
228
229        "IINST" => {
230            let v = val.parse::<i32>()
231                .map_err(|_| TeleinfoError::FrameError(format!("Number parse error on {}", val)))?;
232            Tag::IINST(v)
233        },
234
235        "IMAX" => {
236            let v = val.parse::<i32>()
237                .map_err(|_| TeleinfoError::FrameError(format!("Number parse error on {}", val)))?;
238            Tag::IMAX(v)
239        },
240
241        "ADPS" => {
242            let v = val.parse::<i32>()
243                .map_err(|_| TeleinfoError::FrameError(format!("Number parse error on {}", val)))?;
244            Tag::ADPS(v)
245        },
246
247        "PAPP" => {
248            let v = val.parse::<i32>()
249                .map_err(|_| TeleinfoError::FrameError(format!("Number parse error on {}", val)))?;
250            Tag::PAPP(v)
251        },
252
253        "HHPHC" => {
254            let c = match val.chars().next() {
255                Some(c) => c,
256                None => return Err(TeleinfoError::FrameError("HHPHC should be one char long".to_string()))
257            };
258            Tag::HHPHC(c)
259        },
260
261        "MOTDETAT" => Tag::MOTDETAT(val.to_string()),
262
263        _ => Tag::UNKNOWN(lbl.to_string(), val.to_string())
264    };
265
266    Ok(tag)
267}
268
269fn skip_to<T: Read>(mut input: &mut T, stop_char: char) -> Result<(), TeleinfoError> {
270
271    loop {
272        let c = read_char(&mut input)?;
273
274        if c == stop_char {
275            break;
276        }
277    }
278
279    Ok(())
280}
281
282fn read_to_sep<T: Read>(mut input: &mut T) -> Result<String, TeleinfoError> {
283
284    let mut result: String = String::new();
285
286    loop {
287        let c = read_char(&mut input)?;
288
289        if c ==  SEPARATOR {
290            break;
291        }
292
293        result.push(c);
294    }
295
296    Ok(result)
297}
298
299fn expect_char<T: Read>(mut input: &mut T, expected: char) -> Result<(), TeleinfoError> {
300
301    let c = read_char(&mut input)?;
302
303    if c != expected {
304        return Err(TeleinfoError::FrameError(format!("Expected {} but found {}", expected, c)));
305    }
306
307    Ok(())
308}
309
310fn read_char<T: Read>(input: &mut T) -> Result<char, TeleinfoError> {
311
312    let mut buf = [0u8; 1];
313    let count = input.read(&mut buf)?;
314
315    if count == 0 {
316        return Err(TeleinfoError::EndOfFile);
317    }
318
319    let c = buf[0] as char;
320
321    if c == EOT {
322        return Err(TeleinfoError::EndOfTransmission);
323    }
324
325    Ok(c)
326}
327
328#[cfg(test)]
329mod tests {
330
331    use super::*;
332    use std::path::PathBuf;
333    use std::fs::File;
334
335    #[test]
336    fn test_read_char() {
337
338        let mut v = &[b'x', 4] as &[u8];
339
340        let c = read_char(&mut v);
341        assert_matches!(c, Ok('x'));
342
343        let c = read_char(&mut v);
344        assert_matches!(c, Err(TeleinfoError::EndOfTransmission));
345
346        let c = read_char(&mut v);
347        assert_matches!(c, Err(TeleinfoError::EndOfFile));
348    }
349
350    #[test]
351    fn test_expect_char() {
352
353        let mut v = &[b'a', b'b'] as &[u8];
354
355        let r = expect_char(&mut v, 'a');
356        assert_matches!(r, Ok(()));
357
358        let r = expect_char(&mut v, 'a');
359        assert_matches!(r, Err(TeleinfoError::FrameError(_)));
360    }
361
362    #[test]
363    fn test_read_to_sep() {
364        let mut v = &[b'a', b'b', b'c', SEPARATOR as u8, b'd'] as &[u8];
365
366        let r = read_to_sep(&mut v);
367        assert_eq!(r.unwrap(), "abc");
368
369        let r = read_to_sep(&mut v);
370        assert_matches!(r, Err(TeleinfoError::EndOfFile));
371    }
372
373    #[test]
374    fn test_skip_to() {
375        let mut v = &[b'a', b'b', b'c'] as &[u8];
376
377        let r = skip_to(&mut v, 'b');
378        assert_matches!(r, Ok(()));
379
380        let c = read_char(&mut v);
381        assert_matches!(c, Ok('c'));
382    }
383
384    #[test]
385    fn test_parse_tag() {
386
387        let t = parse_tag("BASE", "99").unwrap();
388        assert_matches!(t, Tag::BASE(99));
389    }
390
391    #[test]
392    fn test_checksum() {
393
394        let s = checksum("PAPP", "00380");
395        assert_eq!(s, ',');
396    }
397
398    #[test]
399    fn test_read_frame() {
400
401        let mut file = get_test_file_reader("badchecksum.txt").unwrap();
402        let frame = Frame::next_frame(&mut file);
403
404        assert_matches!(frame, Err(TeleinfoError::ChecksumError));
405
406        let mut file = get_test_file_reader("frames.txt").unwrap();
407        let frame = Frame::next_frame(&mut file).unwrap();
408
409        assert_eq!(frame.tags.len(), 8);
410
411        for tag in frame.tags {
412            match tag {
413                Tag::PAPP(v) => {
414                    assert_eq!(v, 370);
415                },
416                _ => ()
417            };
418        }
419
420
421    }
422
423    fn get_test_file_reader(file_name: &str) -> std::io::Result<File> {
424
425        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
426        d.push("resources/test");
427        d.push(file_name);
428
429        return File::open(d);
430    }
431}