use std::collections::BTreeMap;
use bitflags::bitflags;
use super::control_status::{
control_status_message, ControlStatusMessage, ControlStatusMessageSubtype,
};
use crate::types::Temperature;
const SUBTYPE_ZONE_STATUS: ControlStatusMessageSubtype = 0x21;
#[derive(Clone, Copy, Debug, PartialEq)]
#[rustfmt::skip]
pub enum ZonePower {
Off = 0b00_000000,
On = 0b01_000000,
Turbo = 0b11_000000,
}
impl TryFrom<u8> for ZonePower {
type Error = MessageError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
let value = value & 0xc0;
let (off, on, turbo) = (Self::Off as u8, Self::On as u8, Self::Turbo as u8);
match value {
x if x == off => Ok(Self::Off),
x if x == on => Ok(Self::On),
x if x == turbo => Ok(Self::Turbo),
_ => Err(MessageError::InvalidData),
}
}
}
impl std::fmt::Display for ZonePower {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.pad(&format!("{:?}", self))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ZoneControl {
Airflow(u8),
Temperature(u8, Temperature),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ZoneSensorReading {
NoSensor,
NotAvailable,
Temperature(Temperature),
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq)]
#[rustfmt::skip]
pub struct ZoneFlags: u8 {
const LowBattery = 1 << 0;
const Spill = 1 << 1;
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ZoneStatus {
pub power: ZonePower,
pub control: ZoneControl,
pub sensor_reading: ZoneSensorReading,
pub flags: ZoneFlags,
}
impl std::fmt::Display for ZoneStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:>3} {:3}%",
self.power,
match self.control {
ZoneControl::Airflow(pct) => pct,
ZoneControl::Temperature(pct, _) => pct,
}
)?;
match self.sensor_reading {
ZoneSensorReading::Temperature(t) => write!(f, " {:#}", t)?,
ZoneSensorReading::NotAvailable => write!(f, " n/a")?,
_ => {}
}
if let ZoneControl::Temperature(_, t) = self.control {
write!(f, " -> {:#}", t)?;
}
if !self.flags.is_empty() {
write!(f, " ")?;
bitflags::parser::to_writer(&self.flags, f)?;
}
Ok(())
}
}
control_status_message!(
SUBTYPE_ZONE_STATUS,
pub struct ZoneStatusMessage {
pub zones: BTreeMap<u8, ZoneStatus>,
},
{
fn impl_frame_normal_len(&self) -> usize {
0
}
fn impl_frame_normal_data<W: std::io::Write>(
&self,
_dst: &mut W,
) -> Result<(), MessageError> {
Ok(())
}
fn impl_frame_repeat_len(&self) -> usize {
if self.is_request && self.zones.is_empty() {
0
} else {
8
}
}
fn impl_frame_repeat_count(&self) -> u16 {
self.zones.len().try_into().unwrap_or(u16::MAX)
}
fn impl_frame_repeat_data<W: std::io::Write>(
&self,
index: u16,
dst: &mut W,
) -> Result<(), MessageError> {
let (zone_idx, zone) = self.zones.iter().nth(index as usize).unwrap();
dst.write_all(&(zone.power as u8 | (zone_idx & 0x3f)).to_be_bytes())?;
match zone.control {
ZoneControl::Airflow(pct) => {
dst.write_all(&(pct & 0x7f).to_be_bytes())?;
dst.write_all(&Temperature::invalid().as_setpoint_bits().to_be_bytes())?;
}
ZoneControl::Temperature(pct, temp) => {
dst.write_all(&(0x80 | (pct & 0x7f)).to_be_bytes())?;
dst.write_all(&temp.as_setpoint_bits().to_be_bytes())?;
}
};
match zone.sensor_reading {
ZoneSensorReading::NoSensor => {
dst.write_all(&[0x00])?;
dst.write_all(&Temperature::invalid().as_sensor_bits().to_be_bytes())?;
}
ZoneSensorReading::NotAvailable => {
dst.write_all(&[0x80])?;
dst.write_all(&Temperature::invalid().as_sensor_bits().to_be_bytes())?;
}
ZoneSensorReading::Temperature(temp) => {
dst.write_all(&[0x80])?;
dst.write_all(&temp.as_sensor_bits().to_be_bytes())?;
}
}
dst.write_all(&zone.flags.bits().to_be_bytes())?;
dst.write_all(&[0x00])?;
Ok(())
}
fn from_frame_data(
message_id: u8,
is_request: bool,
_normal_data: Vec<u8>,
repeat_data: Vec<Vec<u8>>,
) -> Result<Self, MessageError> {
let mut zones = BTreeMap::new();
for data in repeat_data {
if data.len() < 7 {
return Err(MessageError::InvalidData);
}
let zone_idx = data[0] & 0x3f;
let power = ZonePower::try_from(data[0])?;
let control = if data[1] & 0x80 != 0 {
ZoneControl::Temperature(
data[1] & 0x7f,
Temperature::from_setpoint(data[2])
.map_err(|_| MessageError::InvalidData)?,
)
} else {
if data[2] != Temperature::invalid().as_setpoint_bits() {
log::warn!("Got setpoint when in Airflow control: {:#04x}", data[2]);
}
ZoneControl::Airflow(data[1] & 0x7f)
};
let sensor_reading = if data[3] & 0x80 == 0 {
ZoneSensorReading::NoSensor
} else {
Temperature::from_sensor(u16::from_be_bytes(data[4..6].try_into().unwrap()))
.map(ZoneSensorReading::Temperature)
.unwrap_or(ZoneSensorReading::NotAvailable)
};
let flags = ZoneFlags::from_bits_retain(data[6]);
let zone = ZoneStatus {
power,
control,
sensor_reading,
flags,
};
zones.insert(zone_idx, zone);
}
Ok(Self {
message_id,
is_request,
zones,
})
}
}
);
impl ZoneStatusMessage {
pub fn request() -> Self {
Self {
message_id: super::next_msg_id(),
is_request: true,
zones: BTreeMap::new(),
}
}
pub fn new<K: Into<u8>, V: Into<ZoneStatus>, T: IntoIterator<Item = (K, V)>>(zones: T) -> Self {
Self::with_message_id(super::next_msg_id(), zones)
}
pub fn with_message_id<K: Into<u8>, V: Into<ZoneStatus>, T: IntoIterator<Item = (K, V)>>(
message_id: u8,
zones: T,
) -> Self {
Self {
message_id,
is_request: false,
zones: zones
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use super::super::control_status::MSG_HEADER_SIZE;
use crate::conn::tests::data::*;
pub(crate) static ZONES: std::sync::LazyLock<[(u8, ZoneStatus); 2]> =
std::sync::LazyLock::new(|| {
[
(
0,
ZoneStatus {
power: ZonePower::On,
control: ZoneControl::Temperature(0, 25.into()),
sensor_reading: ZoneSensorReading::Temperature(24.3.into()),
flags: ZoneFlags::empty(),
},
),
(
1,
ZoneStatus {
power: ZonePower::Off,
control: ZoneControl::Airflow(100),
sensor_reading: ZoneSensorReading::NoSensor,
flags: ZoneFlags::empty(),
},
),
]
});
#[test]
fn test_zone_status_request() {
let orig = ZoneStatusMessage::request();
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data.len(), MSG_HEADER_SIZE);
let req: ZoneStatusMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[test]
fn test_zone_status_response() {
let orig = ZoneStatusMessage::with_message_id(13, *ZONES);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.msg_id, 13);
assert_eq!(frame.data.len(), MSG_HEADER_SIZE + ZONES.len() * 8);
let resp: ZoneStatusMessage = frame.try_into().expect("from frame failed");
assert_eq!(resp, orig);
}
#[test]
fn test_zone_status_request_from_data() {
let req: ZoneStatusMessage = frame(MSG_REQ_STATUS_ZONES)
.try_into()
.expect("from frame failed");
assert!(req.is_request);
assert_eq!(req.zones.len(), 0);
let f: Frame = req.try_into().expect("into frame failed");
assert_eq!(f, frame(MSG_REQ_STATUS_ZONES));
}
#[test]
fn test_zone_status_response_from_data() {
let resp: ZoneStatusMessage = frame(MSG_RESP_STATUS_ZONES)
.try_into()
.expect("from frame failed");
assert!(!resp.is_request);
assert_eq!(resp.zones.len(), 2);
let f: Frame = resp.try_into().expect("into frame failed");
assert_eq!(f, frame(MSG_RESP_STATUS_ZONES));
}
}