at4_protocol 2.1.1

A rust crate that parses AirTouch 4 messages.
Documentation
use crc16::MODBUS;

use crate::{messaging::MessageType, states};

/// The AC control message. Send this message to the AT4 unit to change the
/// state of an AC unit.
#[derive(Debug, Clone, Copy)]
pub struct ACControlMessage {
    /// The number of the AC unit `(0-3)`.
    pub unit_number: u8,

    /// The fan speed of the AC unit.
    ///
    /// If `None`, the fan speed will be maintained.
    pub fan_speed: Option<states::ACFanSpeed>,

    /// The operating mode of the AC unit. (e.g. `auto`, `cool`, `dry`, `fan`)
    pub mode: states::SetACMode,

    /// The change in power state of the AC unit.
    pub power: states::SetACPowerState,

    /// The temperature of the air that should be generated by the AC unit.
    pub setpoint_temp: states::SetACSetpoint,
}

impl ACControlMessage {
    /// `None` fields will maintain their current values.
    pub fn new(
        unit_number: u8,
        power: states::SetACPowerState,
        mode: states::SetACMode,
        fan_speed: Option<states::ACFanSpeed>,
        setpoint_temp: states::SetACSetpoint,
    ) -> Self {
        Self {
            unit_number,
            fan_speed,
            mode,
            power,
            setpoint_temp,
        }
    }

    /// Create an `ACControlMessage` that will turn off the specified AC unit.
    pub fn set_off(unit: u8) -> Self {
        Self::new(
            unit,
            states::SetACPowerState::Off,
            states::SetACMode::Maintain,
            None,
            states::SetACSetpoint::Maintain,
        )
    }

    /// Convert the `ACControlMessage` into the raw message bytes.
    pub fn build(&self) -> [u8; 14] {
        assert!(self.unit_number <= 3);

        let power: u8 = match self.power {
            states::SetACPowerState::Maintain => 0,
            states::SetACPowerState::Toggle => 1,
            states::SetACPowerState::Off => 0b10,
            states::SetACPowerState::On => 0b11,
        };

        let byte1 = (power << 6) | self.unit_number;

        let ac_mode: u8 = match self.mode {
            states::SetACMode::Auto => 0,
            states::SetACMode::Heat => 0b1,
            states::SetACMode::Dry => 0b10,
            states::SetACMode::Fan => 0b11,
            states::SetACMode::Cool => 0b100,
            states::SetACMode::Maintain => 0b1111,
        };

        let fan_speed: u8 = match self.fan_speed {
            Some(m) => match m {
                states::ACFanSpeed::Auto => 0,
                states::ACFanSpeed::Quiet => 0b1,
                states::ACFanSpeed::Low => 0b10,
                states::ACFanSpeed::Medium => 0b11,
                states::ACFanSpeed::High => 0b100,
                states::ACFanSpeed::Powerful => 0b101,
                states::ACFanSpeed::Turbo => 0b110,
            },
            None => 0b1111,
        };

        let byte2 = (ac_mode << 4) | fan_speed;

        let mut setpoint_value: u8 = 0;
        let setpoint_control_type = match self.setpoint_temp {
            states::SetACSetpoint::Maintain => 0,
            states::SetACSetpoint::Value(v) => {
                setpoint_value = v;
                0b01
            }
            states::SetACSetpoint::Decrease => 0b10,
            states::SetACSetpoint::Increase => 0b11,
        };

        let byte3 = (setpoint_control_type << 6) | setpoint_value;

        let crc = crc16::State::<MODBUS>::calculate(&[
            0x80,
            0xb0,
            0x01,
            MessageType::AC_CONTROL_MESSAGE,
            0x00,
            0x04,
            byte1,
            byte2,
            byte3,
            0,
        ]);

        return [
            0x55,
            0x55,
            0x80,
            0xb0,
            0x01,
            MessageType::AC_CONTROL_MESSAGE,
            0x00,
            0x04,
            byte1,
            byte2,
            byte3,
            0,
            (crc >> 8) as u8,
            (crc & 0xff) as u8,
        ];
    }
}

#[cfg(test)]
mod test {
    use super::ACControlMessage;
    use crate::states;

    #[test]
    fn test_create_ac_control_packet() {
        let packet = ACControlMessage::new(
            1,
            states::SetACPowerState::Off,
            states::SetACMode::Maintain,
            None,
            states::SetACSetpoint::Maintain,
        )
        .build();

        assert_eq!(
            packet,
            [0x55, 0x55, 0x80, 0xb0, 0x01, 0x2c, 0x00, 0x04, 0x81, 0xff, 0x00, 0x00, 0xea, 0x87]
        )
    }
}