use std::collections::BTreeMap;
use bitflags::bitflags;
use super::control_status::{
control_status_message, ControlStatusMessage, ControlStatusMessageSubtype,
};
use crate::types::Temperature;
const SUBTYPE_AC_STATUS: ControlStatusMessageSubtype = 0x23;
macro_rules! ac_status_nibble {
($(#[$($attrss:tt)*])*
$vis:vis enum $name:ident : $ut:ty = $mask:literal {
$($val:ident = $bits:literal,)*
} ) => {
$(#[$($attrss)*])*
#[derive(Clone, Copy, Debug, PartialEq)]
$vis enum $name {
$($val = $bits,)*
}
impl TryFrom<$ut> for $name {
type Error = MessageError;
fn try_from(value: $ut) -> Result<Self, Self::Error> {
let value = value & $mask;
match value {
$(x if x == Self::$val as $ut => Ok(Self::$val),)*
_ => Err(MessageError::InvalidData),
}
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
core::fmt::Debug::fmt(self, f)
}
}
};
}
#[cfg(feature = "control")] pub(super) use ac_status_nibble;
ac_status_nibble!(
pub enum AcPower: u8 = 0xf0 {
Off = 0b0000_0000,
On = 0b0001_0000,
AwayOff = 0b0010_0000,
AwayOn = 0b0011_0000,
Sleep = 0b0101_0000,
}
);
ac_status_nibble!(
#[rustfmt::skip]
pub enum AcMode: u8 = 0xf0 {
Auto = 0b0000_0000,
Heat = 0b0001_0000,
Dry = 0b0010_0000,
Fan = 0b0011_0000,
Cool = 0b0100_0000,
AutoHeat = 0b1000_0000,
AutoCool = 0b1001_0000,
}
);
ac_status_nibble!(
#[rustfmt::skip]
pub enum FanSpeed: u8 = 0x07 {
Auto = 0b0000,
Quiet = 0b0001,
Low = 0b0010,
Medium = 0b0011,
High = 0b0100,
Powerful = 0b0101,
Turbo = 0b0111,
}
);
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq)]
#[rustfmt::skip]
pub struct AcFlags: u8 {
const Timer = 1 << 0;
const Spill = 1 << 1;
const ByPass = 1 << 2;
const Turbo = 1 << 3;
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AcStatus {
pub power: Option<AcPower>,
pub mode: Option<AcMode>,
pub fan_speed: Option<(FanSpeed, bool)>,
pub setpoint: Option<Temperature>,
pub temperature: Option<Temperature>,
pub flags: AcFlags,
pub error: Option<u16>,
pub unused: Option<u16>,
}
impl std::fmt::Display for AcStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::rc::Rc;
let na: Rc<String> = Rc::new("n/a".to_string());
write!(
f,
"{} {} {}",
self.power.map_or(na.clone(), |x| Rc::new(format!("{}", x))),
self.mode.map_or(na.clone(), |x| Rc::new(format!("{}", x))),
self.fan_speed.map_or(na.clone(), |x| Rc::new(format!(
"{}{}",
x.0,
if x.1 { "(IntelligentAuto)" } else { "" }
))),
)?;
if let Some(t) = self.temperature {
write!(f, " {:#}", t)?;
}
if let Some(t) = self.setpoint {
write!(f, " -> {:#}", t)?;
}
if !self.flags.is_empty() {
write!(f, " ")?;
bitflags::parser::to_writer_strict(&self.flags, f)?;
}
Ok(())
}
}
control_status_message!(
SUBTYPE_AC_STATUS,
pub struct AcStatusMessage {
pub acs: BTreeMap<u8, AcStatus>,
},
{
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 {
10
}
}
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 => 0xf0,
} | (ac_idx & 0x0f))
.to_be_bytes(),
)?;
dst.write_all(
&(match ac.mode {
Some(m) => m as u8,
None => 0xf0,
} | match ac.fan_speed {
Some((s, false)) => s as u8,
Some((s, true)) => s as u8 | 0b1000,
None => 0x0f,
})
.to_be_bytes(),
)?;
dst.write_all(
&(match ac.setpoint {
Some(t) => t.as_setpoint_bits(),
None => 0xff,
})
.to_be_bytes(),
)?;
dst.write_all(&ac.flags.bits().to_be_bytes())?;
dst.write_all(
&(match ac.temperature {
Some(t) => t.as_sensor_bits(),
None => 0x03ff,
})
.to_be_bytes(),
)?;
dst.write_all(&ac.error.unwrap_or_default().to_be_bytes())?;
dst.write_all(&ac.unused.unwrap_or_default().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() < 8 {
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()
.map(|s| (s, data[1] & 0b1000 != 0));
let setpoint = Temperature::from_setpoint(data[2]).ok();
let flags = AcFlags::from_bits_retain(data[3]);
let temperature =
Temperature::from_sensor(u16::from_be_bytes(data[4..6].try_into().unwrap()))
.ok();
let error = match u16::from_be_bytes(data[6..8].try_into().unwrap()) {
0 => None,
e => Some(e),
};
let unused = if data.len() >= 10 {
Some(u16::from_be_bytes(data[8..10].try_into().unwrap()))
} else {
None
};
let ac = AcStatus {
power,
mode,
fan_speed,
setpoint,
flags,
temperature,
error,
unused,
};
acs.insert(ac_idx, ac);
}
Ok(Self {
message_id,
is_request,
acs,
})
}
}
);
impl AcStatusMessage {
pub fn request() -> Self {
Self {
message_id: super::next_msg_id(),
is_request: true,
acs: BTreeMap::new(),
}
}
pub fn new<K: Into<u8>, V: Into<AcStatus>, 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<AcStatus>, T: IntoIterator<Item = (K, V)>>(
message_id: u8,
acs: T,
) -> Self {
Self {
message_id,
is_request: false,
acs: acs.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 ACS: std::sync::LazyLock<[(u8, AcStatus); 2]> =
std::sync::LazyLock::new(|| {
[
(
0,
AcStatus {
power: Some(AcPower::On),
mode: Some(AcMode::Heat),
fan_speed: Some((FanSpeed::Low, false)),
setpoint: Some(22.into()),
flags: AcFlags::empty(),
temperature: Some(23.into()),
error: None,
unused: Some(0x8000),
},
),
(
1,
AcStatus {
power: Some(AcPower::Off),
mode: Some(AcMode::Cool),
fan_speed: Some((FanSpeed::Low, false)),
setpoint: Some(20.into()),
flags: AcFlags::empty(),
temperature: Some(24.into()),
error: None,
unused: Some(0x8000),
},
),
]
});
#[test]
fn test_ac_status_request() {
let orig = AcStatusMessage::request();
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.data.len(), MSG_HEADER_SIZE);
let req: AcStatusMessage = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[test]
fn test_zone_status_response() {
let orig = AcStatusMessage::with_message_id(17, *ACS);
let frame = orig.clone().into_frame().expect("into frame failed");
assert_eq!(frame.msg_id, 17);
assert_eq!(frame.data.len(), MSG_HEADER_SIZE + ACS.len() * 10);
let resp: AcStatusMessage = frame.try_into().expect("from frame failed");
assert_eq!(resp, orig);
}
#[test]
fn test_ac_status_request_from_data() {
let req: AcStatusMessage = frame(MSG_REQ_STATUS_ACS)
.try_into()
.expect("from frame failed");
assert!(req.is_request);
assert_eq!(req.acs.len(), 0);
let f: Frame = req.try_into().expect("into frame failed");
assert_eq!(f, frame(MSG_REQ_STATUS_ACS));
}
#[test]
fn test_ac_status_response_from_data() {
let resp: AcStatusMessage = frame(MSG_RESP_STATUS_ACS)
.try_into()
.expect("from frame failed");
assert!(!resp.is_request);
assert_eq!(resp.acs.len(), 2);
let f: Frame = resp.try_into().expect("into frame failed");
assert_eq!(f, frame(MSG_RESP_STATUS_ACS));
}
}