use std::collections::BTreeMap;
use super::{
ac_status::ac_status_nibble,
control_status::{control_status_message, ControlStatusMessage, ControlStatusMessageSubtype},
};
use crate::types::Temperature;
const SUBTYPE_ZONE_CONTROL: ControlStatusMessageSubtype = 0x20;
ac_status_nibble!(
#[allow(clippy::unusual_byte_groupings)]
pub enum ZonePower: u8 = 0x07 {
Toggle = 0b00000_001,
Off = 0b00000_010,
On = 0b00000_011,
Turbo = 0b00000_101,
}
);
ac_status_nibble!(
#[allow(clippy::unusual_byte_groupings)]
pub enum ZoneControlType: u8 = 0x18 {
Toggle = 0b000_01_000,
Airflow = 0b000_10_000,
Temperature = 0b000_11_000,
}
);
ac_status_nibble!(
enum ZoneControlValueBits: u8 = 0xe0 {
Decrement = 0b010_00000,
Increment = 0b011_00000,
Airflow = 0b100_00000,
Temperature = 0b101_00000,
}
);
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ZoneControlValue {
Decrement,
Increment,
Airflow(u8),
Temperature(Temperature),
}
impl ZoneControlValue {
fn as_bits(&self) -> Result<(ZoneControlValueBits, u8), MessageError> {
Ok(match self {
Self::Decrement => (ZoneControlValueBits::Decrement, 0xff),
Self::Increment => (ZoneControlValueBits::Increment, 0xff),
Self::Airflow(pct) if *pct <= 100 => (ZoneControlValueBits::Airflow, *pct),
Self::Temperature(t) if t.is_setpoint_valid() => {
(ZoneControlValueBits::Temperature, t.as_setpoint_bits())
}
_ => return Err(MessageError::InvalidData),
})
}
}
impl std::ops::BitOr<u8> for ZoneControlValue {
type Output = Result<u16, MessageError>;
fn bitor(self, rhs: u8) -> Self::Output {
let (a, b) = self.as_bits()?;
Ok((a as u16 | rhs as u16) << 8 | b as u16)
}
}
impl std::ops::BitOr<ZoneControlValue> for u8 {
type Output = Result<u16, MessageError>;
fn bitor(self, rhs: ZoneControlValue) -> Self::Output {
rhs | self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ZoneControl {
pub power: Option<ZonePower>,
pub control: Option<ZoneControlType>,
pub value: Option<ZoneControlValue>,
}
control_status_message!(
SUBTYPE_ZONE_CONTROL,
pub struct ZoneControlMessage {
zones: BTreeMap<u8, ZoneControl>,
},
{
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 {
4
}
}
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_idx & 0x3f).to_be_bytes())?;
let a = match zone.power {
Some(p) => p as u8,
None => 0x00,
} | match zone.control {
Some(c) => c as u8,
None => 0x00,
};
dst.write_all(
&(match zone.value {
Some(v) => (a | v)?,
None => (a as u16) << 8 | 0x00ff,
})
.to_be_bytes(),
)?;
dst.write_all(&0u8.to_be_bytes())?;
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() < 4 {
return Err(MessageError::InvalidData);
}
let zone_idx = data[0] & 0x3f;
let power = ZonePower::try_from(data[1] & 0x07).ok();
let control = ZoneControlType::try_from(data[1] & 0x18).ok();
let value = match ZoneControlValueBits::try_from(data[1] & 0xe0) {
Ok(ZoneControlValueBits::Decrement) => Some(ZoneControlValue::Decrement),
Ok(ZoneControlValueBits::Increment) => Some(ZoneControlValue::Increment),
Ok(ZoneControlValueBits::Airflow) if data[2] <= 100 => {
Some(ZoneControlValue::Airflow(data[2]))
}
Ok(ZoneControlValueBits::Temperature) => {
match Temperature::from_setpoint(data[2]) {
Ok(t) => Some(ZoneControlValue::Temperature(t)),
_ => None,
}
}
_ => None,
};
let zone = ZoneControl {
power,
control,
value,
};
zones.insert(zone_idx, zone);
}
Ok(Self {
message_id,
is_request,
zones,
})
}
}
);
impl ZoneControlMessage {
pub fn new<K: Into<u8>, V: Into<ZoneControl>, 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<ZoneControl>, T: IntoIterator<Item = (K, V)>>(
message_id: u8,
zones: T,
) -> Self {
Self {
message_id,
is_request: true,
zones: zones
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::super::control_status::MSG_HEADER_SIZE;
use super::*;
use rstest::rstest;
use crate::conn::tests::data::*;
mod data {
#[rustfmt::skip]
pub(crate) const MSG_REQ_ZONE_OFF: &[u8] = &[
0x55, 0x55, 0x55, 0xAA, 0x80, 0xB0, 0x0F, 0xC0, 0x00, 0x0C, 0x20, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x01, 0x02, 0xFF, 0x00, 0xF0, 0xA1, ];
}
#[rstest]
#[case(ZonePower::On)]
#[case(ZonePower::Off)]
#[case(ZonePower::Turbo)]
#[case(ZonePower::Toggle)]
fn test_zone_power(#[case] power: ZonePower) {
let zone_idx = 5u8;
let orig = ZoneControlMessage::new([(
zone_idx,
ZoneControl {
power: Some(power),
control: None,
value: None,
},
)]);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data[MSG_HEADER_SIZE], zone_idx);
assert_eq!(frame.data[MSG_HEADER_SIZE + 1], power as u8);
assert_eq!(frame.data[MSG_HEADER_SIZE + 2], 0xff);
assert_eq!(frame.data[MSG_HEADER_SIZE + 3], 0x00);
let req: ZoneControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[rstest]
#[case(ZoneControlType::Airflow)]
#[case(ZoneControlType::Temperature)]
#[case(ZoneControlType::Toggle)]
fn test_zone_control(#[case] control: ZoneControlType) {
let zone_idx = 5u8;
let orig = ZoneControlMessage::new([(
zone_idx,
ZoneControl {
power: None,
control: Some(control),
value: None,
},
)]);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data[MSG_HEADER_SIZE], zone_idx);
assert_eq!(frame.data[MSG_HEADER_SIZE + 1], control as u8);
assert_eq!(frame.data[MSG_HEADER_SIZE + 2], 0xff);
assert_eq!(frame.data[MSG_HEADER_SIZE + 3], 0x00);
let req: ZoneControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[rstest]
#[case(ZoneControlValue::Increment)]
#[case(ZoneControlValue::Decrement)]
#[case(ZoneControlValue::Airflow(0))]
#[case(ZoneControlValue::Airflow(50))]
#[case(ZoneControlValue::Airflow(100))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(100)))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(180)))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(235)))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(350)))]
fn test_zone_value(#[case] value: ZoneControlValue) {
let zone_idx = 5u8;
let orig = ZoneControlMessage::new([(
zone_idx,
ZoneControl {
power: None,
control: None,
value: Some(value),
},
)]);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data[MSG_HEADER_SIZE], zone_idx);
assert_eq!(
frame.data[MSG_HEADER_SIZE + 1],
match value {
ZoneControlValue::Decrement => ZoneControlValueBits::Decrement,
ZoneControlValue::Increment => ZoneControlValueBits::Increment,
ZoneControlValue::Airflow(_) => ZoneControlValueBits::Airflow,
ZoneControlValue::Temperature(_) => ZoneControlValueBits::Temperature,
} as u8
);
assert_eq!(
frame.data[MSG_HEADER_SIZE + 2],
match value {
ZoneControlValue::Airflow(pct) => pct,
ZoneControlValue::Temperature(t) => t.as_setpoint_bits(),
_ => 0xff,
}
);
assert_eq!(frame.data[MSG_HEADER_SIZE + 3], 0x00);
let req: ZoneControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[rstest]
#[case(ZoneControlValue::Airflow(101))]
#[case(ZoneControlValue::Airflow(255))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(0)))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(99)))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(351)))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(i16::MAX)))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(i16::MIN)))]
#[case(ZoneControlValue::Temperature(Temperature::from_deci(-1)))]
fn test_zone_value_invalid(#[case] value: ZoneControlValue) {
let zone_idx = 5u8;
let orig = ZoneControlMessage::new([(
zone_idx,
ZoneControl {
power: None,
control: None,
value: Some(value),
},
)]);
assert_matches!(orig.clone().into_frame(), Err(MessageError::InvalidData));
}
#[test]
fn test_empty_zone_control() {
let orig = ZoneControlMessage::new(BTreeMap::<u8, ZoneControl>::new());
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data.len(), MSG_HEADER_SIZE);
let req: ZoneControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[test]
fn test_zone_contol_from_data_off() {
let req: ZoneControlMessage = frame(data::MSG_REQ_ZONE_OFF)
.try_into()
.expect("from frame failed");
assert!(req.is_request);
assert_eq!(req.zones.len(), 1);
let zone = &req.zones[&1];
assert_eq!(zone.power, Some(ZonePower::Off));
assert_eq!(zone.control, None);
assert_eq!(zone.value, None);
let f: Frame = req.try_into().expect("into frame failed");
assert_eq!(f, frame(data::MSG_REQ_ZONE_OFF));
}
}