use core::str;
use std::io::Read;
use std::str::FromStr;
pub use error::Error;
use log::{trace, warn};
use crate::Tst;
use crate::line_reader::LineReader;
use crate::unit_value::UnitValue;
mod error;
#[derive(Debug, PartialOrd, PartialEq, Clone, Default)]
pub struct Telegram {
pub ident: String,
pub version: Option<String>,
pub electricity_date: Option<Tst>,
pub electricity_equipment_id: Option<String>,
pub electricity_consumed_tariff_1: Option<UnitValue<f64>>,
pub electricity_consumed_tariff_2: Option<UnitValue<f64>>,
pub electricity_generated_tariff_1: Option<UnitValue<f64>>,
pub electricity_generated_tariff_2: Option<UnitValue<f64>>,
pub current_tariff: Option<String>,
pub power: Option<UnitValue<f64>>,
pub return_power: Option<UnitValue<f64>>,
pub power_failure_count: Option<u32>,
pub long_power_failure_count: Option<u32>,
pub power_failure_log: Vec<PowerFailureEntry>,
pub voltage_sag_l1_count: Option<u32>,
pub voltage_sag_l2_count: Option<u32>,
pub voltage_sag_l3_count: Option<u32>,
pub voltage_swell_l1_count: Option<u32>,
pub voltage_swell_l2_count: Option<u32>,
pub voltage_swell_l3_count: Option<u32>,
pub message: Option<String>,
pub voltage_l1: Option<UnitValue<f64>>,
pub voltage_l2: Option<UnitValue<f64>>,
pub voltage_l3: Option<UnitValue<f64>>,
pub current_l1: Option<UnitValue<u16>>,
pub current_l2: Option<UnitValue<u16>>,
pub current_l3: Option<UnitValue<u16>>,
pub power_l1: Option<UnitValue<f64>>,
pub power_l2: Option<UnitValue<f64>>,
pub power_l3: Option<UnitValue<f64>>,
pub return_power_l1: Option<UnitValue<f64>>,
pub return_power_l2: Option<UnitValue<f64>>,
pub return_power_l3: Option<UnitValue<f64>>,
pub device_type: Option<String>,
pub gas_equipment_id: Option<String>,
pub gas_date: Option<Tst>,
pub gas_consumed: Option<UnitValue<f64>>,
}
impl Telegram {
pub fn read_from(src: impl Read) -> Result<Option<Self>, Error> {
enum ParserState {
WaitingForHeader,
ReadingHeader,
ReadingMessage,
}
const CRLF: &[u8] = b"\r\n";
let mut parser_state = ParserState::WaitingForHeader;
let mut out = Self::default();
let mut crc = crc16::State::<crc16::ARC>::new();
let mut expected_crc = None;
#[expect(clippy::unbuffered_bytes)]
let lines = LineReader::new(src.bytes());
for line in lines {
let mut line = line?;
trace!("Got line: {}", String::from_utf8_lossy(&line));
match parser_state {
ParserState::WaitingForHeader => {
if line.starts_with(b"/") {
crc.update(&line);
crc.update(CRLF);
line.remove(0);
out.ident = String::from_utf8(line)?;
parser_state = ParserState::ReadingHeader;
}
}
ParserState::ReadingHeader => {
if line.is_empty() {
crc.update(CRLF);
parser_state = ParserState::ReadingMessage;
} else {
parser_state = ParserState::WaitingForHeader;
}
}
ParserState::ReadingMessage => {
const CRC_PREFIX: &[u8] = b"!";
if let Some(crc_str) = line.strip_prefix(CRC_PREFIX) {
crc.update(CRC_PREFIX);
expected_crc = Some(u16::from_str_radix(str::from_utf8(crc_str)?, 16)?);
break;
} else {
crc.update(&line);
crc.update(CRLF);
if let Some(line) = ParsedLine::parse(&line) {
match line.obis.obis {
b"0.2.8" => out.version = Some(line.value_str().to_string()),
b"1.0.0" => out.electricity_date = Tst::try_from_bytes(line.value),
b"96.1.1" => out.electricity_equipment_id = parse_octet_string(line.value),
b"1.8.1" => out.electricity_consumed_tariff_1 = Some(line.value_str().parse()?),
b"1.8.2" => out.electricity_consumed_tariff_2 = Some(line.value_str().parse()?),
b"2.8.1" => out.electricity_generated_tariff_1 = Some(line.value_str().parse()?),
b"2.8.2" => out.electricity_generated_tariff_2 = Some(line.value_str().parse()?),
b"96.14.0" => out.current_tariff = parse_octet_string(line.value),
b"1.7.0" => out.power = Some(line.value_str().parse()?),
b"2.7.0" => out.return_power = Some(line.value_str().parse()?),
b"96.7.21" => out.power_failure_count = Some(line.value_str().parse()?),
b"96.7.9" => out.long_power_failure_count = Some(line.value_str().parse()?),
b"99.97.0" => {
out.power_failure_log = parse_buffer(line.value_str())
.into_iter()
.map(|(end_date, duration)| PowerFailureEntry { end_date, duration })
.collect()
}
b"32.32.0" => out.voltage_sag_l1_count = Some(line.value_str().parse()?),
b"52.32.0" => out.voltage_sag_l2_count = Some(line.value_str().parse()?),
b"72.32.0" => out.voltage_sag_l3_count = Some(line.value_str().parse()?),
b"32.36.0" => out.voltage_swell_l1_count = Some(line.value_str().parse()?),
b"52.36.0" => out.voltage_swell_l2_count = Some(line.value_str().parse()?),
b"72.36.0" => out.voltage_swell_l3_count = Some(line.value_str().parse()?),
b"96.13.0" => out.message = parse_octet_string(line.value),
b"32.7.0" => out.voltage_l1 = Some(line.value_str().parse()?),
b"52.7.0" => out.voltage_l2 = Some(line.value_str().parse()?),
b"72.7.0" => out.voltage_l3 = Some(line.value_str().parse()?),
b"31.7.0" => out.current_l1 = Some(line.value_str().parse()?),
b"51.7.0" => out.current_l2 = Some(line.value_str().parse()?),
b"71.7.0" => out.current_l3 = Some(line.value_str().parse()?),
b"21.7.0" => out.power_l1 = Some(line.value_str().parse()?),
b"41.7.0" => out.power_l2 = Some(line.value_str().parse()?),
b"61.7.0" => out.power_l3 = Some(line.value_str().parse()?),
b"22.7.0" => out.return_power_l1 = Some(line.value_str().parse()?),
b"42.7.0" => out.return_power_l2 = Some(line.value_str().parse()?),
b"62.7.0" => out.return_power_l3 = Some(line.value_str().parse()?),
b"24.1.0" => out.device_type = Some(line.value_str().to_string()),
b"96.1.0" => out.gas_equipment_id = parse_octet_string(line.value),
b"24.2.1" => {
let (gas_date, gas_consumed) = parse_mbus_value(line.value_str());
out.gas_date = Tst::try_from_bytes(gas_date.as_bytes());
if let Some(gas_consumed) = gas_consumed {
out.gas_consumed = Some(gas_consumed.parse()?);
}
}
_ => warn!(
"Unknown OBIS: {}-{}-{} with value: {}",
line.obis.medium,
line.obis.channel,
String::from_utf8_lossy(line.obis.obis),
String::from_utf8_lossy(line.value),
),
}
}
}
}
}
}
if let Some(expected_crc) = expected_crc {
let actual_crc = crc.get();
if actual_crc == expected_crc {
Ok(Some(out))
} else {
Err(Error::CrcMismatch(actual_crc, expected_crc))
}
} else {
Ok(None)
}
}
}
#[derive(Debug, PartialOrd, PartialEq, Clone)]
pub struct PowerFailureEntry {
pub end_date: Tst,
pub duration: UnitValue<u64>,
}
fn parse_mbus_value(value: &str) -> (&str, Option<&str>) {
value
.split_once(")(")
.map_or((value, None), |(val1, val2)| (val1, Some(val2)))
}
fn parse_buffer(value: &str) -> Vec<(Tst, UnitValue<u64>)> {
let mut out = vec![];
let mut parts = value.split(")(");
'extract: {
let Some(count) = parts.next() else {
break 'extract;
};
let Ok(count) = u8::from_str(count) else {
break 'extract;
};
out.reserve(usize::from(count));
let Some(obis) = parts.next() else {
break 'extract;
};
let Some((obis, _)) = ParsedObis::parse(obis.as_bytes()) else {
break 'extract;
};
if obis.obis != b"96.7.19" {
break 'extract;
}
while let Some((date, value)) = parts.next().zip(parts.next()) {
let Some(date) = Tst::try_from_bytes(date.as_bytes()) else {
continue;
};
let Ok(value) = UnitValue::from_str(value) else {
continue;
};
out.push((date, value));
}
}
out
}
fn parse_octet_string(value: &[u8]) -> Option<String> {
let mut out = String::with_capacity(value.len() / 2);
for bytes in value.chunks(2) {
out.push(char::from(u8::from_str_radix(str::from_utf8(bytes).ok()?, 16).ok()?));
}
Some(out)
}
struct ParsedLine<'l> {
pub obis: ParsedObis<'l>,
pub value: &'l [u8],
}
impl ParsedLine<'_> {
fn parse(line: &[u8]) -> Option<ParsedLine> {
#[expect(clippy::enum_variant_names)]
enum State {
WaitingForValue,
ReadingValue,
WaitingForAnotherValue,
}
let (obis, line) = ParsedObis::parse(line)?;
let mut out = ParsedLine { obis, value: &[] };
let mut state = State::WaitingForValue;
let mut start_offset = 0;
for (offset, byte) in line.iter().enumerate() {
let c = char::from(*byte);
state = match state {
State::WaitingForValue => match c {
'(' => {
start_offset = offset + 1;
State::ReadingValue
}
_ => return None,
},
State::ReadingValue => match c {
')' => {
out.value = line.get(start_offset..offset)?;
State::WaitingForAnotherValue
}
_ => State::ReadingValue,
},
State::WaitingForAnotherValue => match c {
'(' => State::ReadingValue,
_ => return None,
},
};
}
if matches!(state, State::WaitingForAnotherValue) {
Some(out)
} else {
None
}
}
pub fn value_str(&self) -> &str {
str::from_utf8(self.value).unwrap_or("")
}
}
#[derive(Debug)]
struct ParsedObis<'l> {
pub medium: u8,
pub channel: u8,
pub obis: &'l [u8],
}
impl ParsedObis<'_> {
fn parse(line: &[u8]) -> Option<(ParsedObis, &[u8])> {
enum State {
WaitingForObisMedium,
ReadingObisMedium,
WaitingForObisChannel,
ReadingObisChannel,
WaitingForObis,
ReadingObis,
Done,
}
let mut out = ParsedObis {
medium: 0,
channel: 0,
obis: &[],
};
let mut state = State::WaitingForObisMedium;
let mut start_offset = 0;
for (offset, byte) in line.iter().enumerate() {
let c = char::from(*byte);
state = match state {
State::WaitingForObisMedium => match c {
c if c.is_ascii_digit() => {
start_offset = offset;
State::ReadingObisMedium
}
_ => return None,
},
State::ReadingObisMedium => match c {
c if c.is_ascii_digit() => State::ReadingObisMedium,
'-' => {
out.medium = str::from_utf8(line.get(start_offset..offset)?).ok()?.parse().ok()?;
State::WaitingForObisChannel
}
_ => return None,
},
State::WaitingForObisChannel => match c {
c if c.is_ascii_digit() => {
start_offset = offset;
State::ReadingObisChannel
}
_ => return None,
},
State::ReadingObisChannel => match c {
c if c.is_ascii_digit() => State::ReadingObisChannel,
':' => {
out.channel = str::from_utf8(line.get(start_offset..offset)?).ok()?.parse().ok()?;
State::WaitingForObis
}
_ => return None,
},
State::WaitingForObis => match c {
c if c.is_ascii_digit() => {
start_offset = offset;
State::ReadingObis
}
_ => return None,
},
State::ReadingObis => match c {
c if c.is_ascii_digit() || c == '.' => State::ReadingObis,
'(' => {
out.obis = line.get(start_offset..offset)?;
start_offset = offset;
State::Done
}
_ => return None,
},
State::Done => break,
};
}
match state {
State::ReadingObis => {
out.obis = line.get(start_offset..)?;
Some((out, &[]))
}
State::Done => Some((out, line.get(start_offset..)?)),
_ => None,
}
}
}