huesmith 0.1.0

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

/// Builds and registers a Zigbee endpoint with all clusters for a Hue-compatible light.
pub struct EndpointBuilder {
    endpoint_id: u8,
    profile_id: u16,
    device_type: HueDeviceType,
    identity: &'static DeviceIdentity,
    initial_on: bool,
    initial_level: u8,
}

impl EndpointBuilder {
    pub fn new(
        endpoint_id: u8,
        device_type: HueDeviceType,
        identity: &'static DeviceIdentity,
    ) -> Self {
        Self {
            endpoint_id,
            profile_id: PROFILE_HA,
            device_type,
            identity,
            initial_on: false,
            initial_level: 254,
        }
    }

    pub fn profile_id(mut self, id: u16) -> Self {
        self.profile_id = id;
        self
    }

    pub fn initial_on(mut self, on: bool) -> Self {
        self.initial_on = on;
        self
    }

    pub fn initial_level(mut self, level: u8) -> Self {
        self.initial_level = level;
        self
    }

    pub fn register(self, ep_list: *mut ffi::esp_zb_ep_list_t) {
        let cluster_list = unsafe { ffi::esp_zb_zcl_cluster_list_create() };

        let basic = cluster::create_basic_cluster(self.identity);
        unsafe {
            ffi::esp_zb_cluster_list_add_basic_cluster(
                cluster_list,
                basic,
                ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
            );
        }

        let identify = cluster::create_identify_cluster();
        unsafe {
            ffi::esp_zb_cluster_list_add_identify_cluster(
                cluster_list,
                identify,
                ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
            );
        }

        let groups = cluster::create_groups_cluster();
        unsafe {
            ffi::esp_zb_cluster_list_add_groups_cluster(
                cluster_list,
                groups,
                ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
            );
        }

        let scenes = cluster::create_scenes_cluster();
        unsafe {
            ffi::esp_zb_cluster_list_add_scenes_cluster(
                cluster_list,
                scenes,
                ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
            );
        }

        let on_off = cluster::create_on_off_cluster(self.initial_on);
        unsafe {
            ffi::esp_zb_cluster_list_add_on_off_cluster(
                cluster_list,
                on_off,
                ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
            );
        }

        if self.device_type.has_level_control() {
            let level = cluster::create_level_cluster(self.initial_level);
            unsafe {
                ffi::esp_zb_cluster_list_add_level_cluster(
                    cluster_list,
                    level,
                    ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
                );
            }
        }

        if self.device_type.has_color_control() {
            let (caps, min_m, max_m) = self.device_type.color_params();
            let color = cluster::create_color_control_cluster(caps, min_m, max_m);
            unsafe {
                ffi::esp_zb_cluster_list_add_color_control_cluster(
                    cluster_list,
                    color,
                    ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
                );
            }
        }

        // ZLL Commissioning (0x1000) — server with no attributes.
        // Real Hue bulbs include this in their Simple Descriptor for Touchlink
        // factory-reset compatibility. No commands are handled; the cluster
        // exists only for Bridge discovery.
        let zll = unsafe { ffi::esp_zb_touchlink_commissioning_cluster_create(core::ptr::null()) };
        unsafe {
            ffi::esp_zb_cluster_list_add_touchlink_commissioning_cluster(
                cluster_list,
                zll,
                ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
            );
        }

        // OTA Upgrade (0x0019) — client (out-cluster).
        // Advertising this cluster causes the Bridge to show firmware update
        // status and send OTA Image Notify commands. Actual OTA download is
        // not implemented; the cluster is present for Bridge compatibility only.
        //
        // NOTE: do NOT present as a genuine Hue model (Signify OUI + real model
        // id) with this cluster. The Bridge will then immediately push a real
        // firmware update, and ZBOSS's OTA client handler asserts
        // (zcl_ota_upgrade_commands.c) because there is no OTA backend, crashing
        // the device right after it joins. With a generic identity the Bridge
        // does not push firmware, so this stays a harmless visibility-only cluster.
        let ota_cfg = ffi::esp_zb_ota_cluster_cfg_t {
            ota_upgrade_file_version: 0x0000_0001,
            ota_upgrade_manufacturer: ffi::MANUF_CODE_SIGNIFY,
            ota_upgrade_image_type: 0xFFFF, // match-any
            ota_min_block_reque: 0,
            ota_upgrade_file_offset: 0,
            ota_upgrade_downloaded_file_ver: 0,
            ota_upgrade_server_id: [0xFF; 8], // broadcast = no server known yet
            ota_image_upgrade_status: 0,      // Normal
        };
        let ota = unsafe { ffi::esp_zb_ota_cluster_create(&ota_cfg) };
        unsafe {
            ffi::esp_zb_cluster_list_add_ota_cluster(
                cluster_list,
                ota,
                ffi::ZB_ZCL_CLUSTER_CLIENT_ROLE,
            );
        }

        // Signify 0xFC01 — empty manufacturer-specific server cluster.
        // The Hue Bridge sends an init command (cmd 0x03) to this cluster during
        // device discovery. Advertising it prevents "unexpected cluster" errors.
        // Commands to it receive ZCL Default Response "unsupported command",
        // which the Bridge handles gracefully.
        let fc01 = unsafe { ffi::esp_zb_zcl_attr_list_create(0xFC01) };
        unsafe {
            ffi::esp_zb_cluster_list_add_custom_cluster(
                cluster_list,
                fc01,
                ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
            );
        }

        let ep_cfg = ffi::esp_zb_endpoint_config_t {
            endpoint: self.endpoint_id,
            app_profile_id: self.profile_id,
            app_device_id: self.device_type.device_id(),
            app_device_version: 1,
        };

        let ret = unsafe { ffi::esp_zb_ep_list_add_ep(ep_list, cluster_list, ep_cfg) };

        log::info!(
            "Registered endpoint {} as {:?} (profile 0x{:04X}, device 0x{:04X}) ret={}",
            self.endpoint_id,
            self.device_type,
            self.profile_id,
            self.device_type.device_id(),
            ret,
        );
    }
}