huesmith 0.1.0

Hue-compatible Zigbee light library for ESP32-C6/H2 (ESP-IDF)
use super::ffi;
use huesmith_core::hue::device::{
    COLOR_CAP_COLOR_LOOP, COLOR_CAP_COLOR_TEMP, COLOR_CAP_ENHANCED_HUE,
};
use huesmith_core::hue::identity::DeviceIdentity;

pub fn create_basic_cluster(identity: &DeviceIdentity) -> *mut ffi::esp_zb_attribute_list_t {
    let cfg = ffi::esp_zb_basic_cluster_cfg_t {
        zcl_version: 0x03,
        power_source: ffi::POWER_SOURCE_MAINS,
    };
    let cluster = unsafe { ffi::esp_zb_basic_cluster_create(&cfg) };

    let manuf = zcl_string(identity.manufacturer_name, ZCL_BASIC_NAME_MAX);
    let model = zcl_string(identity.model_identifier, ZCL_BASIC_NAME_MAX);
    let date = zcl_string(identity.date_code, ZCL_BASIC_CODE_MAX);
    let sw_build = zcl_string(identity.sw_build_id, ZCL_BASIC_CODE_MAX);
    // HWVersion is an *optional* Basic attribute that esp_zb_basic_cluster_create
    // does not add, so it must be registered explicitly: a plain update_attr on it
    // returns ESP_ERR_NOT_FOUND and the value never reaches the Bridge. Box::leak'd
    // because ZBOSS keeps the raw pointer for the device's lifetime.
    let hw_ver: &'static mut u8 = Box::leak(Box::new(identity.hw_version));

    unsafe {
        ffi::esp_zb_basic_cluster_add_attr(
            cluster,
            ffi::ATTR_BASIC_MANUFACTURER_NAME,
            manuf.as_ptr() as *mut _,
        );
        ffi::esp_zb_basic_cluster_add_attr(
            cluster,
            ffi::ATTR_BASIC_MODEL_IDENTIFIER,
            model.as_ptr() as *mut _,
        );
        ffi::esp_zb_basic_cluster_add_attr(
            cluster,
            ffi::ATTR_BASIC_DATE_CODE,
            date.as_ptr() as *mut _,
        );
        ffi::esp_zb_basic_cluster_add_attr(
            cluster,
            ffi::ATTR_BASIC_SW_BUILD_ID,
            sw_build.as_ptr() as *mut _,
        );

        let r1 = ffi::esp_zb_cluster_update_attr(
            cluster,
            ffi::ATTR_BASIC_MANUFACTURER_NAME,
            manuf.as_ptr() as *mut _,
        );
        let r2 = ffi::esp_zb_cluster_update_attr(
            cluster,
            ffi::ATTR_BASIC_MODEL_IDENTIFIER,
            model.as_ptr() as *mut _,
        );
        let r3 = ffi::esp_zb_cluster_add_attr(
            cluster,
            ffi::CLUSTER_BASIC,
            ffi::ATTR_BASIC_HW_VERSION,
            ffi::ZCL_ATTR_TYPE_U8,
            ffi::ZB_ZCL_ATTR_ACCESS_READ_ONLY,
            hw_ver as *mut u8 as *mut core::ffi::c_void,
        );

        log::info!(
            "Basic cluster: {} {} (hw v{}) [manuf/model update: {},{}; hw add: {}]",
            identity.manufacturer_name,
            identity.model_identifier,
            identity.hw_version,
            r1,
            r2,
            r3,
        );
    }

    cluster
}

pub fn create_identify_cluster() -> *mut ffi::esp_zb_attribute_list_t {
    let cfg = ffi::esp_zb_identify_cluster_cfg_t { identify_time: 0 };
    unsafe { ffi::esp_zb_identify_cluster_create(&cfg) }
}

pub fn create_groups_cluster() -> *mut ffi::esp_zb_attribute_list_t {
    let cfg = ffi::esp_zb_groups_cluster_cfg_t {
        groups_name_support_id: 0,
    };
    unsafe { ffi::esp_zb_groups_cluster_create(&cfg) }
}

pub fn create_scenes_cluster() -> *mut ffi::esp_zb_attribute_list_t {
    let cfg = ffi::esp_zb_scenes_cluster_cfg_t {
        scenes_count: 0,
        current_scene: 0,
        current_group: 0,
        scene_valid: false,
        name_support: 0,
    };
    unsafe { ffi::esp_zb_scenes_cluster_create(&cfg) }
}

pub fn create_on_off_cluster(initial_on: bool) -> *mut ffi::esp_zb_attribute_list_t {
    let cfg = ffi::esp_zb_on_off_cluster_cfg_t { on_off: initial_on };
    let cluster = unsafe { ffi::esp_zb_on_off_cluster_create(&cfg) };

    let global_scene = Box::leak(Box::new(true));
    let on_time = Box::leak(Box::new(0u16));
    let off_wait = Box::leak(Box::new(0u16));
    let startup = Box::leak(Box::new(if initial_on { 0x01u8 } else { 0x00u8 }));

    unsafe {
        ffi::esp_zb_cluster_add_attr(
            cluster,
            ffi::CLUSTER_ON_OFF,
            ffi::ATTR_ON_OFF_GLOBAL_SCENE_CONTROL,
            ffi::ZCL_ATTR_TYPE_BOOL,
            ffi::ZB_ZCL_ATTR_ACCESS_READ_ONLY,
            global_scene as *mut bool as *mut _,
        );
        ffi::esp_zb_cluster_add_attr(
            cluster,
            ffi::CLUSTER_ON_OFF,
            ffi::ATTR_ON_OFF_ON_TIME,
            ffi::ZCL_ATTR_TYPE_U16,
            ffi::ZB_ZCL_ATTR_ACCESS_READ_WRITE,
            on_time as *mut u16 as *mut _,
        );
        ffi::esp_zb_cluster_add_attr(
            cluster,
            ffi::CLUSTER_ON_OFF,
            ffi::ATTR_ON_OFF_OFF_WAIT_TIME,
            ffi::ZCL_ATTR_TYPE_U16,
            ffi::ZB_ZCL_ATTR_ACCESS_READ_WRITE,
            off_wait as *mut u16 as *mut _,
        );
        ffi::esp_zb_cluster_add_attr(
            cluster,
            ffi::CLUSTER_ON_OFF,
            ffi::ATTR_ON_OFF_START_UP_ON_OFF,
            ffi::ZCL_ATTR_TYPE_ENUM8,
            ffi::ZB_ZCL_ATTR_ACCESS_READ_WRITE,
            startup as *mut u8 as *mut _,
        );
    }

    cluster
}

pub fn create_level_cluster(initial_level: u8) -> *mut ffi::esp_zb_attribute_list_t {
    let cfg = ffi::esp_zb_level_cluster_cfg_t {
        current_level: initial_level,
    };
    let cluster = unsafe { ffi::esp_zb_level_cluster_create(&cfg) };

    let startup_level = Box::leak(Box::new(initial_level));
    let options = Box::leak(Box::new(0u8));

    unsafe {
        ffi::esp_zb_cluster_add_attr(
            cluster,
            ffi::CLUSTER_LEVEL_CONTROL,
            ffi::ATTR_LEVEL_START_UP_CURRENT_LEVEL,
            ffi::ZCL_ATTR_TYPE_U8,
            ffi::ZB_ZCL_ATTR_ACCESS_READ_WRITE,
            startup_level as *mut u8 as *mut _,
        );
        ffi::esp_zb_cluster_add_attr(
            cluster,
            ffi::CLUSTER_LEVEL_CONTROL,
            ffi::ATTR_LEVEL_OPTIONS,
            ffi::ZCL_ATTR_TYPE_U8,
            ffi::ZB_ZCL_ATTR_ACCESS_READ_WRITE,
            options as *mut u8 as *mut _,
        );
    }

    cluster
}

pub fn create_color_control_cluster(
    capabilities: u16,
    min_mireds: u16,
    max_mireds: u16,
) -> *mut ffi::esp_zb_attribute_list_t {
    let cfg = ffi::esp_zb_color_control_cluster_cfg_t {
        current_x: 20495,
        current_y: 21561,
        color_mode: ffi::COLOR_MODE_COLOR_TEMP,
        options: 0,
        enhanced_color_mode: ffi::ENHANCED_COLOR_MODE_COLOR_TEMP,
        color_capabilities: capabilities,
    };
    let cluster = unsafe { ffi::esp_zb_color_control_cluster_create(&cfg) };

    let rw = ffi::ZB_ZCL_ATTR_ACCESS_READ_WRITE;
    let ro = ffi::ZB_ZCL_ATTR_ACCESS_READ_ONLY;
    let ror = ro | ffi::ZB_ZCL_ATTR_ACCESS_REPORTING;
    let u8_t = ffi::ZCL_ATTR_TYPE_U8;
    let u16_t = ffi::ZCL_ATTR_TYPE_U16;

    // Helper to reduce repetition — only valid while `cluster` lives.
    let add = |attr_id: u16, attr_type: u8, access: u8, ptr: *mut u8| unsafe {
        ffi::esp_zb_cluster_add_attr(
            cluster,
            ffi::CLUSTER_COLOR_CONTROL,
            attr_id,
            attr_type,
            access,
            ptr as *mut core::ffi::c_void,
        );
    };

    // Color temperature — only for devices advertising CCT capability (bit 4).
    if capabilities & COLOR_CAP_COLOR_TEMP != 0 {
        let color_temp = Box::leak(Box::new(370u16));
        let min_m = Box::leak(Box::new(min_mireds));
        let max_m = Box::leak(Box::new(max_mireds));
        let startup_ct = Box::leak(Box::new(0xFFFFu16));
        let couple_ct_min = Box::leak(Box::new(min_mireds));
        add(
            ffi::ATTR_COLOR_TEMPERATURE_MIREDS,
            u16_t,
            ror,
            color_temp as *mut u16 as *mut u8,
        );
        add(
            ffi::ATTR_COLOR_TEMP_PHYSICAL_MIN_MIREDS,
            u16_t,
            ro,
            min_m as *mut u16 as *mut u8,
        );
        add(
            ffi::ATTR_COLOR_TEMP_PHYSICAL_MAX_MIREDS,
            u16_t,
            ro,
            max_m as *mut u16 as *mut u8,
        );
        add(
            ffi::ATTR_COLOR_START_UP_COLOR_TEMPERATURE_MIREDS,
            u16_t,
            rw,
            startup_ct as *mut u16 as *mut u8,
        );
        add(
            ffi::ATTR_COLOR_COUPLE_COLOR_TEMP_TO_LEVEL_MIN_MIREDS,
            u16_t,
            ro,
            couple_ct_min as *mut u16 as *mut u8,
        );
    }

    // Enhanced hue / hue-saturation — only for devices advertising enhanced hue (bit 1).
    if capabilities & COLOR_CAP_ENHANCED_HUE != 0 {
        let enhanced_hue = Box::leak(Box::new(0u16));
        let loop_start_hue = Box::leak(Box::new(0u16));
        let loop_stored_hue = Box::leak(Box::new(0u16));
        add(
            ffi::ATTR_COLOR_ENHANCED_CURRENT_HUE,
            u16_t,
            ror,
            enhanced_hue as *mut u16 as *mut u8,
        );
        add(
            ffi::ATTR_COLOR_LOOP_START_ENHANCED_HUE,
            u16_t,
            ro,
            loop_start_hue as *mut u16 as *mut u8,
        );
        add(
            ffi::ATTR_COLOR_LOOP_STORED_ENHANCED_HUE,
            u16_t,
            ro,
            loop_stored_hue as *mut u16 as *mut u8,
        );
    }

    // Color loop — only for devices advertising color loop (bit 2).
    // Requires implementing the ColorLoopSet ZCL command handler if advertised.
    if capabilities & COLOR_CAP_COLOR_LOOP != 0 {
        let loop_active = Box::leak(Box::new(0u8));
        let loop_direction = Box::leak(Box::new(0u8));
        let loop_time = Box::leak(Box::new(25u16));
        add(
            ffi::ATTR_COLOR_LOOP_ACTIVE,
            u8_t,
            ror,
            loop_active as *mut u8,
        );
        // ZCL r7 §5.2.2.2.3.15/16: loop direction and time are read-write
        add(
            ffi::ATTR_COLOR_LOOP_DIRECTION,
            u8_t,
            rw,
            loop_direction as *mut u8,
        );
        add(
            ffi::ATTR_COLOR_LOOP_TIME,
            u16_t,
            rw,
            loop_time as *mut u16 as *mut u8,
        );
    }

    cluster
}

/// ZCL Basic cluster caps ManufacturerName and ModelIdentifier at 32 bytes.
const ZCL_BASIC_NAME_MAX: usize = 32;
/// ZCL Basic cluster caps DateCode and SWBuildID at 16 bytes.
const ZCL_BASIC_CODE_MAX: usize = 16;

/// Encode a ZCL character string (length-prefixed octets), truncated to the
/// attribute's spec limit. ZCL strings are octet sequences, so truncation is
/// byte-wise; identity strings are expected to be ASCII.
fn zcl_string(s: &'static str, max: usize) -> &'static [u8] {
    let bytes = s.as_bytes();
    if bytes.len() > max {
        log::warn!("Basic cluster string {s:?} exceeds the ZCL {max}-byte limit; truncating");
    }
    let len = bytes.len().min(max);
    let mut buf = Vec::with_capacity(1 + len);
    buf.push(len as u8);
    buf.extend_from_slice(&bytes[..len]);
    buf.leak()
}