From b4beb16b13024d153b101a2475661b498a6f2f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:18:26 +0200 Subject: [PATCH] feat(zigbee): Add Binary Output support --- .../README.md | 21 ++- .../Zigbee_Binary_Input_Output.ino} | 56 +++++-- .../ci.json | 0 libraries/Zigbee/src/ep/ZigbeeBinary.cpp | 152 ++++++++++++++++++ libraries/Zigbee/src/ep/ZigbeeBinary.h | 48 ++++-- 5 files changed, 247 insertions(+), 30 deletions(-) rename libraries/Zigbee/examples/{Zigbee_Binary_Input => Zigbee_Binary_Input_Output}/README.md (78%) rename libraries/Zigbee/examples/{Zigbee_Binary_Input/Zigbee_Binary_Input.ino => Zigbee_Binary_Input_Output/Zigbee_Binary_Input_Output.ino} (67%) rename libraries/Zigbee/examples/{Zigbee_Binary_Input => Zigbee_Binary_Input_Output}/ci.json (100%) diff --git a/libraries/Zigbee/examples/Zigbee_Binary_Input/README.md b/libraries/Zigbee/examples/Zigbee_Binary_Input_Output/README.md similarity index 78% rename from libraries/Zigbee/examples/Zigbee_Binary_Input/README.md rename to libraries/Zigbee/examples/Zigbee_Binary_Input_Output/README.md index 6ca3aac7119..a3f70a175b3 100644 --- a/libraries/Zigbee/examples/Zigbee_Binary_Input/README.md +++ b/libraries/Zigbee/examples/Zigbee_Binary_Input_Output/README.md @@ -1,6 +1,6 @@ -# Arduino-ESP32 Zigbee Binary Input Example +# Arduino-ESP32 Zigbee Binary Input Output Example -This example shows how to configure the Zigbee end device and use it as a Home Automation (HA) binary input device with two different applications: HVAC fan status and security zone armed status. +This example shows how to configure the Zigbee end device and use it as a Home Automation (HA) binary input/output device with multiple applications: HVAC fan status/control, security zone armed status, and HVAC humidifier control. # Supported Targets @@ -9,12 +9,17 @@ Currently, this example supports the following targets. | Supported Targets | ESP32-C6 | ESP32-H2 | | ----------------- | -------- | -------- | -## Binary Input Functions - - * The example implements two binary inputs: - - HVAC Fan Status: Reports the current state of a fan - - Security Zone Armed: Reports the armed state of a security zone - * By clicking the button (BOOT) on this board, it will toggle both binary inputs and immediately send a report of their states to the network. +## Binary Input/Output Functions + + * The example implements three binary devices: + - **Binary Fan Device (Endpoint 1)**: + - Binary Input: HVAC Fan Status - Reports the current state of a fan + - Binary Output: HVAC Fan - Controls the fan switch with callback function + - **Binary Zone Device (Endpoint 2)**: + - Binary Input: Security Zone Armed - Reports the armed state of a security zone + - **Binary Humidifier Device (Endpoint 3)**: + - Binary Output: HVAC Humidifier - Controls the humidifier switch with callback function + * By clicking the button (BOOT) on this board, it will toggle all binary inputs/outputs and immediately send a report of their states to the network. * Holding the button for more than 3 seconds will trigger a factory reset of the Zigbee device. ## Hardware Required diff --git a/libraries/Zigbee/examples/Zigbee_Binary_Input/Zigbee_Binary_Input.ino b/libraries/Zigbee/examples/Zigbee_Binary_Input_Output/Zigbee_Binary_Input_Output.ino similarity index 67% rename from libraries/Zigbee/examples/Zigbee_Binary_Input/Zigbee_Binary_Input.ino rename to libraries/Zigbee/examples/Zigbee_Binary_Input_Output/Zigbee_Binary_Input_Output.ino index de0cf606dcd..4e08ccb7cf1 100644 --- a/libraries/Zigbee/examples/Zigbee_Binary_Input/Zigbee_Binary_Input.ino +++ b/libraries/Zigbee/examples/Zigbee_Binary_Input_Output/Zigbee_Binary_Input_Output.ino @@ -13,9 +13,9 @@ // limitations under the License. /** - * @brief This example demonstrates Zigbee binary input device. + * @brief This example demonstrates Zigbee binary input/output device. * - * The example demonstrates how to use Zigbee library to create an end device binary sensor device. + * The example demonstrates how to use Zigbee library to create an end device binary sensor/switch device. * * Proper Zigbee mode must be selected in Tools->Zigbee mode * and also the correct partition scheme must be selected in Tools->Partition Scheme. @@ -34,13 +34,28 @@ /* Zigbee binary sensor device configuration */ #define BINARY_DEVICE_ENDPOINT_NUMBER 1 -uint8_t binaryPin = A0; uint8_t button = BOOT_PIN; ZigbeeBinary zbBinaryFan = ZigbeeBinary(BINARY_DEVICE_ENDPOINT_NUMBER); ZigbeeBinary zbBinaryZone = ZigbeeBinary(BINARY_DEVICE_ENDPOINT_NUMBER + 1); +ZigbeeBinary zbBinaryHumidifier = ZigbeeBinary(BINARY_DEVICE_ENDPOINT_NUMBER + 2); -bool binaryStatus = false; +bool zoneStatus = false; + +void fanSwitch(bool state) { + Serial.println("Fan switch changed to: " + String(state)); + if (state) { + zbBinaryFan.setBinaryInput(state); + zbBinaryFan.reportBinaryInput(); + } else { + zbBinaryFan.setBinaryInput(state); + zbBinaryFan.reportBinaryInput(); + } +} + +void humidifierSwitch(bool state) { + Serial.println("Humidifier switch changed to: " + String(state)); +} void setup() { Serial.begin(115200); @@ -55,19 +70,33 @@ void setup() { // Optional: set Zigbee device name and model zbBinaryFan.setManufacturerAndModel("Espressif", "ZigbeeBinarySensor"); - // Set up binary fan status input (HVAC) + // Set up binary fan status input + switch output (HVAC) zbBinaryFan.addBinaryInput(); zbBinaryFan.setBinaryInputApplication(BINARY_INPUT_APPLICATION_TYPE_HVAC_FAN_STATUS); zbBinaryFan.setBinaryInputDescription("Fan Status"); + zbBinaryFan.addBinaryOutput(); + zbBinaryFan.setBinaryOutputApplication(BINARY_OUTPUT_APPLICATION_TYPE_HVAC_FAN); + zbBinaryFan.setBinaryOutputDescription("Fan Switch"); + + zbBinaryFan.onBinaryOutputChange(fanSwitch); + // Set up binary zone armed input (Security) zbBinaryZone.addBinaryInput(); zbBinaryZone.setBinaryInputApplication(BINARY_INPUT_APPLICATION_TYPE_SECURITY_ZONE_ARMED); zbBinaryZone.setBinaryInputDescription("Zone Armed"); + // Set up binary humidifier output (HVAC) + zbBinaryHumidifier.addBinaryOutput(); + zbBinaryHumidifier.setBinaryOutputApplication(BINARY_OUTPUT_APPLICATION_TYPE_HVAC_HUMIDIFIER); + zbBinaryHumidifier.setBinaryOutputDescription("Humidifier Switch"); + + zbBinaryHumidifier.onBinaryOutputChange(humidifierSwitch); + // Add endpoints to Zigbee Core Zigbee.addEndpoint(&zbBinaryFan); Zigbee.addEndpoint(&zbBinaryZone); + Zigbee.addEndpoint(&zbBinaryHumidifier); Serial.println("Starting Zigbee..."); // When all EPs are registered, start Zigbee in End Device mode @@ -101,12 +130,19 @@ void loop() { Zigbee.factoryReset(); } } - // Toggle binary input - binaryStatus = !binaryStatus; - zbBinaryFan.setBinaryInput(binaryStatus); - zbBinaryZone.setBinaryInput(binaryStatus); - zbBinaryFan.reportBinaryInput(); + + // Toggle fan + zbBinaryFan.setBinaryOutput(!zbBinaryFan.getBinaryOutput()); + zbBinaryFan.reportBinaryOutput(); + + // Toggle zone + zoneStatus = !zoneStatus; + zbBinaryZone.setBinaryInput(zoneStatus); zbBinaryZone.reportBinaryInput(); + + // Toggle humidifier + zbBinaryHumidifier.setBinaryOutput(!zbBinaryHumidifier.getBinaryOutput()); + zbBinaryHumidifier.reportBinaryOutput(); } delay(100); } diff --git a/libraries/Zigbee/examples/Zigbee_Binary_Input/ci.json b/libraries/Zigbee/examples/Zigbee_Binary_Input_Output/ci.json similarity index 100% rename from libraries/Zigbee/examples/Zigbee_Binary_Input/ci.json rename to libraries/Zigbee/examples/Zigbee_Binary_Input_Output/ci.json diff --git a/libraries/Zigbee/src/ep/ZigbeeBinary.cpp b/libraries/Zigbee/src/ep/ZigbeeBinary.cpp index aa37ceb3020..e726858e669 100644 --- a/libraries/Zigbee/src/ep/ZigbeeBinary.cpp +++ b/libraries/Zigbee/src/ep/ZigbeeBinary.cpp @@ -41,6 +41,35 @@ bool ZigbeeBinary::addBinaryInput() { return true; } +bool ZigbeeBinary::addBinaryOutput() { + esp_zb_attribute_list_t *esp_zb_binary_output_cluster = esp_zb_binary_output_cluster_create(NULL); + + // Create default description for Binary Output + char default_description[] = "\x0D" + "Binary Output"; + uint32_t application_type = 0x00000000 | (0x04 << 24); // Group ID 0x04 + + esp_err_t ret = esp_zb_binary_output_cluster_add_attr(esp_zb_binary_output_cluster, ESP_ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID, (void *)default_description); + if (ret != ESP_OK) { + log_e("Failed to add description attribute: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + + ret = esp_zb_binary_output_cluster_add_attr(esp_zb_binary_output_cluster, ESP_ZB_ZCL_ATTR_BINARY_OUTPUT_APPLICATION_TYPE_ID, (void *)&application_type); + if (ret != ESP_OK) { + log_e("Failed to add application type attribute: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + + ret = esp_zb_cluster_list_add_binary_output_cluster(_cluster_list, esp_zb_binary_output_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + if (ret != ESP_OK) { + log_e("Failed to add Binary Output cluster: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + _binary_clusters |= BINARY_OUTPUT; + return true; +} + // Check Zigbee Cluster Specification 3.14.11.19.4 Binary Inputs (BI) Types for application type values bool ZigbeeBinary::setBinaryInputApplication(uint32_t application_type) { if (!(_binary_clusters & BINARY_INPUT)) { @@ -61,6 +90,26 @@ bool ZigbeeBinary::setBinaryInputApplication(uint32_t application_type) { return true; } +// Check Zigbee Cluster Specification 3.14.11.19.5 Binary Outputs (BO) Types for application type values +bool ZigbeeBinary::setBinaryOutputApplication(uint32_t application_type) { + if (!(_binary_clusters & BINARY_OUTPUT)) { + log_e("Binary Output cluster not added"); + return false; + } + + // Add the Binary Output group ID (0x04) to the application type + uint32_t application_type_value = (0x04 << 24) | application_type; + + esp_zb_attribute_list_t *binary_output_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + esp_err_t ret = esp_zb_cluster_update_attr(binary_output_cluster, ESP_ZB_ZCL_ATTR_BINARY_OUTPUT_APPLICATION_TYPE_ID, (void *)&application_type_value); + if (ret != ESP_OK) { + log_e("Failed to set Binary Output application type: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + return true; +} + bool ZigbeeBinary::setBinaryInput(bool input) { esp_zb_zcl_status_t ret = ESP_ZB_ZCL_STATUS_SUCCESS; if (!(_binary_clusters & BINARY_INPUT)) { @@ -141,4 +190,107 @@ bool ZigbeeBinary::setBinaryInputDescription(const char *description) { return true; } +bool ZigbeeBinary::setBinaryOutputDescription(const char *description) { + if (!(_binary_clusters & BINARY_OUTPUT)) { + log_e("Binary Output cluster not added"); + return false; + } + + // Allocate a new array of size length + 2 (1 for the length, 1 for null terminator) + char zb_description[ZB_MAX_NAME_LENGTH + 2]; + + // Convert description to ZCL string + size_t description_length = strlen(description); + if (description_length > ZB_MAX_NAME_LENGTH) { + log_e("Description is too long"); + return false; + } + + // Get and check the binary output cluster + esp_zb_attribute_list_t *binary_output_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + if (binary_output_cluster == nullptr) { + log_e("Failed to get binary output cluster"); + return false; + } + + // Store the length as the first element + zb_description[0] = static_cast(description_length); // Cast size_t to char + // Use memcpy to copy the characters to the result array + memcpy(zb_description + 1, description, description_length); + // Null-terminate the array + zb_description[description_length + 1] = '\0'; + + // Update the description attribute + esp_err_t ret = esp_zb_cluster_update_attr(binary_output_cluster, ESP_ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID, (void *)zb_description); + if (ret != ESP_OK) { + log_e("Failed to set description: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + return true; +} + +//set attribute method -> method overridden in child class +void ZigbeeBinary::zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) { + if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT) { + if (message->attribute.id == ESP_ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_BOOL) { + _output_state = *(bool *)message->attribute.data.value; + binaryOutputChanged(); + } else { + log_w("Received message ignored. Attribute ID: %d not supported for Binary Output", message->attribute.id); + } + } else { + log_w("Received message ignored. Cluster ID: %d not supported for Binary endpoint", message->info.cluster); + } +} + +void ZigbeeBinary::binaryOutputChanged() { + if (_on_binary_output_change) { + _on_binary_output_change(_output_state); + } else { + log_w("No callback function set for binary output change"); + } +} + +bool ZigbeeBinary::setBinaryOutput(bool output) { + esp_zb_zcl_status_t ret = ESP_ZB_ZCL_STATUS_SUCCESS; + _output_state = output; + binaryOutputChanged(); + + log_v("Updating binary output to %d", output); + /* Update binary output */ + esp_zb_lock_acquire(portMAX_DELAY); + ret = esp_zb_zcl_set_attribute_val( + _endpoint, ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, &_output_state, false + ); + esp_zb_lock_release(); + + if (ret != ESP_ZB_ZCL_STATUS_SUCCESS) { + log_e("Failed to set binary output: 0x%x: %s", ret, esp_zb_zcl_status_to_name(ret)); + return false; + } + return true; +} + +bool ZigbeeBinary::reportBinaryOutput() { + /* Send report attributes command */ + esp_zb_zcl_report_attr_cmd_t 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_BINARY_OUTPUT_PRESENT_VALUE_ID; + report_attr_cmd.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI; + report_attr_cmd.clusterID = ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT; + 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 send Binary Output report: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + log_v("Binary Output report sent"); + return true; +} + #endif // CONFIG_ZB_ENABLED diff --git a/libraries/Zigbee/src/ep/ZigbeeBinary.h b/libraries/Zigbee/src/ep/ZigbeeBinary.h index 5a543604970..a6823599a14 100644 --- a/libraries/Zigbee/src/ep/ZigbeeBinary.h +++ b/libraries/Zigbee/src/ep/ZigbeeBinary.h @@ -38,10 +38,26 @@ enum zigbee_binary_clusters { #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_HEAT_DETECTION 0x01000008 // Type 0x01, Index 0x0008 #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_OTHER 0x0100FFFF // Type 0x01, Index 0xFFFF +// HVAC application types for Binary Output (more can be found in Zigbee Cluster Specification 3.14.11.19.5 Binary Outputs (BO) Types) +#define BINARY_OUTPUT_APPLICATION_TYPE_HVAC_BOILER 0x00000003 // Type 0x00, Index 0x0003 +#define BINARY_OUTPUT_APPLICATION_TYPE_HVAC_CHILLER 0x0000000D // Type 0x00, Index 0x000D +#define BINARY_OUTPUT_APPLICATION_TYPE_HVAC_FAN 0x00000022 // Type 0x00, Index 0x0022 +#define BINARY_OUTPUT_APPLICATION_TYPE_HVAC_HEATING_VALVE 0x0000002C // Type 0x00, Index 0x002C +#define BINARY_OUTPUT_APPLICATION_TYPE_HVAC_HUMIDIFIER 0x00000033 // Type 0x00, Index 0x0033 +#define BINARY_OUTPUT_APPLICATION_TYPE_HVAC_PREHEAT 0x00000034 // Type 0x00, Index 0x0034 +#define BINARY_OUTPUT_APPLICATION_TYPE_HVAC_OTHER 0x0000FFFF // Type 0x00, Index 0xFFFF + +// Security application types for Binary Output +#define BINARY_OUTPUT_APPLICATION_TYPE_SECURITY_ARM_DISARM_COMMAND 0x01000000 // Type 0x01, Index 0x0000 +#define BINARY_OUTPUT_APPLICATION_TYPE_SECURITY_OCCUPANCY_CONTROL 0x01000001 // Type 0x01, Index 0x0001 +#define BINARY_OUTPUT_APPLICATION_TYPE_SECURITY_ENABLE_CONTROL 0x01000002 // Type 0x01, Index 0x0002 +#define BINARY_OUTPUT_APPLICATION_TYPE_SECURITY_ACCESS_CONTROL 0x01000003 // Type 0x01, Index 0x0003 +#define BINARY_OUTPUT_APPLICATION_TYPE_SECURITY_OTHER 0x0100FFFF // Type 0x01, Index 0xFFFF + typedef struct zigbee_binary_cfg_s { esp_zb_basic_cluster_cfg_t basic_cfg; esp_zb_identify_cluster_cfg_t identify_cfg; - // esp_zb_binary_output_cluster_cfg_t binary_output_cfg; + esp_zb_binary_output_cluster_cfg_t binary_output_cfg; esp_zb_binary_input_cluster_cfg_t binary_input_cfg; } zigbee_binary_cfg_t; @@ -52,33 +68,41 @@ class ZigbeeBinary : public ZigbeeEP { // Add binary cluster bool addBinaryInput(); - // bool addBinaryOutput(); + bool addBinaryOutput(); // Set the application type and description for the binary input bool setBinaryInputApplication(uint32_t application_type); // Check esp_zigbee_zcl_binary_input.h for application type values bool setBinaryInputDescription(const char *description); // Set the application type and description for the binary output - // bool setBinaryOutputApplication(uint32_t application_type); // Check esp_zigbee_zcl_binary_output.h for application type values - // bool setBinaryOutputDescription(const char *description); + bool setBinaryOutputApplication(uint32_t application_type); // Check esp_zigbee_zcl_binary_output.h for application type values + bool setBinaryOutputDescription(const char *description); // Use to set a cb function to be called on binary output change - // void onBinaryOutputChange(void (*callback)(bool binary_output)) { - // _on_binary_output_change = callback; - // } + void onBinaryOutputChange(void (*callback)(bool binary_output)) { + _on_binary_output_change = callback; + } - // Set the binary input value + // Set the binary input/output value bool setBinaryInput(bool input); + bool setBinaryOutput(bool output); + + // Get the Binary Output value + bool getBinaryOutput() { + return _output_state; + } - // Report Binary Input value + // Report Binary Input/Output value bool reportBinaryInput(); + bool reportBinaryOutput(); private: - // void zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) override; + void zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) override; - // void (*_on_binary_output_change)(bool); - // void binaryOutputChanged(bool binary_output); + void (*_on_binary_output_change)(bool); + void binaryOutputChanged(); + bool _output_state; uint8_t _binary_clusters; };