// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /* Common Class for Zigbee End Point */ #include "ZigbeeEP.h" #if CONFIG_ZB_ENABLED #include "esp_zigbee_cluster.h" #include "zcl/esp_zigbee_zcl_power_config.h" /* Zigbee End Device Class */ ZigbeeEP::ZigbeeEP(uint8_t endpoint) { _endpoint = endpoint; log_v("Endpoint: %d", _endpoint); _ep_config.endpoint = 0; _cluster_list = nullptr; _on_identify = nullptr; _on_ota_state_change = nullptr; _on_default_response = nullptr; _read_model = NULL; _read_manufacturer = NULL; _time_status = 0; _is_bound = false; _use_manual_binding = false; _allow_multiple_binding = false; if (!lock) { lock = xSemaphoreCreateBinary(); if (lock == NULL) { log_e("Semaphore creation failed"); } } } void ZigbeeEP::setVersion(uint8_t version) { _ep_config.app_device_version = version; esp_zb_attribute_list_t *basic_cluster = esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_BASIC, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); if (basic_cluster == nullptr) { log_e("Failed to get basic cluster for application version"); return; } esp_err_t ret = esp_zb_basic_cluster_add_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_APPLICATION_VERSION_ID, (void *)&version); if (ret != ESP_OK) { log_e("Failed to add application version to basic cluster: 0x%x: %s", ret, esp_err_to_name(ret)); } } void ZigbeeEP::setHardwareVersion(uint8_t version) { esp_zb_attribute_list_t *basic_cluster = esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_BASIC, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); if (basic_cluster == nullptr) { log_e("Failed to get basic cluster for hardware version"); return; } esp_err_t ret = esp_zb_basic_cluster_add_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_HW_VERSION_ID, (void *)&version); if (ret != ESP_OK) { log_e("Failed to add hardware version to basic cluster: 0x%x: %s", ret, esp_err_to_name(ret)); } } bool ZigbeeEP::setManufacturerAndModel(const char *name, const char *model) { // Allocate a new array of size length + 2 (1 for the length, 1 for null terminator) char zb_name[ZB_MAX_NAME_LENGTH + 2]; char zb_model[ZB_MAX_NAME_LENGTH + 2]; // Convert manufacturer to ZCL string size_t name_length = strlen(name); size_t model_length = strlen(model); if (name_length > ZB_MAX_NAME_LENGTH || model_length > ZB_MAX_NAME_LENGTH) { log_e("Manufacturer or model name is too long"); return false; } // Get and check the basic cluster esp_zb_attribute_list_t *basic_cluster = esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_BASIC, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); if (basic_cluster == nullptr) { log_e("Failed to get basic cluster"); return false; } // Store the length as the first element zb_name[0] = static_cast(name_length); // Cast size_t to char zb_model[0] = static_cast(model_length); // Use memcpy to copy the characters to the result array memcpy(zb_name + 1, name, name_length); memcpy(zb_model + 1, model, model_length); // Null-terminate the array zb_name[name_length + 1] = '\0'; zb_model[model_length + 1] = '\0'; // Update the manufacturer and model attributes esp_err_t ret_name = esp_zb_basic_cluster_add_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, (void *)zb_name); if (ret_name != ESP_OK) { log_e("Failed to set manufacturer: 0x%x: %s", ret_name, esp_err_to_name(ret_name)); } esp_err_t ret_model = esp_zb_basic_cluster_add_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, (void *)zb_model); if (ret_model != ESP_OK) { log_e("Failed to set model: 0x%x: %s", ret_model, esp_err_to_name(ret_model)); } return ret_name == ESP_OK && ret_model == ESP_OK; } bool ZigbeeEP::setPowerSource(zb_power_source_t power_source, uint8_t battery_percentage, uint8_t battery_voltage) { esp_zb_attribute_list_t *basic_cluster = esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_BASIC, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); esp_err_t ret = esp_zb_cluster_update_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_POWER_SOURCE_ID, (void *)&power_source); if (ret != ESP_OK) { log_e("Failed to set power source: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } if (power_source == ZB_POWER_SOURCE_BATTERY) { // Add power config cluster and battery percentage attribute if (battery_percentage > 100) { battery_percentage = 100; } battery_percentage = battery_percentage * 2; esp_zb_attribute_list_t *power_config_cluster = esp_zb_zcl_attr_list_create(ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG); ret = esp_zb_power_config_cluster_add_attr(power_config_cluster, ESP_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_PERCENTAGE_REMAINING_ID, (void *)&battery_percentage); if (ret != ESP_OK) { log_e("Failed to add battery percentage attribute: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } ret = esp_zb_power_config_cluster_add_attr(power_config_cluster, ESP_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_VOLTAGE_ID, (void *)&battery_voltage); if (ret != ESP_OK) { log_e("Failed to add battery voltage attribute: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } ret = esp_zb_cluster_list_add_power_config_cluster(_cluster_list, power_config_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); if (ret != ESP_OK) { log_e("Failed to add power config cluster: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } } _power_source = power_source; return true; } bool ZigbeeEP::setBatteryPercentage(uint8_t percentage) { // 100% = 200 in decimal, 0% = 0 // Convert percentage to 0-200 range esp_zb_zcl_status_t ret = ESP_ZB_ZCL_STATUS_SUCCESS; if (percentage > 100) { percentage = 100; } percentage = percentage * 2; esp_zb_lock_acquire(portMAX_DELAY); ret = esp_zb_zcl_set_attribute_val( _endpoint, ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_PERCENTAGE_REMAINING_ID, &percentage, false ); esp_zb_lock_release(); if (ret != ESP_ZB_ZCL_STATUS_SUCCESS) { log_e("Failed to set battery percentage: 0x%x: %s", ret, esp_zb_zcl_status_to_name(ret)); return false; } log_v("Battery percentage updated"); return true; } bool ZigbeeEP::setBatteryVoltage(uint8_t voltage) { esp_zb_zcl_status_t ret = ESP_ZB_ZCL_STATUS_SUCCESS; esp_zb_lock_acquire(portMAX_DELAY); ret = esp_zb_zcl_set_attribute_val( _endpoint, ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_VOLTAGE_ID, &voltage, false ); esp_zb_lock_release(); if (ret != ESP_ZB_ZCL_STATUS_SUCCESS) { log_e("Failed to set battery voltage: 0x%x: %s", ret, esp_zb_zcl_status_to_name(ret)); return false; } log_v("Battery voltage updated"); return true; } bool ZigbeeEP::reportBatteryPercentage() { /* Send report attributes command */ esp_zb_zcl_report_attr_cmd_t report_attr_cmd; memset(&report_attr_cmd, 0, sizeof(report_attr_cmd)); report_attr_cmd.address_mode = ESP_ZB_APS_ADDR_MODE_DST_ADDR_ENDP_NOT_PRESENT; report_attr_cmd.attributeID = ESP_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_PERCENTAGE_REMAINING_ID; report_attr_cmd.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI; report_attr_cmd.clusterID = ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG; report_attr_cmd.zcl_basic_cmd.src_endpoint = _endpoint; report_attr_cmd.manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC; esp_zb_lock_acquire(portMAX_DELAY); esp_err_t ret = esp_zb_zcl_report_attr_cmd_req(&report_attr_cmd); esp_zb_lock_release(); if (ret != ESP_OK) { log_e("Failed to report battery percentage: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } log_v("Battery percentage reported"); return true; } char *ZigbeeEP::readManufacturer(uint8_t endpoint, uint16_t short_addr, esp_zb_ieee_addr_t ieee_addr) { /* Read peer Manufacture Name & Model Identifier */ esp_zb_zcl_read_attr_cmd_t read_req; memset(&read_req, 0, sizeof(read_req)); if (short_addr != 0) { read_req.address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT; read_req.zcl_basic_cmd.dst_addr_u.addr_short = short_addr; } else { read_req.address_mode = ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT; memcpy(read_req.zcl_basic_cmd.dst_addr_u.addr_long, ieee_addr, sizeof(esp_zb_ieee_addr_t)); } read_req.zcl_basic_cmd.src_endpoint = _endpoint; read_req.zcl_basic_cmd.dst_endpoint = endpoint; read_req.clusterID = ESP_ZB_ZCL_CLUSTER_ID_BASIC; uint16_t attributes[] = { ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, }; read_req.attr_number = ZB_ARRAY_LENGHT(attributes); read_req.attr_field = attributes; if (_read_manufacturer != NULL) { free(_read_manufacturer); } _read_manufacturer = NULL; esp_zb_lock_acquire(portMAX_DELAY); esp_zb_zcl_read_attr_cmd_req(&read_req); esp_zb_lock_release(); //Wait for response or timeout if (xSemaphoreTake(lock, ZB_CMD_TIMEOUT) != pdTRUE) { log_e("Error while reading manufacturer"); } return _read_manufacturer; } char *ZigbeeEP::readModel(uint8_t endpoint, uint16_t short_addr, esp_zb_ieee_addr_t ieee_addr) { /* Read peer Manufacture Name & Model Identifier */ esp_zb_zcl_read_attr_cmd_t read_req; memset(&read_req, 0, sizeof(read_req)); if (short_addr != 0) { read_req.address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT; read_req.zcl_basic_cmd.dst_addr_u.addr_short = short_addr; } else { read_req.address_mode = ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT; memcpy(read_req.zcl_basic_cmd.dst_addr_u.addr_long, ieee_addr, sizeof(esp_zb_ieee_addr_t)); } read_req.zcl_basic_cmd.src_endpoint = _endpoint; read_req.zcl_basic_cmd.dst_endpoint = endpoint; read_req.clusterID = ESP_ZB_ZCL_CLUSTER_ID_BASIC; uint16_t attributes[] = { ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, }; read_req.attr_number = ZB_ARRAY_LENGHT(attributes); read_req.attr_field = attributes; if (_read_model != NULL) { free(_read_model); } _read_model = NULL; esp_zb_lock_acquire(portMAX_DELAY); esp_zb_zcl_read_attr_cmd_req(&read_req); esp_zb_lock_release(); //Wait for response or timeout if (xSemaphoreTake(lock, ZB_CMD_TIMEOUT) != pdTRUE) { log_e("Error while reading model"); } return _read_model; } void ZigbeeEP::printBoundDevices() { log_i("Bound devices:"); for ([[maybe_unused]] const auto &device : _bound_devices) { log_i( "Device on endpoint %d, short address: 0x%x, ieee address: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x", device->endpoint, device->short_addr, device->ieee_addr[7], device->ieee_addr[6], device->ieee_addr[5], device->ieee_addr[4], device->ieee_addr[3], device->ieee_addr[2], device->ieee_addr[1], device->ieee_addr[0] ); } } void ZigbeeEP::printBoundDevices(Print &print) { print.println("Bound devices:"); for ([[maybe_unused]] const auto &device : _bound_devices) { print.printf( "Device on endpoint %d, short address: 0x%x, ieee address: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\r\n", device->endpoint, device->short_addr, device->ieee_addr[7], device->ieee_addr[6], device->ieee_addr[5], device->ieee_addr[4], device->ieee_addr[3], device->ieee_addr[2], device->ieee_addr[1], device->ieee_addr[0] ); } } void ZigbeeEP::zbReadBasicCluster(const esp_zb_zcl_attribute_t *attribute) { /* Basic cluster attributes */ if (attribute->id == ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID && attribute->data.type == ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING && attribute->data.value) { zbstring_t *zbstr = (zbstring_t *)attribute->data.value; _read_manufacturer = (char *)malloc(zbstr->len + 1); if (_read_manufacturer == NULL) { log_e("Failed to allocate memory for manufacturer data"); xSemaphoreGive(lock); return; } memcpy(_read_manufacturer, zbstr->data, zbstr->len); _read_manufacturer[zbstr->len] = '\0'; log_i("Peer Manufacturer is \"%s\"", _read_manufacturer); xSemaphoreGive(lock); } if (attribute->id == ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID && attribute->data.type == ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING && attribute->data.value) { zbstring_t *zbstr = (zbstring_t *)attribute->data.value; _read_model = (char *)malloc(zbstr->len + 1); if (_read_model == NULL) { log_e("Failed to allocate memory for model data"); xSemaphoreGive(lock); return; } memcpy(_read_model, zbstr->data, zbstr->len); _read_model[zbstr->len] = '\0'; log_i("Peer Model is \"%s\"", _read_model); xSemaphoreGive(lock); } } void ZigbeeEP::zbIdentify(const esp_zb_zcl_set_attr_value_message_t *message) { if (message->attribute.id == ESP_ZB_ZCL_CMD_IDENTIFY_IDENTIFY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) { if (_on_identify != NULL) { _on_identify(*(uint16_t *)message->attribute.data.value); } } else { log_w("Other identify commands are not implemented yet."); } } void ZigbeeEP::zbOTAState(bool otaActive) { if (_on_ota_state_change != NULL) { _on_ota_state_change(otaActive); } } bool ZigbeeEP::addTimeCluster(tm time, int32_t gmt_offset) { time_t utc_time = 0; // Check if time is set if (time.tm_year > 0) { // Convert time to UTC utc_time = mktime(&time); } // Create time cluster server attributes esp_zb_attribute_list_t *time_cluster_server = esp_zb_zcl_attr_list_create(ESP_ZB_ZCL_CLUSTER_ID_TIME); esp_err_t ret = esp_zb_time_cluster_add_attr(time_cluster_server, ESP_ZB_ZCL_ATTR_TIME_TIME_ZONE_ID, (void *)&gmt_offset); if (ret != ESP_OK) { log_e("Failed to add time zone attribute: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } ret = esp_zb_time_cluster_add_attr(time_cluster_server, ESP_ZB_ZCL_ATTR_TIME_TIME_ID, (void *)&utc_time); if (ret != ESP_OK) { log_e("Failed to add time attribute: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } ret = esp_zb_time_cluster_add_attr(time_cluster_server, ESP_ZB_ZCL_ATTR_TIME_TIME_STATUS_ID, (void *)&_time_status); if (ret != ESP_OK) { log_e("Failed to add time status attribute: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } // Create time cluster client attributes esp_zb_attribute_list_t *time_cluster_client = esp_zb_zcl_attr_list_create(ESP_ZB_ZCL_CLUSTER_ID_TIME); // Add time clusters to cluster list ret = esp_zb_cluster_list_add_time_cluster(_cluster_list, time_cluster_server, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); if (ret != ESP_OK) { log_e("Failed to add time cluster (server role): 0x%x: %s", ret, esp_err_to_name(ret)); return false; } ret = esp_zb_cluster_list_add_time_cluster(_cluster_list, time_cluster_client, ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE); if (ret != ESP_OK) { log_e("Failed to add time cluster (client role): 0x%x: %s", ret, esp_err_to_name(ret)); return false; } return true; } bool ZigbeeEP::setTime(tm time) { esp_zb_zcl_status_t ret = ESP_ZB_ZCL_STATUS_SUCCESS; time_t utc_time = mktime(&time); log_d("Setting time to %lld", utc_time); esp_zb_lock_acquire(portMAX_DELAY); ret = esp_zb_zcl_set_attribute_val(_endpoint, ESP_ZB_ZCL_CLUSTER_ID_TIME, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_TIME_TIME_ID, &utc_time, false); esp_zb_lock_release(); if (ret != ESP_ZB_ZCL_STATUS_SUCCESS) { log_e("Failed to set time: 0x%x: %s", ret, esp_zb_zcl_status_to_name(ret)); return false; } return true; } bool ZigbeeEP::setTimezone(int32_t gmt_offset) { esp_zb_zcl_status_t ret = ESP_ZB_ZCL_STATUS_SUCCESS; log_d("Setting timezone to %d", gmt_offset); esp_zb_lock_acquire(portMAX_DELAY); ret = esp_zb_zcl_set_attribute_val(_endpoint, ESP_ZB_ZCL_CLUSTER_ID_TIME, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_TIME_TIME_ZONE_ID, &gmt_offset, false); esp_zb_lock_release(); if (ret != ESP_ZB_ZCL_STATUS_SUCCESS) { log_e("Failed to set timezone: 0x%x: %s", ret, esp_zb_zcl_status_to_name(ret)); return false; } return true; } tm ZigbeeEP::getTime(uint8_t endpoint, int32_t short_addr, esp_zb_ieee_addr_t ieee_addr) { /* Read peer time */ esp_zb_zcl_read_attr_cmd_t read_req; memset(&read_req, 0, sizeof(read_req)); if (short_addr >= 0) { read_req.address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT; read_req.zcl_basic_cmd.dst_addr_u.addr_short = (uint16_t)short_addr; } else { read_req.address_mode = ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT; memcpy(read_req.zcl_basic_cmd.dst_addr_u.addr_long, ieee_addr, sizeof(esp_zb_ieee_addr_t)); } uint16_t attributes[] = {ESP_ZB_ZCL_ATTR_TIME_TIME_ID}; read_req.attr_number = ZB_ARRAY_LENGHT(attributes); read_req.attr_field = attributes; read_req.clusterID = ESP_ZB_ZCL_CLUSTER_ID_TIME; read_req.zcl_basic_cmd.dst_endpoint = endpoint; read_req.zcl_basic_cmd.src_endpoint = _endpoint; // clear read time _read_time = 0; log_v("Reading time from endpoint %d", endpoint); esp_zb_zcl_read_attr_cmd_req(&read_req); //Wait for response or timeout if (xSemaphoreTake(lock, ZB_CMD_TIMEOUT) != pdTRUE) { log_e("Error while reading time"); return tm(); } struct tm *timeinfo = localtime(&_read_time); if (timeinfo) { // Update time setTime(*timeinfo); // Update time status to synced _time_status |= 0x02; esp_zb_lock_acquire(portMAX_DELAY); esp_zb_zcl_set_attribute_val( _endpoint, ESP_ZB_ZCL_CLUSTER_ID_TIME, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_TIME_TIME_STATUS_ID, &_time_status, false ); esp_zb_lock_release(); return *timeinfo; } else { log_e("Error while converting time"); return tm(); } } int32_t ZigbeeEP::getTimezone(uint8_t endpoint, int32_t short_addr, esp_zb_ieee_addr_t ieee_addr) { /* Read peer timezone */ esp_zb_zcl_read_attr_cmd_t read_req; memset(&read_req, 0, sizeof(read_req)); if (short_addr >= 0) { read_req.address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT; read_req.zcl_basic_cmd.dst_addr_u.addr_short = (uint16_t)short_addr; } else { read_req.address_mode = ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT; memcpy(read_req.zcl_basic_cmd.dst_addr_u.addr_long, ieee_addr, sizeof(esp_zb_ieee_addr_t)); } uint16_t attributes[] = {ESP_ZB_ZCL_ATTR_TIME_TIME_ZONE_ID}; read_req.attr_number = ZB_ARRAY_LENGHT(attributes); read_req.attr_field = attributes; read_req.clusterID = ESP_ZB_ZCL_CLUSTER_ID_TIME; read_req.zcl_basic_cmd.dst_endpoint = endpoint; read_req.zcl_basic_cmd.src_endpoint = _endpoint; // clear read timezone _read_timezone = 0; log_v("Reading timezone from endpoint %d", endpoint); esp_zb_zcl_read_attr_cmd_req(&read_req); //Wait for response or timeout if (xSemaphoreTake(lock, ZB_CMD_TIMEOUT) != pdTRUE) { log_e("Error while reading timezone"); return 0; } setTimezone(_read_timezone); return _read_timezone; } void ZigbeeEP::zbReadTimeCluster(const esp_zb_zcl_attribute_t *attribute) { /* Time cluster attributes */ if (attribute->id == ESP_ZB_ZCL_ATTR_TIME_TIME_ID && attribute->data.type == ESP_ZB_ZCL_ATTR_TYPE_UTC_TIME) { log_v("Time attribute received"); log_v("Time: %lld", *(uint32_t *)attribute->data.value); _read_time = *(uint32_t *)attribute->data.value; xSemaphoreGive(lock); } else if (attribute->id == ESP_ZB_ZCL_ATTR_TIME_TIME_ZONE_ID && attribute->data.type == ESP_ZB_ZCL_ATTR_TYPE_S32) { log_v("Timezone attribute received"); log_v("Timezone: %d", *(int32_t *)attribute->data.value); _read_timezone = *(int32_t *)attribute->data.value; xSemaphoreGive(lock); } } // typedef struct esp_zb_ota_cluster_cfg_s { // uint32_t ota_upgrade_file_version; /*!< The attribute indicates the file version of the running firmware image on the device */ // uint16_t ota_upgrade_manufacturer; /*!< The attribute indicates the value for the manufacturer of the device */ // uint16_t ota_upgrade_image_type; /*!< The attribute indicates the the image type of the file that the client is currently downloading */ // uint32_t ota_upgrade_downloaded_file_ver; /*!< The attribute indicates the file version of the downloaded image on the device*/ // esp_zb_ota_cluster_cfg_t; // typedef struct esp_zb_zcl_ota_upgrade_client_variable_s { // uint16_t timer_query; /*!< The field indicates the time of querying OTA image for OTA upgrade client */ // uint16_t hw_version; /*!< The hardware version */ // uint8_t max_data_size; /*!< The maximum size of OTA data */ // } esp_zb_zcl_ota_upgrade_client_variable_t; bool ZigbeeEP::addOTAClient( uint32_t file_version, uint32_t downloaded_file_ver, uint16_t hw_version, uint16_t manufacturer, uint16_t image_type, uint8_t max_data_size ) { esp_zb_ota_cluster_cfg_t ota_cluster_cfg = {}; ota_cluster_cfg.ota_upgrade_file_version = file_version; //OTA_UPGRADE_RUNNING_FILE_VERSION; ota_cluster_cfg.ota_upgrade_downloaded_file_ver = downloaded_file_ver; //OTA_UPGRADE_DOWNLOADED_FILE_VERSION; ota_cluster_cfg.ota_upgrade_manufacturer = manufacturer; //OTA_UPGRADE_MANUFACTURER; ota_cluster_cfg.ota_upgrade_image_type = image_type; //OTA_UPGRADE_IMAGE_TYPE; esp_zb_attribute_list_t *ota_cluster = esp_zb_ota_cluster_create(&ota_cluster_cfg); esp_zb_zcl_ota_upgrade_client_variable_t variable_config = {}; variable_config.timer_query = ESP_ZB_ZCL_OTA_UPGRADE_QUERY_TIMER_COUNT_DEF; variable_config.hw_version = hw_version; //OTA_UPGRADE_HW_VERSION; variable_config.max_data_size = max_data_size; //OTA_UPGRADE_MAX_DATA_SIZE; uint16_t ota_upgrade_server_addr = 0xffff; uint8_t ota_upgrade_server_ep = 0xff; esp_err_t ret = esp_zb_ota_cluster_add_attr(ota_cluster, ESP_ZB_ZCL_ATTR_OTA_UPGRADE_CLIENT_DATA_ID, (void *)&variable_config); if (ret != ESP_OK) { log_e("Failed to add OTA client data: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } ret = esp_zb_ota_cluster_add_attr(ota_cluster, ESP_ZB_ZCL_ATTR_OTA_UPGRADE_SERVER_ADDR_ID, (void *)&ota_upgrade_server_addr); if (ret != ESP_OK) { log_e("Failed to add OTA server address: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } ret = esp_zb_ota_cluster_add_attr(ota_cluster, ESP_ZB_ZCL_ATTR_OTA_UPGRADE_SERVER_ENDPOINT_ID, (void *)&ota_upgrade_server_ep); if (ret != ESP_OK) { log_e("Failed to add OTA server endpoint: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } ret = esp_zb_cluster_list_add_ota_cluster(_cluster_list, ota_cluster, ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE); if (ret != ESP_OK) { log_e("Failed to add OTA cluster: 0x%x: %s", ret, esp_err_to_name(ret)); return false; } return true; } static void findOTAServer(esp_zb_zdp_status_t zdo_status, uint16_t addr, uint8_t endpoint, void *user_ctx) { if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { esp_zb_ota_upgrade_client_query_interval_set(*((uint8_t *)user_ctx), OTA_UPGRADE_QUERY_INTERVAL); esp_zb_ota_upgrade_client_query_image_req(addr, endpoint); log_i("Query OTA upgrade from server endpoint: %d after %d seconds", endpoint, OTA_UPGRADE_QUERY_INTERVAL); } else { log_w("No OTA Server found"); } } void ZigbeeEP::requestOTAUpdate() { esp_zb_zdo_match_desc_req_param_t req; memset(&req, 0, sizeof(req)); uint16_t cluster_list[] = {ESP_ZB_ZCL_CLUSTER_ID_OTA_UPGRADE}; /* Match the OTA server of coordinator */ req.addr_of_interest = 0x0000; req.dst_nwk_addr = 0x0000; req.num_in_clusters = 1; req.num_out_clusters = 0; req.profile_id = ESP_ZB_AF_HA_PROFILE_ID; req.cluster_list = cluster_list; esp_zb_lock_acquire(portMAX_DELAY); if (esp_zb_bdb_dev_joined()) { esp_zb_zdo_match_cluster(&req, findOTAServer, &_endpoint); } esp_zb_lock_release(); } void ZigbeeEP::removeBoundDevice(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr) { log_d( "Attempting to remove device with endpoint %d and IEEE address %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x", endpoint, ieee_addr[7], ieee_addr[6], ieee_addr[5], ieee_addr[4], ieee_addr[3], ieee_addr[2], ieee_addr[1], ieee_addr[0] ); for (std::list::iterator it = _bound_devices.begin(); it != _bound_devices.end(); ++it) { if ((*it)->endpoint == endpoint && memcmp((*it)->ieee_addr, ieee_addr, sizeof(esp_zb_ieee_addr_t)) == 0) { log_d("Found matching device, removing it"); _bound_devices.erase(it); if (_bound_devices.empty()) { _is_bound = false; } return; } } log_w("No matching device found for removal"); } void ZigbeeEP::removeBoundDevice(zb_device_params_t *device) { if (!device) { log_e("Invalid device parameters provided"); return; } log_d( "Attempting to remove device with endpoint %d, short address 0x%04x, IEEE address %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x", device->endpoint, device->short_addr, device->ieee_addr[7], device->ieee_addr[6], device->ieee_addr[5], device->ieee_addr[4], device->ieee_addr[3], device->ieee_addr[2], device->ieee_addr[1], device->ieee_addr[0] ); for (std::list::iterator it = _bound_devices.begin(); it != _bound_devices.end(); ++it) { bool endpoint_matches = ((*it)->endpoint == device->endpoint); bool short_addr_matches = (device->short_addr != 0xFFFF && (*it)->short_addr == device->short_addr); bool ieee_addr_matches = (memcmp((*it)->ieee_addr, device->ieee_addr, sizeof(esp_zb_ieee_addr_t)) == 0); if (endpoint_matches && (short_addr_matches || ieee_addr_matches)) { log_d("Found matching device by %s, removing it", short_addr_matches ? "short address" : "IEEE address"); _bound_devices.erase(it); if (_bound_devices.empty()) { _is_bound = false; } return; } } log_w("No matching device found for removal"); } void ZigbeeEP::zbDefaultResponse(const esp_zb_zcl_cmd_default_resp_message_t *message) { log_v("Default response received for endpoint %d", _endpoint); log_v("Status code: %s", esp_zb_zcl_status_to_name(message->status_code)); log_v("Response to command: %d", message->resp_to_cmd); if (_on_default_response) { _on_default_response((zb_cmd_type_t)message->resp_to_cmd, message->status_code); } } // Global function implementation const char *esp_zb_zcl_status_to_name(esp_zb_zcl_status_t status) { switch (status) { case ESP_ZB_ZCL_STATUS_SUCCESS: return "Success"; case ESP_ZB_ZCL_STATUS_FAIL: return "Fail"; case ESP_ZB_ZCL_STATUS_NOT_AUTHORIZED: return "Not authorized"; case ESP_ZB_ZCL_STATUS_MALFORMED_CMD: return "Malformed command"; case ESP_ZB_ZCL_STATUS_UNSUP_CLUST_CMD: return "Unsupported cluster command"; case ESP_ZB_ZCL_STATUS_UNSUP_GEN_CMD: return "Unsupported general command"; case ESP_ZB_ZCL_STATUS_UNSUP_MANUF_CLUST_CMD: return "Unsupported manufacturer cluster command"; case ESP_ZB_ZCL_STATUS_UNSUP_MANUF_GEN_CMD: return "Unsupported manufacturer general command"; case ESP_ZB_ZCL_STATUS_INVALID_FIELD: return "Invalid field"; case ESP_ZB_ZCL_STATUS_UNSUP_ATTRIB: return "Unsupported attribute"; case ESP_ZB_ZCL_STATUS_INVALID_VALUE: return "Invalid value"; case ESP_ZB_ZCL_STATUS_READ_ONLY: return "Read only"; case ESP_ZB_ZCL_STATUS_INSUFF_SPACE: return "Insufficient space"; case ESP_ZB_ZCL_STATUS_DUPE_EXISTS: return "Duplicate exists"; case ESP_ZB_ZCL_STATUS_NOT_FOUND: return "Not found"; case ESP_ZB_ZCL_STATUS_UNREPORTABLE_ATTRIB: return "Unreportable attribute"; case ESP_ZB_ZCL_STATUS_INVALID_TYPE: return "Invalid type"; case ESP_ZB_ZCL_STATUS_WRITE_ONLY: return "Write only"; case ESP_ZB_ZCL_STATUS_INCONSISTENT: return "Inconsistent"; case ESP_ZB_ZCL_STATUS_ACTION_DENIED: return "Action denied"; case ESP_ZB_ZCL_STATUS_TIMEOUT: return "Timeout"; case ESP_ZB_ZCL_STATUS_ABORT: return "Abort"; case ESP_ZB_ZCL_STATUS_INVALID_IMAGE: return "Invalid OTA upgrade image"; case ESP_ZB_ZCL_STATUS_WAIT_FOR_DATA: return "Server does not have data block available yet"; case ESP_ZB_ZCL_STATUS_NO_IMAGE_AVAILABLE: return "No image available"; case ESP_ZB_ZCL_STATUS_REQUIRE_MORE_IMAGE: return "Require more image"; case ESP_ZB_ZCL_STATUS_NOTIFICATION_PENDING: return "Notification pending"; case ESP_ZB_ZCL_STATUS_HW_FAIL: return "Hardware failure"; case ESP_ZB_ZCL_STATUS_SW_FAIL: return "Software failure"; case ESP_ZB_ZCL_STATUS_CALIB_ERR: return "Calibration error"; case ESP_ZB_ZCL_STATUS_UNSUP_CLUST: return "Cluster is not found on the target endpoint"; case ESP_ZB_ZCL_STATUS_LIMIT_REACHED: return "Limit reached"; default: return "Unknown status"; } } #endif // CONFIG_ZB_ENABLED