at4_protocol 2.1.1

A rust crate that parses AirTouch 4 messages.
Documentation
use crate::{messaging::group_status_message::GroupStatus, states};

use super::ReceivableMessage;

/// Parse a group status packet.
pub fn parse(bytes: &[u8]) -> Result<ReceivableMessage, &'static str> {
    let data_length: usize = (((bytes[6] as u16) << 2) | (bytes[7] as u16)) as usize;

    // Check length
    if data_length % 6 != 0 {
        return Err("invalid message data length to be a group status message.");
    }

    // Only grab the data
    let data = bytes.get(8..8 + data_length).unwrap();

    let mut groups: Vec<GroupStatus> = Vec::with_capacity(data_length / 6);

    for g in data.chunks(6) {
        let power_state = match g[0] >> 6 {
            0 => states::GroupPowerState::Off,
            1 => states::GroupPowerState::On,
            2 => states::GroupPowerState::Turbo,
            _ => return Err("invalid value for `power_state`."),
        };

        let group_number = g[0] & 0b00111111;

        let control_method = match g[1] >> 7 {
            0 => states::GroupControlMethod::Percentage,
            1 => states::GroupControlMethod::Temperature,
            _ => unreachable!(),
        };

        let open_percentage = g[1] & 0b01111111;

        let battery_low: bool = (g[2] >> 7) == 1;
        let turbo_support: bool = ((g[2] >> 6) & 1) == 1;
        let target_setpoint = g[2] & 0b00111111;

        let has_sensor = (g[3] >> 7) == 1;

        let current_temperature: Option<f64> = {
            if g[4] == 0xff {
                None
            } else {
                let g4 = g[4] as u16;
                let g5 = g[5] as u16;

                let num = (g4 << 3) | (g5 >> 5);

                Some(((num as f64) - 500.00) / 10.00)
            }
        };

        let spill = ((g[5] >> 4) & 0b1) == 1;

        groups.push(GroupStatus {
            battery_low,
            control_method,
            current_temperature,
            group_number,
            has_sensor,
            spill,
            turbo_support,
            open_percentage,
            target_setpoint,
            power_state,
        })
    }

    Ok(ReceivableMessage::GroupStatuses(groups))
}

#[cfg(test)]
mod test {
    use crate::{messaging::group_status_message::GroupStatus, states};

    use super::parse;

    #[test]
    fn test_parses_valid() {
        let groups = parse(&[
            0x55, 0x55, 0xb0, 0x80, 0x01, 0x2b, 0x00, 0x0c, 0x40, 0x64, 0x00, 0x00, 0xff, 0x00,
            0x41, 0xe4, 0x1a, 0x80, 0x61, 0x80, 0x65, 0x79,
        ])
        .unwrap();

        match groups {
            crate::messaging::ReceivableMessage::GroupStatuses(m) => {
                let valid = vec![
                    GroupStatus {
                        power_state: states::GroupPowerState::On,
                        group_number: 0,
                        control_method: states::GroupControlMethod::Percentage,
                        open_percentage: 100,
                        battery_low: false,
                        turbo_support: false,
                        target_setpoint: 0,
                        has_sensor: false,
                        current_temperature: None,
                        spill: false,
                    },
                    GroupStatus {
                        power_state: states::GroupPowerState::On,
                        group_number: 1,
                        control_method: states::GroupControlMethod::Temperature,
                        open_percentage: 100,
                        battery_low: false,
                        turbo_support: false,
                        target_setpoint: 26,
                        has_sensor: true,
                        current_temperature: Some(28.0),
                        spill: false,
                    },
                ];

                m.iter()
                    .zip(valid.iter())
                    .for_each(|(a, b)| assert_eq!(a, b));
            }
            _ => panic!("incorrect message type returned."),
        }
    }
}