use std::collections::BTreeMap;
use bitflags::bitflags;
use super::extended::{extended_message, ExtendedMessage, ExtendedMessageSubtype};
const SUBTYPE_AC_CAP: ExtendedMessageSubtype = 0xff11;
pub type Setpoint = u8;
bitflags! {
#[derive(Clone, Debug, PartialEq)]
#[rustfmt::skip]
pub struct AcModes: u8 {
const Auto = 1 << 0;
const Heat = 1 << 1;
const Dry = 1 << 2;
const Fan = 1 << 3;
const Cool = 1 << 4;
}
#[derive(Clone, Debug, PartialEq)]
#[rustfmt::skip]
pub struct FanSpeeds: u8 {
const Auto = 1 << 0;
const Quiet = 1 << 1;
const Low = 1 << 2;
const Medium = 1 << 3;
const High = 1 << 4;
const Powerful = 1 << 5;
const Turbo = 1 << 6;
const IntelligentAuto = 1 << 7;
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AcCapability {
pub name: String,
pub zone_start_index: u8,
pub zone_count: u8,
pub supported_modes: AcModes,
pub supported_fan_speeds: FanSpeeds,
pub setpoint_cool_min: Setpoint,
pub setpoint_cool_max: Setpoint,
pub setpoint_heat_min: Setpoint,
pub setpoint_heat_max: Setpoint,
}
impl std::fmt::Display for AcCapability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let w = std::cmp::max(f.width().unwrap_or(NAME_LEN_MAX), self.name.len());
let s = " ".repeat(w + 2);
if f.alternate() {
write!(f,
"{:>w$}: Zones: {} [{}\u{2013}{}]\n{s}Modes: {}\n{s}Fan: {}\n{s}Cooling: {}\u{2013}{}\n{s}Heating: {}\u{2013}{}",
self.name,
self.zone_count,
self.zone_start_index,
self.zone_count + self.zone_start_index - 1,
self.supported_modes.iter_names().map(|(n, _)| n).collect::<Vec<&str>>().join(","),
self.supported_fan_speeds.iter_names().map(|(n, _)| n).collect::<Vec<&str>>().join(","),
self.setpoint_cool_min,
self.setpoint_cool_max,
self.setpoint_heat_min,
self.setpoint_heat_max,
)
} else {
write!(f,
"{}: Zones: {} [{}\u{2013}{}]; Modes: {}; Fan: {}; Cooling: {}\u{2013}{}; Heating: {}\u{2013}{}",
self.name,
self.zone_count,
self.zone_start_index,
self.zone_count + self.zone_start_index - 1,
self.supported_modes.iter_names().map(|(n, _)| n).collect::<Vec<&str>>().join(","),
self.supported_fan_speeds.iter_names().map(|(n, _)| n).collect::<Vec<&str>>().join(","),
self.setpoint_cool_min,
self.setpoint_cool_max,
self.setpoint_heat_min,
self.setpoint_heat_max,
)
}
}
}
const AC_CAP_SIZE: usize = 24;
const NAME_LEN_MAX: usize = 16;
extended_message!(SUBTYPE_AC_CAP,
pub struct AcCapabilityRequest {
pub ac_index: Option<u8>,
}
pub struct AcCapabilityResponse {
pub acs: BTreeMap<u8, AcCapability>,
}
{
fn impl_frame_data_len(&self) -> usize {
if self.ac_index.is_none() { 0 } else { size_of::<u8>() }
}
fn impl_frame_data<W: std::io::Write>(&self, dst: &mut W) -> Result<(), super::MessageError> {
if let Some(idx) = self.ac_index {
dst.write_all(&idx.to_be_bytes())?;
}
Ok(())
}
fn from_frame_data(message_id: u8, data: Vec<u8>) -> Result<Self, super::MessageError> {
match data[..] {
[] => { Ok(Self { message_id, ac_index: None }) },
[ac_index] => { Ok(Self { message_id, ac_index: Some(ac_index) }) },
_ => Err(MessageError::InvalidData),
}
}
}
{
fn impl_frame_data_len(&self) -> usize {
(AC_CAP_SIZE + 2 * size_of::<u8>()) * self.acs.len()
}
fn impl_frame_data<W: std::io::Write>(&self, dst: &mut W) -> Result<(), super::MessageError> {
for (idx, ac) in self.acs.iter() {
let name_len = {
let mut i = std::cmp::min(ac.name.len(), NAME_LEN_MAX);
while !ac.name.is_char_boundary(i) {
i -= 1;
}
i
};
dst.write_all(&idx.to_be_bytes())?;
dst.write_all(&(AC_CAP_SIZE as u8).to_be_bytes())?;
dst.write_all(&ac.name.as_bytes()[..name_len])?;
dst.write_all("\0".repeat(NAME_LEN_MAX - name_len).as_bytes())?;
dst.write_all(&ac.zone_start_index.to_be_bytes())?;
dst.write_all(&ac.zone_count.to_be_bytes())?;
dst.write_all(&ac.supported_modes.bits().to_be_bytes())?;
dst.write_all(&ac.supported_fan_speeds.bits().to_be_bytes())?;
dst.write_all(&ac.setpoint_cool_min.to_be_bytes())?;
dst.write_all(&ac.setpoint_cool_max.to_be_bytes())?;
dst.write_all(&ac.setpoint_heat_min.to_be_bytes())?;
dst.write_all(&ac.setpoint_heat_max.to_be_bytes())?;
}
Ok(())
}
fn from_frame_data(message_id: u8, data: Vec<u8>) -> Result<Self, super::MessageError> {
let mut i: usize = 0;
let mut acs = BTreeMap::new();
while i < data.len() {
if data.len() < i + AC_CAP_SIZE + 2 * size_of::<u8>()
|| (data[i+1] as usize) < AC_CAP_SIZE
{
return Err(MessageError::InvalidData);
}
let idx = data[i];
let sz = data[i+1] as usize;
let name_buf = &data[i+2..i+2+NAME_LEN_MAX];
let name_len = name_buf.iter().position(|&c| c == b'\0').unwrap_or(NAME_LEN_MAX);
let zone_start_index = data[i+18];
let zone_count = data[i+19];
let supported_modes = AcModes::from_bits_retain(data[i+20]);
let supported_fan_speeds = FanSpeeds::from_bits_retain(data[i+21]);
let setpoint_cool_min = data[i+22];
let setpoint_cool_max = data[i+23];
let setpoint_heat_min = data[i+24];
let setpoint_heat_max = data[i+25];
let name = String::from_utf8_lossy(&name_buf[..name_len]).to_string();
acs.insert(idx, AcCapability {
name,
zone_start_index,
zone_count,
supported_modes,
supported_fan_speeds,
setpoint_cool_min,
setpoint_cool_max,
setpoint_heat_min,
setpoint_heat_max,
});
i += sz + 2 * size_of::<u8>();
}
Ok(Self { message_id, acs })
}
});
impl AcCapabilityRequest {
pub fn new(ac_index: Option<u8>) -> Self {
Self {
message_id: super::next_msg_id(),
ac_index,
}
}
}
impl AcCapabilityResponse {
pub fn new<K: Into<u8>, V: Into<AcCapability>, 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<AcCapability>, T: IntoIterator<Item = (K, V)>>(
message_id: u8,
acs: T,
) -> Self {
Self {
message_id,
acs: acs.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
}
}
pub fn by_index(&self) -> impl Iterator<Item = (u8, &AcCapability)> {
self.acs.iter().map(|(k, v)| (*k, v))
}
pub fn by_name(&self) -> impl Iterator<Item = (u8, &AcCapability)> {
let mut s: Vec<_> = self.acs.iter().map(|(k, v)| (*k, v)).collect();
s.sort_by_key(|(_, a)| &a.name);
Iter::new(&self.acs, s.iter().map(|(k, _)| *k).collect::<Vec<_>>())
}
}
struct Iter<'a, I: IntoIterator<Item = u8>> {
acs: &'a BTreeMap<u8, AcCapability>,
order: <I as IntoIterator>::IntoIter,
}
impl<'a, I: IntoIterator<Item = u8>> Iter<'a, I> {
fn new(acs: &'a BTreeMap<u8, AcCapability>, order: I) -> Self {
Self {
acs,
order: order.into_iter(),
}
}
}
impl<'a, I: IntoIterator<Item = u8>> Iterator for Iter<'a, I> {
type Item = (u8, &'a AcCapability);
fn next(&mut self) -> Option<Self::Item> {
self.order.next().and_then(|i| Some((i, self.acs.get(&i)?)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::extended::ExtendedMessageSubtype;
use crate::conn::tests::data::*;
static ACS: std::sync::LazyLock<[(u8, AcCapability); 3]> = std::sync::LazyLock::new(|| {
[
(
0,
AcCapability {
name: "Upstairs".to_string(),
zone_start_index: 0,
zone_count: 3,
supported_modes: AcModes::all(),
supported_fan_speeds: FanSpeeds::Low | FanSpeeds::Medium | FanSpeeds::High,
setpoint_cool_min: 16,
setpoint_cool_max: 30,
setpoint_heat_min: 16,
setpoint_heat_max: 30,
},
),
(
2,
AcCapability {
name: "Downstairs".to_string(),
zone_start_index: 0,
zone_count: 4,
supported_modes: AcModes::all(),
supported_fan_speeds: FanSpeeds::all(),
setpoint_cool_min: 18,
setpoint_cool_max: 30,
setpoint_heat_min: 16,
setpoint_heat_max: 26,
},
),
(
1,
AcCapability {
name: "Basement".to_string(),
zone_start_index: 0,
zone_count: 1,
supported_modes: AcModes::Heat | AcModes::Cool,
supported_fan_speeds: FanSpeeds::Low | FanSpeeds::High,
setpoint_cool_min: 14,
setpoint_cool_max: 24,
setpoint_heat_min: 10,
setpoint_heat_max: 28,
},
),
]
});
#[test]
fn test_by_name() {
let a = AcCapabilityResponse::new(ACS.clone());
let mut i = a.by_name();
assert_matches!(i.next(), Some((1, ac)) => { assert_eq!(ac.name, "Basement")});
assert_matches!(i.next(), Some((2, ac)) => { assert_eq!(ac.name, "Downstairs")});
assert_matches!(i.next(), Some((0, ac)) => { assert_eq!(ac.name, "Upstairs")});
assert_matches!(i.next(), None);
}
#[test]
fn test_ac_capability_request_all() {
let orig = AcCapabilityRequest::new(None);
let frame: Frame = orig.clone().try_into().expect("into frame failed");
assert_eq!(
frame.data.len(),
size_of::<super::super::extended::ExtendedMessageSubtype>()
);
let req: AcCapabilityRequest = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
}
#[test]
fn test_ac_capability_request_one() {
let orig = AcCapabilityRequest::new(Some(7));
let frame: Frame = orig.clone().try_into().expect("into frame failed");
assert_eq!(
frame.data.len(),
size_of::<ExtendedMessageSubtype>() + size_of::<u8>()
);
let req: AcCapabilityRequest = frame.try_into().expect("from frame failed");
assert_eq!(req, orig);
assert_eq!(req.ac_index, Some(7));
}
#[test]
fn test_ac_capability_response_one() {
let orig = AcCapabilityResponse::new([ACS[1].clone()]);
let key = ACS[1].0;
let frame: Frame = orig.clone().try_into().expect("into frame failed");
assert_eq!(
frame.data.len(),
size_of::<ExtendedMessageSubtype>() + 2 * size_of::<u8>() + AC_CAP_SIZE
);
let resp: AcCapabilityResponse = frame.try_into().expect("from frame failed");
assert_eq!(resp, orig);
assert_eq!(resp.acs.len(), 1);
assert_matches!(resp.acs.first_key_value(),
Some((k, v)) => {
assert_eq!(*k, key);
assert_eq!(v.name, "Downstairs");
assert_eq!(v.zone_count, 4);
}
);
}
#[test]
fn test_ac_capability_response_all() {
let orig = AcCapabilityResponse::new(ACS.clone());
let frame: Frame = orig.clone().try_into().expect("into frame failed");
assert_eq!(
frame.data.len(),
size_of::<ExtendedMessageSubtype>() + (2 * size_of::<u8>() + AC_CAP_SIZE) * ACS.len()
);
let resp: AcCapabilityResponse = frame.try_into().expect("from frame failed");
assert_eq!(resp, orig);
assert_eq!(resp.acs.len(), ACS.len());
for (idx, ac) in &ACS[..] {
assert_matches!(resp.acs.get(idx), Some(a) => {
assert_eq!(a, ac);
})
}
}
#[test]
fn test_ac_capability_req_from_data_one() {
let req: AcCapabilityRequest = frame(MSG_REQ_AC_CAP_ONE)
.try_into()
.expect("from frame failed");
assert_matches!(req.ac_index, Some(idx) => {
assert_eq!(idx, 0);
});
let f: Frame = req.try_into().expect("into frame failed");
assert_eq!(f, frame(MSG_REQ_AC_CAP_ONE));
}
#[test]
fn test_ac_capability_req_from_data_all() {
let req: AcCapabilityRequest = frame(MSG_REQ_AC_CAP_ALL)
.try_into()
.expect("from frame failed");
assert_matches!(req.ac_index, None);
let f: Frame = req.try_into().expect("into frame failed");
assert_eq!(f, frame(MSG_REQ_AC_CAP_ALL));
}
#[test]
fn test_ac_capability_resp_from_data() {
let resp: AcCapabilityResponse = frame(&decode(MSG_RESP_AC_CAP))
.try_into()
.expect("from frame failed");
let mut iter = resp.by_index();
assert_matches!(iter.next(), Some((idx, ac)) => {
assert_eq!(idx, 0);
assert_eq!(ac.name, "UUUNIT 01 UUU");
assert_eq!(ac.zone_count, 4);
assert_eq!(ac.zone_start_index, 0);
assert_eq!(ac.supported_modes,
AcModes::Heat | AcModes::Cool | AcModes::Dry | AcModes::Auto);
assert_eq!(ac.supported_fan_speeds,
FanSpeeds::Low | FanSpeeds::Medium | FanSpeeds::High | FanSpeeds::Auto);
assert_eq!((ac.setpoint_cool_min, ac.setpoint_cool_max), (16, 31));
assert_eq!((ac.setpoint_heat_min, ac.setpoint_heat_max), (18, 31));
});
assert_matches!(iter.next(), None);
drop(iter);
let f: Frame = resp.try_into().expect("into frame failed");
assert_eq!(f, frame(&decode(MSG_RESP_AC_CAP)));
}
}