use core::fmt::{Debug, Display};
use core::str::FromStr;
use crate::{Error, Result};
#[derive(Debug, PartialEq, Eq)]
pub enum Line {
L1,
L2,
L3,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Power {
Active,
Reactive,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Direction {
FromGrid,
ToGrid,
}
use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
use Direction::*;
use Power::*;
#[derive(Debug, PartialEq, Eq)]
pub enum Object {
DateTime(OffsetDateTime),
Energy(Power, Direction, u32),
TotalPower(Power, Direction, u32),
Power(Line, Power, Direction, u32),
Voltage(Line, u16),
Current(Line, u16),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Obis(pub u8, pub u8, pub u8, pub u8, pub u8);
impl Display for Obis {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let Self(a, b, c, d, e) = self;
write!(f, "{}-{}:{}.{}.{}", a, b, c, d, e)
}
}
impl Obis {
fn from_str_opt(s: &str) -> Option<Self> {
let (a, s) = s.split_once('-')?;
let a = a.parse().ok()?;
let (b, s) = s.split_once(':')?;
let b = b.parse().ok()?;
let mut iter = s.split('.').map(|part| part.parse().ok());
Some(Self(a, b, iter.next()??, iter.next()??, iter.next()??))
}
}
impl FromStr for Obis {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Obis::from_str_opt(s).ok_or(Error::InvalidFormat)
}
}
fn split_value(s: &str) -> Option<(&str, &str)> {
let end = s.len().checked_sub(1)?; let inner = s.get(..end)?;
inner.split_once('*')
}
fn parse_decimal<const F: u8>(s: &str) -> Option<u32> {
let (decimal, _unit) = split_value(s)?;
let (i, f) = decimal.rsplit_once('.')?;
if f.len() != F.into() {
return None;
}
let i: u32 = i.parse().ok()?;
let f: u32 = f.parse().ok()?;
i.checked_mul(10u32.pow(F.into()))?.checked_add(f)
}
fn parse_kilo(s: &str) -> Result<u32, Error> {
parse_decimal::<3>(s).ok_or(Error::InvalidFormat)
}
fn parse_deci(s: &str) -> Result<u16, Error> {
parse_decimal::<1>(s)
.and_then(|v| v.try_into().ok())
.ok_or(Error::InvalidFormat)
}
fn pow_dir(a: u8) -> Result<(Power, Direction)> {
match a {
1 => Ok((Active, FromGrid)),
2 => Ok((Active, ToGrid)),
3 => Ok((Reactive, FromGrid)),
4 => Ok((Reactive, ToGrid)),
_ => Err(Error::InvalidFormat),
}
}
impl FromStr for Object {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (obis, body) = s.split_once('(').ok_or(Error::InvalidFormat)?;
let obis: Obis = obis.parse()?;
match obis {
Obis(0, 0, 1, 0, 0) => Ok(Object::DateTime(parse_datetime(body)?)),
Obis(1, 0, c @ 1..=4, d @ 7..=8, 0) => {
let (pow, dir) = pow_dir(c)?;
match d {
7 => Ok(Object::TotalPower(pow, dir, parse_kilo(body)?)),
8 => Ok(Object::Energy(pow, dir, parse_kilo(body)?)),
_ => unreachable!(),
}
}
Obis(1, 0, c @ 21..=24 | c @ 41..=44 | c @ 61..=64, 7, 0) => {
let line = match c / 20 {
1 => Line::L1,
2 => Line::L2,
3 => Line::L3,
_ => unreachable!(),
};
let (pow, dir) = pow_dir(c % 20)?;
Ok(Object::Power(line, pow, dir, parse_kilo(body)?))
}
Obis(1, 0, c @ 31..=32 | c @ 51..=52 | c @ 71..=72, 7, 0) => {
let line = match c {
31..=32 => Line::L1,
51..=52 => Line::L2,
71..=72 => Line::L3,
_ => unreachable!(),
};
match c % 10 {
1 => Ok(Object::Current(line, parse_deci(body)?)),
2 => Ok(Object::Voltage(line, parse_deci(body)?)),
_ => unreachable!(),
}
}
_ => Err(Error::UnrecognizedReference),
}
}
}
fn parse_datetime(s: &str) -> Result<OffsetDateTime> {
let parsetwo = |i| {
s.get(i..=(i + 1))
.and_then(|s| s.parse::<u8>().ok())
.ok_or(Error::InvalidFormat)
};
let year: i32 = i32::from(parsetwo(0)?) + 2000;
let month = match s.get(2..4).ok_or(Error::InvalidFormat)? {
"01" => Month::January,
"02" => Month::February,
"03" => Month::March,
"04" => Month::April,
"05" => Month::May,
"06" => Month::June,
"07" => Month::July,
"08" => Month::August,
"09" => Month::September,
"10" => Month::October,
"11" => Month::November,
"12" => Month::December,
_ => return Err(Error::InvalidFormat),
};
let day = parsetwo(4)?;
let date = Date::from_calendar_date(year, month, day).map_err(|_| Error::InvalidFormat)?;
let time = Time::from_hms(parsetwo(6)?, parsetwo(8)?, parsetwo(10)?)
.map_err(|_| Error::InvalidFormat)?;
let offset = match s.get(12..=12) {
Some("W") => UtcOffset::__from_hms_unchecked(1, 0, 0),
Some("S") => UtcOffset::__from_hms_unchecked(2, 0, 0),
_ => return Err(Error::InvalidFormat),
};
Ok(PrimitiveDateTime::new(date, time).assume_offset(offset))
}
#[cfg(test)]
mod tests {
use time::macros::datetime;
use crate::Line;
use super::{parse_datetime, Direction, Object, Power};
#[test]
fn datetime_obj() {
assert_eq!(
"0-0:1.0.0(221022162844W)".parse::<Object>().unwrap(),
Object::DateTime(datetime!(2022-10-22 15:28:44 UTC))
);
}
#[test]
fn datetime_parsing() {
assert!(parse_datetime("9999999999W").is_err());
assert!(parse_datetime("aaaaaa").is_err());
assert!(parse_datetime("220717231648").is_err());
assert_eq!(
parse_datetime("220717231648S").unwrap(),
datetime!(2022-07-17 21:16:48 UTC)
);
}
#[test]
fn parse() {
assert_eq!(
"1-0:1.8.0(00006136.930*kWh)".parse::<Object>().unwrap(),
Object::Energy(Power::Active, Direction::FromGrid, 6136930)
);
assert_eq!(
"1-0:72.7.0(235.5*V)".parse::<Object>().unwrap(),
Object::Voltage(Line::L3, 2355)
);
}
}