vcontrol 0.6.0

A library for communication with Viessmann heating controllers.
Documentation
use core::{fmt, str::FromStr};

use arrayref::array_ref;
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};

#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CircuitTimes {
  mon: CircuitTime,
  tue: CircuitTime,
  wed: CircuitTime,
  thu: CircuitTime,
  fri: CircuitTime,
  sat: CircuitTime,
  sun: CircuitTime,
}

impl CircuitTimes {
  #[rustfmt::skip]
  pub fn from_bytes(bytes: &[u8; 56]) -> Self {
    Self {
      mon: CircuitTime::from_bytes(array_ref![bytes,  0, 8]),
      tue: CircuitTime::from_bytes(array_ref![bytes,  8, 8]),
      wed: CircuitTime::from_bytes(array_ref![bytes, 16, 8]),
      thu: CircuitTime::from_bytes(array_ref![bytes, 24, 8]),
      fri: CircuitTime::from_bytes(array_ref![bytes, 32, 8]),
      sat: CircuitTime::from_bytes(array_ref![bytes, 40, 8]),
      sun: CircuitTime::from_bytes(array_ref![bytes, 48, 8]),
    }
  }

  #[rustfmt::skip]
  pub fn to_bytes(&self) -> [u8; 56] {
    let mon = self.mon.to_bytes();
    let tue = self.tue.to_bytes();
    let wed = self.wed.to_bytes();
    let thu = self.thu.to_bytes();
    let fri = self.fri.to_bytes();
    let sat = self.sat.to_bytes();
    let sun = self.sun.to_bytes();

    [
      mon[0], mon[1], mon[2], mon[3], mon[4], mon[5], mon[6], mon[7],
      tue[0], tue[1], tue[2], tue[3], tue[4], tue[5], tue[6], tue[7],
      wed[0], wed[1], wed[2], wed[3], wed[4], wed[5], wed[6], wed[7],
      thu[0], thu[1], thu[2], thu[3], thu[4], thu[5], thu[6], thu[7],
      fri[0], fri[1], fri[2], fri[3], fri[4], fri[5], fri[6], fri[7],
      sat[0], sat[1], sat[2], sat[3], sat[4], sat[5], sat[6], sat[7],
      sun[0], sun[1], sun[2], sun[3], sun[4], sun[5], sun[6], sun[7],
    ]
  }
}

#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct CircuitTime([Option<TimeSpan>; 4]);

impl CircuitTime {
  pub fn from_bytes(bytes: &[u8; 8]) -> Self {
    Self([
      Time::from_byte(bytes[0]).zip(Time::from_byte(bytes[1])).map(|(from, to)| TimeSpan { from, to }),
      Time::from_byte(bytes[2]).zip(Time::from_byte(bytes[3])).map(|(from, to)| TimeSpan { from, to }),
      Time::from_byte(bytes[4]).zip(Time::from_byte(bytes[5])).map(|(from, to)| TimeSpan { from, to }),
      Time::from_byte(bytes[6]).zip(Time::from_byte(bytes[7])).map(|(from, to)| TimeSpan { from, to }),
    ])
  }

  #[rustfmt::skip]
  pub fn to_bytes(&self) -> [u8; 8] {
    let timespan1 = self.0[0].map(|t| t.to_bytes()).unwrap_or([0xff, 0xff]);
    let timespan2 = self.0[1].map(|t| t.to_bytes()).unwrap_or([0xff, 0xff]);
    let timespan3 = self.0[2].map(|t| t.to_bytes()).unwrap_or([0xff, 0xff]);
    let timespan4 = self.0[3].map(|t| t.to_bytes()).unwrap_or([0xff, 0xff]);

    [
      timespan1[0], timespan1[1],
      timespan2[0], timespan2[1],
      timespan3[0], timespan3[1],
      timespan4[0], timespan4[1],
    ]
  }
}

impl fmt::Display for CircuitTime {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    let mut comma = false;
    for timespan in self.0 {
      if comma {
        write!(f, ", ")?;
      }

      if let Some(timespan) = timespan {
        write!(f, "{}", timespan)?;
      } else {
        write!(f, "--:-- – --:--")?;
      }

      comma = true;
    }

    Ok(())
  }
}

impl fmt::Debug for CircuitTime {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "CircuitTime(")?;
    fmt::Display::fmt(self, f)?;
    write!(f, ")")
  }
}

#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
struct TimeSpan {
  from: Time,
  to: Time,
}

impl TimeSpan {
  pub fn to_bytes(self) -> [u8; 2] {
    [self.from.to_byte(), self.to.to_byte()]
  }
}

impl fmt::Display for TimeSpan {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{}{}", self.from, self.to)
  }
}

#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq)]
struct Time {
  hour: u8,
  minute: u8,
}

impl Time {
  pub const fn from_byte(byte: u8) -> Option<Self> {
    match byte {
      0xff => None,
      byte => {
        let hour = byte >> 3;
        let minute = (byte & 0b111) * 10;

        assert!(hour <= 24);
        assert!(minute < 60);

        Some(Self { hour, minute })
      },
    }
  }

  pub const fn to_byte(self) -> u8 {
    (self.hour << 3) | (self.minute / 10)
  }
}

impl fmt::Display for Time {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{:02}:{:02}", self.hour, self.minute)
  }
}

impl FromStr for Time {
  type Err = &'static str;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    let mut chars = s.chars();

    fn char_to_u8(c: char) -> Option<u8> {
      if c.is_ascii_digit() {
        return Some(c as u8 - b'0');
      }

      None
    }

    let h1 = chars.next().and_then(char_to_u8).ok_or("first hour character is not a number")?;
    let h2 = chars.next().and_then(char_to_u8).ok_or("second hour character is not a number")?;
    chars.next().filter(|&sep| sep == ':').ok_or("separator is not ':'")?;
    let m1 = chars.next().and_then(char_to_u8).ok_or("first minute character is not a number")?;
    let m2 = chars.next().and_then(char_to_u8).ok_or("second minute character is not a number")?;

    let hour = h1 * 10 + h2;

    if hour > 24 {
      return Err("hour out of range");
    }

    let minute = m1 * 10 + m2;

    if minute >= 60 {
      return Err("minute out of range");
    }

    Ok(Time { hour, minute })
  }
}

impl<'de> Deserialize<'de> for Time {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    let s = String::deserialize(deserializer)?;
    s.parse::<Time>().map_err(de::Error::custom)
  }
}

impl Serialize for Time {
  fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
    serializer.serialize_str(&self.to_string())
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn time_from_str_24() {
    let s = r#""24:00""#;

    let time = serde_json::from_str::<Time>(s).unwrap();
    assert_eq!(time, Time { hour: 24, minute: 0 });
  }
}