huesmith 0.1.0

Hue-compatible Zigbee light library for ESP32-C6/H2 (ESP-IDF)
use super::ffi;
use huesmith_core::light::command::{
    LightCommand, DEFAULT_COLOR_TRANSITION_TENTHS, DEFAULT_LEVEL_TRANSITION_TENTHS,
};

/// Dispatch an incoming ZCL set_attribute_value message into a [`LightCommand`].
/// Returns `None` if the message is not relevant to light control.
///
/// `size` is the attribute value's declared byte length from the ZCL frame. Each
/// branch reads a fixed width (1 or 2 bytes) through a raw pointer, so it is
/// checked against `size` first: a malformed frame that declares a shorter value
/// than its attribute's type is rejected rather than read past its buffer. The
/// ZBOSS stack normally validates type/size before this callback, so this is
/// defense in depth — but the read is `unsafe` and the guard is nearly free.
pub fn parse_set_attribute(
    cluster_id: u16,
    attr_id: u16,
    size: u16,
    data: *const core::ffi::c_void,
) -> Option<LightCommand> {
    match cluster_id {
        ffi::CLUSTER_ON_OFF => match attr_id {
            ffi::ATTR_ON_OFF => {
                if size < 1 {
                    return None;
                }
                let on = unsafe { (data as *const u8).read() != 0 };
                Some(if on {
                    LightCommand::On
                } else {
                    LightCommand::Off
                })
            }
            _ => None,
        },
        ffi::CLUSTER_LEVEL_CONTROL => match attr_id {
            ffi::ATTR_LEVEL_CURRENT_LEVEL => {
                if size < 1 {
                    return None;
                }
                // Raw wire value: LightState normalizes every 0-254 ZCL domain
                // (level/hue/saturation) at its single transition entry point,
                // so 255 (reserved) can't wrap downstream scaling math.
                let level = unsafe { (data as *const u8).read() };
                Some(LightCommand::SetLevel {
                    level,
                    transition_time: DEFAULT_LEVEL_TRANSITION_TENTHS,
                })
            }
            _ => None,
        },
        ffi::CLUSTER_COLOR_CONTROL => match attr_id {
            ffi::ATTR_COLOR_CURRENT_X => {
                if size < 2 {
                    return None;
                }
                let x = unsafe { (data as *const u16).read_unaligned() };
                Some(LightCommand::MoveToColor {
                    x: Some(x),
                    y: None,
                    transition_time: DEFAULT_COLOR_TRANSITION_TENTHS,
                })
            }
            ffi::ATTR_COLOR_CURRENT_Y => {
                if size < 2 {
                    return None;
                }
                let y = unsafe { (data as *const u16).read_unaligned() };
                Some(LightCommand::MoveToColor {
                    x: None,
                    y: Some(y),
                    transition_time: DEFAULT_COLOR_TRANSITION_TENTHS,
                })
            }
            ffi::ATTR_COLOR_TEMPERATURE_MIREDS => {
                if size < 2 {
                    return None;
                }
                let mireds = unsafe { (data as *const u16).read_unaligned() };
                Some(LightCommand::MoveToColorTemp {
                    mireds,
                    transition_time: DEFAULT_COLOR_TRANSITION_TENTHS,
                })
            }
            ffi::ATTR_COLOR_CURRENT_HUE => {
                if size < 1 {
                    return None;
                }
                let hue = unsafe { (data as *const u8).read() };
                Some(LightCommand::MoveToHueAndSaturation {
                    hue: Some(hue),
                    saturation: None,
                    transition_time: DEFAULT_COLOR_TRANSITION_TENTHS,
                })
            }
            ffi::ATTR_COLOR_CURRENT_SATURATION => {
                if size < 1 {
                    return None;
                }
                let sat = unsafe { (data as *const u8).read() };
                Some(LightCommand::MoveToHueAndSaturation {
                    hue: None,
                    saturation: Some(sat),
                    transition_time: DEFAULT_COLOR_TRANSITION_TENTHS,
                })
            }
            ffi::ATTR_COLOR_ENHANCED_CURRENT_HUE => {
                if size < 2 {
                    return None;
                }
                let enhanced = unsafe { (data as *const u16).read_unaligned() };
                Some(LightCommand::EnhancedMoveToHueAndSaturation {
                    enhanced_hue: enhanced,
                    saturation: None,
                    transition_time: DEFAULT_COLOR_TRANSITION_TENTHS,
                })
            }
            ffi::ATTR_COLOR_MODE | ffi::ATTR_COLOR_ENHANCED_COLOR_MODE => None,
            _ => None,
        },
        ffi::CLUSTER_IDENTIFY => match attr_id {
            0x0000 => {
                if size < 2 {
                    return None;
                }
                let duration = unsafe { (data as *const u16).read_unaligned() };
                Some(LightCommand::Identify { duration })
            }
            _ => None,
        },
        _ => None,
    }
}