airtouch5 0.2.0

A library for communicating with AirTouch 5 air conditioning system control consoles
Documentation
//! Zone names extended message.
//!
//! See §4.b.iii.

use std::collections::BTreeMap;

use super::extended::{extended_message, ExtendedMessage, ExtendedMessageSubtype};

const SUBTYPE_ZONE_NAME: ExtendedMessageSubtype = 0xff13;

extended_message!(SUBTYPE_ZONE_NAME,
pub struct ZoneNameRequest {
    /// The index of the zone to retrieve the name of, or `None` to retrieve
    /// the names of all zones.
    pub zone_index: Option<u8>,
}
pub struct ZoneNameResponse {
    /// Map of zone index to zone name.
    pub zones: BTreeMap<u8, String>,
}
{
    fn impl_frame_data_len(&self) -> usize {
        if self.zone_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.zone_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, zone_index: None }) },
            [zone_index] => { Ok(Self { message_id, zone_index: Some(zone_index) }) },
            _ => Err(MessageError::InvalidData),
        }
    }
}
{
    fn impl_frame_data_len(&self) -> usize {
        self.zones.values().fold(self.zones.len() * size_of::<u8>() * 2, |a, z| a + z.len())
    }

    fn impl_frame_data<W: std::io::Write>(&self, dst: &mut W) -> Result<(), super::MessageError> {
        for (idx, name) in self.zones.iter() {
            dst.write_all(&idx.to_be_bytes())?;
            dst.write_all(&(name.len() as u8).to_be_bytes())?;
            dst.write_all(name.as_bytes())?;
        }
        Ok(())
    }

    fn from_frame_data(message_id: u8, data: Vec<u8>) -> Result<Self, super::MessageError> {
        let mut i: usize = 0;
        let mut zones = BTreeMap::new();
        while i < data.len() {
            if data.len() < i + 2 || data.len() < i + 2 + data[i+1] as usize {
                return Err(MessageError::InvalidData);
            }
            let l = data[i+1] as usize;
            zones.insert(data[i], match core::str::from_utf8(&data[i+2..i+2+l]) {
                Ok(valid) => valid,
                Err(err) =>
                    // the name is sometimes truncated in the middle of a utf-8 character, see
                    // [crate-level docs](index.html#bugs-zone-name-encoding)
                    // SAFETY: from_utf8() already validated this range
                    unsafe { core::str::from_utf8_unchecked(&data[i+2..i+2+err.valid_up_to()]) }
            }.to_string());
            i += 2 + l;
        }
        Ok(Self { message_id, zones })
    }
});

impl ZoneNameRequest {
    /// Construct a `ZoneNameRequest` for the given zone index, or `None` to
    /// request all zone names.
    pub fn new(zone_index: Option<u8>) -> Self {
        Self {
            message_id: super::next_msg_id(),
            zone_index,
        }
    }
}

impl ZoneNameResponse {
    /// Construct a `ZoneNameResponse` for the given zone name data.
    ///
    /// `zones` is an iterator of `(zone_index, zone name)` pairs.
    pub fn new<K: Into<u8>, V: Into<String>, T: IntoIterator<Item = (K, V)>>(zones: T) -> Self {
        Self::with_message_id(super::next_msg_id(), zones)
    }

    /// Construct a `ZoneNameResponse` for the given zone name data, with the
    /// given `message_id`.
    ///
    /// `zones` is an iterator of `(zone_index, zone name)` pairs.
    pub fn with_message_id<K: Into<u8>, V: Into<String>, T: IntoIterator<Item = (K, V)>>(
        message_id: u8,
        zones: T,
    ) -> Self {
        Self {
            message_id,
            zones: zones
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
        }
    }

    /// Iterate the zone names of this response in zone index order.
    pub fn by_index(&self) -> impl Iterator<Item = (u8, &str)> {
        self.zones.iter().map(|(k, v)| (*k, v.as_str()))
    }

    /// Iterate the zone names of this response in zone name order.
    pub fn by_name(&self) -> impl Iterator<Item = (u8, &str)> {
        let mut s: Vec<_> = self.zones.iter().map(|(k, v)| (*k, v.as_str())).collect();
        // TODO: this is lexical ordering, implement proper unicode-alphabetical ordering
        s.sort_by_key(|(_, n)| *n);
        Iter::new(&self.zones, s.iter().map(|(k, _)| *k).collect::<Vec<_>>())
    }
}

/// Iterator newtype for ordered iteration of zone names (currently only
/// for `ZoneNameResponse::by_name()`).
struct Iter<'a, I: IntoIterator<Item = u8>> {
    zones: &'a BTreeMap<u8, String>,
    order: <I as IntoIterator>::IntoIter,
}
impl<'a, I: IntoIterator<Item = u8>> Iter<'a, I> {
    fn new(zones: &'a BTreeMap<u8, String>, order: I) -> Self {
        Self {
            zones,
            order: order.into_iter(),
        }
    }
}
impl<'a, I: IntoIterator<Item = u8>> Iterator for Iter<'a, I> {
    type Item = (u8, &'a str);

    fn next(&mut self) -> Option<Self::Item> {
        self.order
            .next()
            .and_then(|i| Some((i, self.zones.get(&i)?.as_str())))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use super::super::extended::ExtendedMessageSubtype;
    use crate::conn::tests::data::*;

    const ZONE_NAMES: [(u8, &str); 3] = [(0, "Living"), (2, "Bedroom"), (1, "Kitchen")];

    #[test]
    fn test_by_name() {
        let z = ZoneNameResponse::new(ZONE_NAMES);
        let mut i = z.by_name();
        assert_eq!(i.next(), Some((2, "Bedroom")));
        assert_eq!(i.next(), Some((1, "Kitchen")));
        assert_eq!(i.next(), Some((0, "Living")));
        assert_eq!(i.next(), None);
    }

    #[test]
    fn test_zone_name_request_all() {
        let orig = ZoneNameRequest::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: ZoneNameRequest = frame.try_into().expect("from frame failed");
        assert_eq!(req, orig);
    }

    #[test]
    fn test_zone_name_request_one() {
        let orig = ZoneNameRequest::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: ZoneNameRequest = frame.try_into().expect("from frame failed");
        assert_eq!(req, orig);
        assert_eq!(req.zone_index, Some(7));
    }

    #[test]
    fn test_zone_name_response_one() {
        let orig = ZoneNameResponse::new([ZONE_NAMES[1]]);
        let key = ZONE_NAMES[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>() + orig.zones[&key].len()
        );
        let resp: ZoneNameResponse = frame.try_into().expect("from frame failed");
        assert_eq!(resp, orig);
        assert_eq!(resp.zones.len(), 1);
        assert_matches!(resp.zones.first_key_value(),
            Some((k, v)) => {
                assert_eq!(*k, key);
                assert_eq!(*v, "Bedroom");
            }
        );
    }

    #[test]
    fn test_zone_name_response_all() {
        let orig = ZoneNameResponse::new(ZONE_NAMES);
        let frame: Frame = orig.clone().try_into().expect("into frame failed");
        assert_eq!(
            frame.data.len(),
            ZONE_NAMES
                .iter()
                .fold(size_of::<ExtendedMessageSubtype>(), |a, (_, name)| a
                    + name.len()
                    + 2 * size_of::<u8>())
        );
        let resp: ZoneNameResponse = frame.try_into().expect("from frame failed");
        assert_eq!(resp, orig);
        assert_eq!(resp.zones.len(), ZONE_NAMES.len());
        for (idx, name) in ZONE_NAMES {
            assert_matches!(resp.zones.get(&idx), Some(n) => {
                assert_eq!(n, name);
            })
        }
    }

    #[test]
    fn test_zone_name_req_from_data_one() {
        let req: ZoneNameRequest = frame(MSG_REQ_ZONE_NAME_ONE)
            .try_into()
            .expect("from frame failed");
        assert_matches!(req.zone_index, Some(idx) => {
            assert_eq!(idx, 0);
        });
        let f: Frame = req.try_into().expect("into frame failed");
        assert_eq!(f, frame(MSG_REQ_ZONE_NAME_ONE));
    }

    #[test]
    fn test_zone_name_req_from_data_all() {
        let req: ZoneNameRequest = frame(MSG_REQ_ZONE_NAME_ALL)
            .try_into()
            .expect("from frame failed");
        assert_matches!(req.zone_index, None);
        let f: Frame = req.try_into().expect("into frame failed");
        assert_eq!(f, frame(MSG_REQ_ZONE_NAME_ALL));
    }

    #[test]
    fn test_zone_name_resp_from_data_one() {
        let resp: ZoneNameResponse = frame(MSG_RESP_ZONE_NAME_ONE)
            .try_into()
            .expect("from frame failed");
        let mut iter = resp.by_index();
        assert_matches!(iter.next(), Some((idx, name)) => {
            assert_eq!(idx, 0);
            assert_eq!(name, "Living")
        });
        assert_matches!(iter.next(), None);
        drop(iter);
        let f: Frame = resp.try_into().expect("into frame failed");
        assert_eq!(f, frame(MSG_RESP_ZONE_NAME_ONE));
    }

    #[test]
    fn test_zone_name_resp_from_data_all() {
        let resp: ZoneNameResponse = frame(MSG_RESP_ZONE_NAME_ALL)
            .try_into()
            .expect("from frame failed");
        let mut iter = resp.by_index();
        assert_matches!(iter.next(), Some((idx, name)) => {
            assert_eq!(idx, 0);
            assert_eq!(name, "Living")
        });
        assert_matches!(iter.next(), Some((idx, name)) => {
            assert_eq!(idx, 1);
            assert_eq!(name, "Kitchen")
        });
        assert_matches!(iter.next(), Some((idx, name)) => {
            assert_eq!(idx, 2);
            assert_eq!(name, "Bedroom")
        });
        assert_matches!(iter.next(), None);
        drop(iter);
        let f: Frame = resp.try_into().expect("into frame failed");
        assert_eq!(f, frame(MSG_RESP_ZONE_NAME_ALL));
    }

    // FIXME: tests with truncated utf-8-bugged names
    #[test]
    fn test_zone_name_resp_truncated_mid() {
        // When the name contains a character whose utf-8 encoding is
        // multi-byte, the console appears to erroneously truncate the
        // name (by the difference between the number of bytes and the
        // number of *characters*)
        let data = [
            &MSG_RESP_ZONE_NAME_ONE[..14], // original header etc
            &"Ƚǐving".as_bytes()[..6],     // incorrectly truncated data
            &[0x29, 0x7d],                 // CRC
        ]
        .concat();

        let resp: ZoneNameResponse = frame(&data).try_into().expect("from frame failed");
        let mut iter = resp.by_index();
        assert_matches!(iter.next(), Some((idx, name)) => {
            assert_eq!(idx, 0);
            assert_eq!(name, "Ƚǐvi") // expected to be truncated
        });
        assert_matches!(iter.next(), None);
    }

    #[test]
    fn test_zone_name_resp_truncated_end() {
        // ...and when the multibyte character is at the end of the string,
        // it will be trunacted in mid-utf-8-sequence, giving an entirely
        // invalid utf-8 string
        let data = [
            &MSG_RESP_ZONE_NAME_ONE[..14], // original header etc
            &"Livinǵ".as_bytes()[..6],     // incorrectly truncated data
            &[0xce, 0x2f],                 // CRC
        ]
        .concat();

        let resp: ZoneNameResponse = frame(&data).try_into().expect("from frame failed");
        let mut iter = resp.by_index();
        assert_matches!(iter.next(), Some((idx, name)) => {
            assert_eq!(idx, 0);
            assert_eq!(name, "Livin") // expected to be truncated
        });
        assert_matches!(iter.next(), None);
    }
}