use std::collections::BTreeMap;
use super::{
ac_status::ac_status_nibble,
control_status::{control_status_message, ControlStatusMessage, ControlStatusMessageSubtype},
};
use crate::types::Temperature;
const SUBTYPE_AC_CONTROL: ControlStatusMessageSubtype = 0x22;
ac_status_nibble!(
pub enum AcPower: u8 = 0xf0 {
Toggle = 0b0001_0000,
Off = 0b0010_0000,
On = 0b0011_0000,
Away = 0b0100_0000,
Sleep = 0b0101_0000,
}
);
ac_status_nibble!(
pub enum AcMode: u8 = 0xf0 {
Auto = 0b0000_0000,
Heat = 0b0001_0000,
Dry = 0b0010_0000,
Fan = 0b0011_0000,
Cool = 0b0100_0000,
}
);
ac_status_nibble!(
pub enum FanSpeed: u8 = 0x0f {
Auto = 0b0000,
Quiet = 0b0001,
Low = 0b0010,
Medium = 0b0011,
High = 0b0100,
Powerful = 0b0101,
Turbo = 0b0111,
IntelligentAuto = 0b1000,
}
);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AcControl {
pub power: Option<AcPower>,
pub mode: Option<AcMode>,
pub fan_speed: Option<FanSpeed>,
pub setpoint: Option<Temperature>,
}
control_status_message!(
SUBTYPE_AC_CONTROL,
pub struct AcControlMessage {
acs: BTreeMap<u8, AcControl>,
},
{
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.acs.is_empty() {
0
} else {
4
}
}
fn impl_frame_repeat_count(&self) -> u16 {
self.acs.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 (ac_idx, ac) = self.acs.iter().nth(index as usize).unwrap();
dst.write_all(
&(match ac.power {
Some(p) => p as u8,
None => 0x00,
} | (ac_idx & 0x0f))
.to_be_bytes(),
)?;
dst.write_all(
&(match ac.mode {
Some(m) => m as u8,
None => 0xf0,
} | match ac.fan_speed {
Some(f) => f as u8,
None => 0x0f,
})
.to_be_bytes(),
)?;
dst.write_all(
&(match ac.setpoint {
Some(s) if s.is_setpoint_valid() => 0x4000 | s.as_setpoint_bits() as u16,
Some(_) => return Err(MessageError::InvalidData),
None => Temperature::invalid().as_setpoint_bits() as u16, })
.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 acs = BTreeMap::new();
for data in repeat_data {
if data.len() < 4 {
return Err(MessageError::InvalidData);
}
let ac_idx = data[0] & 0x0f;
let power = AcPower::try_from(data[0]).ok();
let mode = AcMode::try_from(data[1]).ok();
let fan_speed = FanSpeed::try_from(data[1]).ok();
let setpoint = match data[2] {
0x40 => Temperature::from_setpoint(data[3]).ok(),
_ => None,
};
let ac = AcControl {
power,
mode,
fan_speed,
setpoint,
};
acs.insert(ac_idx, ac);
}
Ok(Self {
message_id,
is_request,
acs,
})
}
}
);
impl AcControlMessage {
pub fn new<K: Into<u8>, V: Into<AcControl>, T: IntoIterator<Item = (K, V)>>(acs: T) -> Self {
Self::with_message_id(super::next_msg_id(), acs)
}
pub fn with_message_id<K: Into<u8>, V: Into<AcControl>, T: IntoIterator<Item = (K, V)>>(
message_id: u8,
acs: T,
) -> Self {
Self {
message_id,
is_request: true,
acs: acs.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_AC_OFF: &[u8] = &[
0x55, 0x55, 0x55, 0xAA, 0x80, 0xb0, 0x01, 0xC0, 0x00, 0x0C, 0x22, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x21, 0xFF, 0x00, 0xFF, 0xD3, 0x47, ];
#[rustfmt::skip]
pub(crate) const MSG_REQ_AC_ON: &[u8] = &[
0x55, 0x55, 0x55, 0xAA, 0x80, 0xb0, 0x01, 0xC0, 0x00, 0x10, 0x22, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x4F, 0x00, 0xFF, 0x01, 0xFF, 0x40, 0xA0, 0x10, 0x4B, ];
}
#[test]
fn test_empty_ac_control() {
let orig = AcControlMessage::new(BTreeMap::<u8, AcControl>::new());
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data.len(), MSG_HEADER_SIZE);
let req: AcControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[rstest]
#[case(AcPower::On)]
#[case(AcPower::Off)]
#[case(AcPower::Toggle)]
#[case(AcPower::Sleep)]
#[case(AcPower::Away)]
fn test_ac_power(#[case] power: AcPower) {
let ac_idx = 5u8;
let orig = AcControlMessage::new([(
ac_idx,
AcControl {
power: Some(power),
mode: None,
fan_speed: None,
setpoint: None,
},
)]);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data[MSG_HEADER_SIZE], power as u8 | ac_idx);
assert_eq!(frame.data[MSG_HEADER_SIZE + 1], 0xff);
assert_eq!(frame.data[MSG_HEADER_SIZE + 2], 0x00);
assert_eq!(frame.data[MSG_HEADER_SIZE + 3], 0xff);
let req: AcControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[rstest]
#[case(AcMode::Cool)]
#[case(AcMode::Heat)]
#[case(AcMode::Fan)]
#[case(AcMode::Dry)]
#[case(AcMode::Auto)]
fn test_ac_mode(#[case] mode: AcMode) {
let ac_idx = 5u8;
let orig = AcControlMessage::new([(
ac_idx,
AcControl {
power: None,
mode: Some(mode),
fan_speed: None,
setpoint: None,
},
)]);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data[MSG_HEADER_SIZE], ac_idx);
assert_eq!(frame.data[MSG_HEADER_SIZE + 1], mode as u8 | 0x0f);
assert_eq!(frame.data[MSG_HEADER_SIZE + 2], 0x00);
assert_eq!(frame.data[MSG_HEADER_SIZE + 3], 0xff);
let req: AcControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[rstest]
#[case(FanSpeed::Low)]
#[case(FanSpeed::Medium)]
#[case(FanSpeed::High)]
#[case(FanSpeed::Powerful)]
#[case(FanSpeed::Quiet)]
#[case(FanSpeed::Turbo)]
#[case(FanSpeed::Auto)]
#[case(FanSpeed::IntelligentAuto)]
fn test_ac_fan_speed(#[case] speed: FanSpeed) {
let ac_idx = 5u8;
let orig = AcControlMessage::new([(
ac_idx,
AcControl {
power: None,
mode: None,
fan_speed: Some(speed),
setpoint: None,
},
)]);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data[MSG_HEADER_SIZE], ac_idx);
assert_eq!(frame.data[MSG_HEADER_SIZE + 1], speed as u8 | 0xf0);
assert_eq!(frame.data[MSG_HEADER_SIZE + 2], 0x00);
assert_eq!(frame.data[MSG_HEADER_SIZE + 3], 0xff);
let req: AcControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[rstest]
#[case(Temperature::from_deci(180))]
#[case(Temperature::from_deci(195))]
#[case(Temperature::from_deci(257))]
fn test_ac_setpoint(#[case] setpoint: Temperature) {
let ac_idx = 5u8;
let orig = AcControlMessage::new([(
ac_idx,
AcControl {
power: None,
mode: None,
fan_speed: None,
setpoint: Some(setpoint),
},
)]);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data[MSG_HEADER_SIZE], ac_idx);
assert_eq!(frame.data[MSG_HEADER_SIZE + 1], 0xff);
assert_eq!(frame.data[MSG_HEADER_SIZE + 2], 0x40);
assert_eq!(frame.data[MSG_HEADER_SIZE + 3], setpoint.as_setpoint_bits());
let req: AcControlMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[rstest]
#[case(Temperature::from_deci(0))]
#[case(Temperature::from_deci(99))]
#[case(Temperature::from_deci(351))]
#[case(Temperature::from_deci(i16::MAX))]
#[case(Temperature::from_deci(i16::MIN))]
#[case(Temperature::from_deci(-1))]
fn test_ac_setpoint_invalid(#[case] setpoint: Temperature) {
let ac_idx = 5u8;
let orig = AcControlMessage::new([(
ac_idx,
AcControl {
power: None,
mode: None,
fan_speed: None,
setpoint: Some(setpoint),
},
)]);
assert_matches!(orig.clone().into_frame(), Err(MessageError::InvalidData));
}
#[test]
fn test_ac_contol_from_data_off() {
let req: AcControlMessage = frame(data::MSG_REQ_AC_OFF)
.try_into()
.expect("from frame failed");
assert!(req.is_request);
assert_eq!(req.acs.len(), 1);
let ac = &req.acs[&1];
assert_eq!(ac.power, Some(AcPower::Off));
assert_eq!(ac.mode, None);
assert_eq!(ac.fan_speed, None);
assert_eq!(ac.setpoint, None);
let f: Frame = req.try_into().expect("into frame failed");
assert_eq!(f, frame(data::MSG_REQ_AC_OFF));
}
#[test]
fn test_ac_contol_from_data_on() {
let req: AcControlMessage = frame(data::MSG_REQ_AC_ON)
.try_into()
.expect("from frame failed");
assert!(req.is_request);
assert_eq!(req.acs.len(), 2);
let ac = &req.acs[&0];
assert_eq!(ac.power, None);
assert_eq!(ac.mode, Some(AcMode::Cool));
assert_eq!(ac.fan_speed, None);
assert_eq!(ac.setpoint, None);
let ac = &req.acs[&1];
assert_eq!(ac.power, None);
assert_eq!(ac.mode, None);
assert_eq!(ac.fan_speed, None);
assert_eq!(ac.setpoint, Some(Temperature::from_deci(260)));
let f: Frame = req.try_into().expect("into frame failed");
assert_eq!(f, frame(data::MSG_REQ_AC_ON));
}
}