diff --git a/CMakeLists.txt b/CMakeLists.txt index f21183ee11e..b3194feaa83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -306,6 +306,7 @@ set(ARDUINO_LIBRARY_Zigbee_SRCS libraries/Zigbee/src/ep/ZigbeeBinary.cpp libraries/Zigbee/src/ep/ZigbeePowerOutlet.cpp libraries/Zigbee/src/ep/ZigbeeFanControl.cpp + libraries/Zigbee/src/ep/ZigbeeMultistate.cpp ) set(ARDUINO_LIBRARY_BLE_SRCS diff --git a/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/README.md b/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/README.md new file mode 100644 index 00000000000..181e303938a --- /dev/null +++ b/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/README.md @@ -0,0 +1,90 @@ +# Arduino-ESP32 Zigbee Multistate Input Output Example + +This example shows how to configure the Zigbee end device and use it as a Home Automation (HA) multistate input/output device. + +# Supported Targets + +Currently, this example supports the following targets. + +| Supported Targets | ESP32-C6 | ESP32-H2 | +| ----------------- | -------- | -------- | + +## Multistate Device Functions + +This example demonstrates two different multistate devices: + +1. **Standard Multistate Device** (`zbMultistateDevice`): Uses predefined application states from the Zigbee specification + - Application Type 0: Fan states (Off, On, Auto) + - Application Type 7: Light states (High, Normal, Low) + +2. **Custom Multistate Device** (`zbMultistateDeviceCustom`): Uses user-defined custom states + - Custom fan states: Off, On, UltraSlow, Slow, Fast, SuperFast + +* After this board first starts up, it will be configured as two multistate devices with different state configurations. +* By clicking the button (BOOT) on this board, the devices will cycle through their respective states and report the changes to the network. + +## Hardware Required + +* A USB cable for power supply and programming + +### Configure the Project + +Set the Button GPIO by changing the `button` variable. By default, it's the pin `BOOT_PIN` (BOOT button on ESP32-C6 and ESP32-H2). + +The example creates two multistate devices: +- **Endpoint 1**: Standard multistate device using predefined Zigbee application types +- **Endpoint 2**: Custom multistate device using user-defined states + +You can modify the state names and configurations by changing the following variables: +- `multistate_custom_state_names[]`: Array of custom state names +- Application type and lenght macros: `ZB_MULTISTATE_APPLICATION_TYPE_X_STATE_NAMES`, +`ZB_MULTISTATE_APPLICATION_TYPE_X_NUM_STATES`, `ZB_MULTISTATE_APPLICATION_TYPE_X_INDEX` +- Device descriptions and application types in the setup() function + +#### Using Arduino IDE + +To get more information about the Espressif boards see [Espressif Development Kits](https://www.espressif.com/en/products/devkits). + +* Before Compile/Verify, select the correct board: `Tools -> Board`. +* Select the End device Zigbee mode: `Tools -> Zigbee mode: Zigbee ED (end device)` +* Select Partition Scheme for Zigbee: `Tools -> Partition Scheme: Zigbee 4MB with spiffs` +* Select the COM port: `Tools -> Port: xxx` where the `xxx` is the detected COM port. +* Optional: Set debug level to verbose to see all logs from Zigbee stack: `Tools -> Core Debug Level: Verbose`. + +## Troubleshooting + +If the End device flashed with this example is not connecting to the coordinator, erase the flash of the End device before flashing the example to the board. It is recommended to do this if you re-flash the coordinator. +You can do the following: + +* In the Arduino IDE go to the Tools menu and set `Erase All Flash Before Sketch Upload` to `Enabled`. +* Add to the sketch `Zigbee.factoryReset();` to reset the device and Zigbee stack. + +By default, the coordinator network is closed after rebooting or flashing new firmware. +To open the network you have 2 options: + +* Open network after reboot by setting `Zigbee.setRebootOpenNetwork(time);` before calling `Zigbee.begin();`. +* In application you can anytime call `Zigbee.openNetwork(time);` to open the network for devices to join. + +***Important: Make sure you are using a good quality USB cable and that you have a reliable power source*** + +* **LED not blinking:** Check the wiring connection and the IO selection. +* **Programming Fail:** If the programming/flash procedure fails, try reducing the serial connection speed. +* **COM port not detected:** Check the USB cable and the USB to Serial driver installation. + +If the error persists, you can ask for help at the official [ESP32 forum](https://esp32.com) or see [Contribute](#contribute). + +## Contribute + +To know how to contribute to this project, see [How to contribute.](https://github.com/espressif/arduino-esp32/blob/master/CONTRIBUTING.rst) + +If you have any **feedback** or **issue** to report on this example/library, please open an issue or fix it by creating a new PR. Contributions are more than welcome! + +Before creating a new issue, be sure to try Troubleshooting and check if the same issue was already created by someone else. + +## Resources + +* Official ESP32 Forum: [Link](https://esp32.com) +* Arduino-ESP32 Official Repository: [espressif/arduino-esp32](https://github.com/espressif/arduino-esp32) +* ESP32-C6 Datasheet: [Link to datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-c6_datasheet_en.pdf) +* ESP32-H2 Datasheet: [Link to datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-h2_datasheet_en.pdf) +* Official ESP-IDF documentation: [ESP-IDF](https://idf.espressif.com) diff --git a/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/Zigbee_Multistate_Input_Output.ino b/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/Zigbee_Multistate_Input_Output.ino new file mode 100644 index 00000000000..21416bbf7d7 --- /dev/null +++ b/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/Zigbee_Multistate_Input_Output.ino @@ -0,0 +1,188 @@ +// 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. + +/** + * @brief This example demonstrates Zigbee multistate input / output device. + * + * The example demonstrates how to use Zigbee library to create a end device multistate device. + * In the example, we have two multistate devices: + * - zbMultistateDevice: uses defined application states from Zigbee specification + * - zbMultistateDeviceCustom: uses custom application states (user defined) + * + * Proper Zigbee mode must be selected in Tools->Zigbee mode + * and also the correct partition scheme must be selected in Tools->Partition Scheme. + * + * Please check the README.md for instructions and more detailed description. + * + * Created by Jan Procházka (https://github.com/P-R-O-C-H-Y/) + * Modified by Pat Clay + */ + +#ifndef ZIGBEE_MODE_ZCZR +#error "Zigbee coordinator/router device mode is not selected in Tools->Zigbee mode" +#endif + +#include "Zigbee.h" + +/* Zigbee multistate device configuration */ +#define MULTISTATE_DEVICE_ENDPOINT_NUMBER 1 + +uint8_t button = BOOT_PIN; + +// zbMultistateDevice will use defined application states +ZigbeeMultistate zbMultistateDevice = ZigbeeMultistate(MULTISTATE_DEVICE_ENDPOINT_NUMBER); + +// zbMultistateDeviceCustom will use custom application states (user defined) +ZigbeeMultistate zbMultistateDeviceCustom = ZigbeeMultistate(MULTISTATE_DEVICE_ENDPOINT_NUMBER + 1); + +const char* multistate_custom_state_names[6] = {"Off", "On", "UltraSlow", "Slow", "Fast", "SuperFast"}; + +void onStateChange(uint16_t state) { + // print the state + Serial.printf("Received state change: %d\r\n", state); + // print the state name using the stored state names + const char* const* state_names = zbMultistateDevice.getMultistateOutputStateNames(); + if (state_names && state < zbMultistateDevice.getMultistateOutputStateNamesLength()) { + Serial.printf("State name: %s\r\n", state_names[state]); + } + // print state index of possible options + Serial.printf("State index: %d / %d\r\n", state, zbMultistateDevice.getMultistateOutputStateNamesLength() - 1); +} + +void onStateChangeCustom(uint16_t state) { + // print the state + Serial.printf("Received state change: %d\r\n", state); + // print the state name using the stored state names + const char* const* state_names = zbMultistateDeviceCustom.getMultistateOutputStateNames(); + if (state_names && state < zbMultistateDeviceCustom.getMultistateOutputStateNamesLength()) { + Serial.printf("State name: %s\r\n", state_names[state]); + } + // print state index of possible options + Serial.printf("State index: %d / %d\r\n", state, zbMultistateDevice.getMultistateOutputStateNamesLength() - 1); + + Serial.print("Changing to fan mode to: "); + switch (state) { + case 0: + Serial.println("Off"); + break; + case 1: + Serial.println("On"); + break; + case 2: + Serial.println("Slow"); + break; + case 3: + Serial.println("Medium"); + break; + case 4: + Serial.println("Fast"); + break; + default: + Serial.println("Invalid state"); + break; + } +} + +void setup() { + Serial.begin(115200); + + // Init button switch + pinMode(button, INPUT_PULLUP); + + // Optional: set Zigbee device name and model + zbMultistateDevice.setManufacturerAndModel("Espressif", "ZigbeeMultistateDevice"); + + // Set up analog input + zbMultistateDevice.addMultistateInput(); + zbMultistateDevice.setMultistateInputApplication(ZB_MULTISTATE_APPLICATION_TYPE_0_INDEX); + zbMultistateDevice.setMultistateInputDescription("Fan (on/off/auto)"); + zbMultistateDevice.setMultistateInputStates(ZB_MULTISTATE_APPLICATION_TYPE_0_STATE_NAMES, ZB_MULTISTATE_APPLICATION_TYPE_0_NUM_STATES); + + // Set up analog output + zbMultistateDevice.addMultistateOutput(); + zbMultistateDevice.setMultistateOutputApplication(ZB_MULTISTATE_APPLICATION_TYPE_7_INDEX); + zbMultistateDevice.setMultistateOutputDescription("Light (high/normal/low)"); + zbMultistateDevice.setMultistateOutputStates(ZB_MULTISTATE_APPLICATION_TYPE_7_STATE_NAMES, ZB_MULTISTATE_APPLICATION_TYPE_7_NUM_STATES); + + // Set up custom output + zbMultistateDeviceCustom.addMultistateOutput(); + zbMultistateDeviceCustom.setMultistateOutputApplication(ZB_MULTISTATE_APPLICATION_TYPE_OTHER_INDEX); + zbMultistateDeviceCustom.setMultistateOutputDescription("Fan (on/off/slow/medium/fast)"); + zbMultistateDeviceCustom.setMultistateOutputStates(multistate_custom_state_names, 5); + + // Set callback function for multistate output change + zbMultistateDevice.onMultistateOutputChange(onStateChange); + zbMultistateDeviceCustom.onMultistateOutputChange(onStateChangeCustom); + + // Add endpoints to Zigbee Core + Zigbee.addEndpoint(&zbMultistateDevice); + Zigbee.addEndpoint(&zbMultistateDeviceCustom); + + Serial.println("Starting Zigbee..."); + // When all EPs are registered, start Zigbee in Router Device mode + if (!Zigbee.begin(ZIGBEE_ROUTER)) { + Serial.println("Zigbee failed to start!"); + Serial.println("Rebooting..."); + ESP.restart(); + } else { + Serial.println("Zigbee started successfully!"); + } + Serial.println("Connecting to network"); + while (!Zigbee.connected()) { + Serial.print("."); + delay(100); + } + Serial.println("Connected"); +} + +void loop() { + static uint32_t timeCounter = 0; + + // Checking button for factory reset and reporting + if (digitalRead(button) == LOW) { // Push button pressed + // Key debounce handling + delay(100); + int startTime = millis(); + while (digitalRead(button) == LOW) { + delay(50); + if ((millis() - startTime) > 3000) { + // If key pressed for more than 3secs, factory reset Zigbee and reboot + Serial.println("Resetting Zigbee to factory and rebooting in 1s."); + delay(1000); + Zigbee.factoryReset(); + } + } + // For demonstration purposes, increment the multistate output/input value by 1 + if (zbMultistateDevice.getMultistateOutput() < zbMultistateDevice.getMultistateOutputStateNamesLength() - 1) { + zbMultistateDevice.setMultistateOutput(zbMultistateDevice.getMultistateOutput() + 1); + zbMultistateDevice.reportMultistateOutput(); + zbMultistateDevice.setMultistateInput(zbMultistateDevice.getMultistateOutput()); + zbMultistateDevice.reportMultistateInput(); + } else { + zbMultistateDevice.setMultistateOutput(0); + zbMultistateDevice.reportMultistateOutput(); + zbMultistateDevice.setMultistateInput(zbMultistateDevice.getMultistateOutput()); + zbMultistateDevice.reportMultistateInput(); + } + + if (zbMultistateDeviceCustom.getMultistateOutput() < zbMultistateDeviceCustom.getMultistateOutputStateNamesLength() - 1) { + zbMultistateDeviceCustom.setMultistateOutput(zbMultistateDeviceCustom.getMultistateOutput() + 1); + zbMultistateDeviceCustom.reportMultistateOutput(); + } else { + zbMultistateDeviceCustom.setMultistateOutput(0); + zbMultistateDeviceCustom.reportMultistateOutput(); + } + } + delay(100); +} diff --git a/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/ci.json b/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/ci.json new file mode 100644 index 00000000000..15d6190e4ae --- /dev/null +++ b/libraries/Zigbee/examples/Zigbee_Multistate_Input_Output/ci.json @@ -0,0 +1,6 @@ +{ + "fqbn_append": "PartitionScheme=zigbee_zczr,ZigbeeMode=zczr", + "requires": [ + "CONFIG_ZB_ENABLED=y" + ] +} diff --git a/libraries/Zigbee/keywords.txt b/libraries/Zigbee/keywords.txt index 23f3af3bf02..7e7ab8a5a81 100644 --- a/libraries/Zigbee/keywords.txt +++ b/libraries/Zigbee/keywords.txt @@ -26,6 +26,7 @@ ZigbeeFlowSensor KEYWORD1 ZigbeeGateway KEYWORD1 ZigbeeIlluminanceSensor KEYWORD1 ZigbeeLight KEYWORD1 +ZigbeeMultistate KEYWORD1 ZigbeeOccupancySensor KEYWORD1 ZigbeePM25Sensor KEYWORD1 ZigbeePowerOutlet KEYWORD1 @@ -222,6 +223,27 @@ getFanMode KEYWORD2 getFanModeSequence KEYWORD2 onFanModeChange KEYWORD2 +# ZigbeeMultistate +addMultistateInput KEYWORD2 +addMultistateOutput KEYWORD2 +onMultistateOutputChange KEYWORD2 +setMultistateInput KEYWORD2 +getMultistateInput KEYWORD2 +setMultistateOutput KEYWORD2 +getMultistateOutput KEYWORD2 +reportMultistateInput KEYWORD2 +reportMultistateOutput KEYWORD2 +setMultistateInputApplication KEYWORD2 +setMultistateInputDescription KEYWORD2 +setMultistateInputStates KEYWORD2 +setMultistateOutputApplication KEYWORD2 +setMultistateOutputDescription KEYWORD2 +setMultistateOutputStates KEYWORD2 +getMultistateInputStateNames KEYWORD2 +getMultistateInputStateNamesLength KEYWORD2 +getMultistateOutputStateNames KEYWORD2 +getMultistateOutputStateNamesLength KEYWORD2 + ####################################### # Constants (LITERAL1) ####################################### @@ -236,3 +258,42 @@ ZIGBEE_DEFAULT_RADIO_CONFIG LITERAL1 ZIGBEE_DEFAULT_UART_RCP_RADIO_CONFIG LITERAL1 ZIGBEE_DEFAULT_HOST_CONFIG LITERAL1 ZB_ARRAY_LENGHT LITERAL1 + +# ZigbeeMultistate +ZB_MULTISTATE_APPLICATION_TYPE_0_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_0_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_0_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_1_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_1_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_1_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_2_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_2_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_2_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_3_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_3_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_3_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_4_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_4_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_4_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_5_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_5_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_5_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_6_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_6_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_6_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_7_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_7_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_7_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_8_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_8_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_8_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_9_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_9_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_9_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_10_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_10_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_10_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_11_INDEX LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_11_NUM_STATES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_11_STATE_NAMES LITERAL1 +ZB_MULTISTATE_APPLICATION_TYPE_OTHER_INDEX LITERAL1 diff --git a/libraries/Zigbee/src/Zigbee.h b/libraries/Zigbee/src/Zigbee.h index 65c9e7f0daa..3ee7eefcfe9 100644 --- a/libraries/Zigbee/src/Zigbee.h +++ b/libraries/Zigbee/src/Zigbee.h @@ -28,6 +28,7 @@ #include "ep/ZigbeeElectricalMeasurement.h" #include "ep/ZigbeeFlowSensor.h" #include "ep/ZigbeeIlluminanceSensor.h" +#include "ep/ZigbeeMultistate.h" #include "ep/ZigbeeOccupancySensor.h" #include "ep/ZigbeePM25Sensor.h" #include "ep/ZigbeePressureSensor.h" diff --git a/libraries/Zigbee/src/ep/ZigbeeMultistate.cpp b/libraries/Zigbee/src/ep/ZigbeeMultistate.cpp new file mode 100644 index 00000000000..f11c5804053 --- /dev/null +++ b/libraries/Zigbee/src/ep/ZigbeeMultistate.cpp @@ -0,0 +1,395 @@ +#include "ZigbeeMultistate.h" +#if CONFIG_ZB_ENABLED + +ZigbeeMultistate::ZigbeeMultistate(uint8_t endpoint) : ZigbeeEP(endpoint) { + _device_id = ESP_ZB_HA_SIMPLE_SENSOR_DEVICE_ID; + + //Create basic multistate clusters without configuration + _cluster_list = esp_zb_zcl_cluster_list_create(); + esp_zb_cluster_list_add_basic_cluster(_cluster_list, esp_zb_basic_cluster_create(NULL), ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + esp_zb_cluster_list_add_identify_cluster(_cluster_list, esp_zb_identify_cluster_create(NULL), ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + + _ep_config = {.endpoint = _endpoint, .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID, .app_device_id = ESP_ZB_HA_SIMPLE_SENSOR_DEVICE_ID, .app_device_version = 0}; + + // Initialize member variables + _multistate_clusters = 0; + _input_state = 0; + _output_state = 0; + _input_state_names = nullptr; + _input_state_names_length = 0; + _output_state_names = nullptr; + _output_state_names_length = 0; + _on_multistate_output_change = nullptr; +} + +bool ZigbeeMultistate::addMultistateInput() { + esp_zb_multistate_input_cluster_cfg_t multistate_input_cfg = { + .number_of_states = 3, + .out_of_service = false, + .present_value = 0, + .status_flags = ESP_ZB_ZCL_MULTI_VALUE_STATUS_FLAGS_NORMAL + }; + + esp_zb_attribute_list_t *multistate_input_cluster = esp_zb_multistate_input_cluster_create(&multistate_input_cfg); + + // Create default description for Multistate Input + char default_description[] = "\x10" // Size of the description text + "Multistate Input"; // Description text + uint32_t application_type = 0x00000000 | (0x0D << 24); // Application type + const char* state_text[] = { "Off", "On", "Auto" }; // State text array + + esp_err_t ret = esp_zb_multistate_input_cluster_add_attr(multistate_input_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_INPUT_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_multistate_input_cluster_add_attr(multistate_input_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_INPUT_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_multistate_input_cluster_add_attr(multistate_input_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_INPUT_STATE_TEXT_ID, (void *)state_text); + if (ret != ESP_OK) { + log_e("Failed to add state text attribute: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + + ret = esp_zb_cluster_list_add_multistate_input_cluster(_cluster_list, multistate_input_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + if (ret != ESP_OK) { + log_e("Failed to add Multistate Input cluster: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + + _multistate_clusters |= MULTISTATE_INPUT; + return true; +} + +bool ZigbeeMultistate::addMultistateOutput() { + esp_zb_multistate_output_cluster_cfg_t multistate_output_cfg = { + .number_of_states = 3, + .out_of_service = false, + .present_value = 0, + .status_flags = ESP_ZB_ZCL_MULTI_VALUE_STATUS_FLAGS_NORMAL + }; + + esp_zb_attribute_list_t *multistate_output_cluster = esp_zb_multistate_output_cluster_create(&multistate_output_cfg); + + // Create default description for Multistate Output + char default_description[] = "\x11" // Size of the description text + "Multistate Output"; // Description text + uint32_t application_type = 0x00000000 | (0x0E << 24); + const char* state_text[] = { "Off", "On", "Auto" }; // State text array + uint16_t num_states = 3; // Number of states + + esp_err_t ret = esp_zb_multistate_output_cluster_add_attr(multistate_output_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_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_multistate_output_cluster_add_attr(multistate_output_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_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_multistate_output_cluster_add_attr(multistate_output_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_OUTPUT_STATE_TEXT_ID, (void *)state_text); + if (ret != ESP_OK) { + log_e("Failed to add state text attribute: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + + ret = esp_zb_cluster_list_add_multistate_output_cluster(_cluster_list, multistate_output_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + if (ret != ESP_OK) { + log_e("Failed to add Multistate Output cluster: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + + _multistate_clusters |= MULTISTATE_OUTPUT; + return true; +} + +bool ZigbeeMultistate::setMultistateInputApplication(uint32_t application_type) { + if (!(_multistate_clusters & MULTISTATE_INPUT)) { + log_e("Multistate Input cluster not added"); + return false; + } + + // Add the Multistate Input group ID (0x0D) to the application type + uint32_t application_type_value = (0x0D << 24) | application_type; + + esp_zb_attribute_list_t *multistate_input_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + esp_err_t ret = esp_zb_cluster_update_attr(multistate_input_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_INPUT_APPLICATION_TYPE_ID, (void *)&application_type_value); + if (ret != ESP_OK) { + log_e("Failed to set Multistate Input application type: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + return true; +} + +bool ZigbeeMultistate::setMultistateOutputApplication(uint32_t application_type) { + if (!(_multistate_clusters & MULTISTATE_OUTPUT)) { + log_e("Multistate Output cluster not added"); + return false; + } + + // Add the Multistate Output group ID (0x0E) to the application type + uint32_t application_type_value = (0x0E << 24) | application_type; + + esp_zb_attribute_list_t *multistate_output_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + esp_err_t ret = esp_zb_cluster_update_attr(multistate_output_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_OUTPUT_APPLICATION_TYPE_ID, (void *)&application_type_value); + if (ret != ESP_OK) { + log_e("Failed to set Multistate Output application type: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + return true; +} + +bool ZigbeeMultistate::setMultistateInputDescription(const char *description) { + if (!(_multistate_clusters & MULTISTATE_INPUT)) { + log_e("Multistate Input 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 multistate input cluster + esp_zb_attribute_list_t *multistate_input_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + if (multistate_input_cluster == nullptr) { + log_e("Failed to get multistate input 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(multistate_input_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_INPUT_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; +} + +bool ZigbeeMultistate::setMultistateOutputDescription(const char *description) { + if (!(_multistate_clusters & MULTISTATE_OUTPUT)) { + log_e("Multistate 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 multistate output cluster + esp_zb_attribute_list_t *multistate_output_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + if (multistate_output_cluster == nullptr) { + log_e("Failed to get multistate 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(multistate_output_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_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; +} + +bool ZigbeeMultistate::setMultistateInputStates(const char * const states[], uint16_t states_length) { + if (!(_multistate_clusters & MULTISTATE_INPUT)) { + log_e("Multistate Input cluster not added"); + return false; + } + + esp_zb_attribute_list_t *multistate_input_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + if (multistate_input_cluster == nullptr) { + log_e("Failed to get multistate input cluster"); + return false; + } + + esp_err_t ret = esp_zb_cluster_update_attr(multistate_input_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_INPUT_STATE_TEXT_ID, (void *)states); + if (ret != ESP_OK) { + log_e("Failed to set states text: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + ret = esp_zb_cluster_update_attr(multistate_input_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_INPUT_NUMBER_OF_STATES_ID, (void *)&states_length); + if (ret != ESP_OK) { + log_e("Failed to set number of states: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + + // Store state names locally for getter access + _input_state_names = states; + _input_state_names_length = states_length; + return true; +} + +bool ZigbeeMultistate::setMultistateOutputStates(const char * const states[], uint16_t states_length) { + if (!(_multistate_clusters & MULTISTATE_OUTPUT)) { + log_e("Multistate Output cluster not added"); + return false; + } + + esp_zb_attribute_list_t *multistate_output_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + if (multistate_output_cluster == nullptr) { + log_e("Failed to get multistate output cluster"); + return false; + } + + esp_err_t ret = esp_zb_cluster_update_attr(multistate_output_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_OUTPUT_STATE_TEXT_ID, (void *)states); + if (ret != ESP_OK) { + log_e("Failed to set states text: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + ret = esp_zb_cluster_update_attr(multistate_output_cluster, ESP_ZB_ZCL_ATTR_MULTISTATE_OUTPUT_NUMBER_OF_STATES_ID, (void *)&states_length); + if (ret != ESP_OK) { + log_e("Failed to set number of states: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + + // Store state names locally for getter access + _output_state_names = states; + _output_state_names_length = states_length; + return true; +} + +//set attribute method -> method overridden in child class +void ZigbeeMultistate::zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) { + if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT) { + if (message->attribute.id == ESP_ZB_ZCL_ATTR_MULTISTATE_OUTPUT_PRESENT_VALUE_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) { + _output_state = *(uint16_t *)message->attribute.data.value; + multistateOutputChanged(); + } else { + log_w("Received message ignored. Attribute ID: %d not supported for Multistate Output", message->attribute.id); + } + } else { + log_w("Received message ignored. Cluster ID: %d not supported for Multistate endpoint", message->info.cluster); + } +} + +void ZigbeeMultistate::multistateOutputChanged() { + if (_on_multistate_output_change) { + _on_multistate_output_change(_output_state); + } else { + log_w("No callback function set for multistate output change"); + } +} + +bool ZigbeeMultistate::setMultistateInput(uint16_t state) { + esp_zb_zcl_status_t ret = ESP_ZB_ZCL_STATUS_SUCCESS; + if (!(_multistate_clusters & MULTISTATE_INPUT)) { + log_e("Multistate Input cluster not added"); + return false; + } + log_d("Setting multistate input to %d", state); + esp_zb_lock_acquire(portMAX_DELAY); + ret = esp_zb_zcl_set_attribute_val( + _endpoint, ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_MULTISTATE_INPUT_PRESENT_VALUE_ID, &state, false + ); + esp_zb_lock_release(); + if (ret != ESP_ZB_ZCL_STATUS_SUCCESS) { + log_e("Failed to set multistate input: 0x%x: %s", ret, esp_zb_zcl_status_to_name(ret)); + return false; + } + return true; +} + +bool ZigbeeMultistate::setMultistateOutput(uint16_t state) { + esp_zb_zcl_status_t ret = ESP_ZB_ZCL_STATUS_SUCCESS; + _output_state = state; + multistateOutputChanged(); + + log_v("Updating multistate output to %d", state); + /* Update multistate output */ + esp_zb_lock_acquire(portMAX_DELAY); + ret = esp_zb_zcl_set_attribute_val( + _endpoint, ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_MULTISTATE_OUTPUT_PRESENT_VALUE_ID, &_output_state, false + ); + esp_zb_lock_release(); + + if (ret != ESP_ZB_ZCL_STATUS_SUCCESS) { + log_e("Failed to set multistate output: 0x%x: %s", ret, esp_zb_zcl_status_to_name(ret)); + return false; + } + return true; +} + +bool ZigbeeMultistate::reportMultistateInput() { + /* 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_MULTISTATE_INPUT_PRESENT_VALUE_ID; + report_attr_cmd.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI; + report_attr_cmd.clusterID = ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT; + 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 Multistate Input report: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + log_v("Multistate Input report sent"); + return true; +} + +bool ZigbeeMultistate::reportMultistateOutput() { + /* 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_MULTISTATE_OUTPUT_PRESENT_VALUE_ID; + report_attr_cmd.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI; + report_attr_cmd.clusterID = ESP_ZB_ZCL_CLUSTER_ID_MULTI_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 Multistate Output report: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + log_v("Multistate Output report sent"); + return true; +} + +#endif // CONFIG_ZB_ENABLED diff --git a/libraries/Zigbee/src/ep/ZigbeeMultistate.h b/libraries/Zigbee/src/ep/ZigbeeMultistate.h new file mode 100644 index 00000000000..f2bdf414ba2 --- /dev/null +++ b/libraries/Zigbee/src/ep/ZigbeeMultistate.h @@ -0,0 +1,150 @@ +/* Class of Zigbee Multistate sensor endpoint inherited from common EP class */ + +#pragma once + +#include "soc/soc_caps.h" +#include "sdkconfig.h" +#if CONFIG_ZB_ENABLED + +#include "ZigbeeEP.h" +#include "ha/esp_zigbee_ha_standard.h" + +// Types for Multistate Input/Output +// uint16_t for present value -> index to array of states +// uint16_t for number of states +// uint8_t chars for state names, with max of 16 chars ASCII + +// Multistate Input/Output Application Types Defines +#define ZB_MULTISTATE_APPLICATION_TYPE_0_INDEX 0x0000 +#define ZB_MULTISTATE_APPLICATION_TYPE_0_NUM_STATES 3 +#define ZB_MULTISTATE_APPLICATION_TYPE_0_STATE_NAMES (const char* const[]){"Off", "On", "Auto"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_1_INDEX 0x0001 +#define ZB_MULTISTATE_APPLICATION_TYPE_1_NUM_STATES 4 +#define ZB_MULTISTATE_APPLICATION_TYPE_1_STATE_NAMES (const char* const[]){"Off", "Low", "Medium", "High"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_2_INDEX 0x0002 +#define ZB_MULTISTATE_APPLICATION_TYPE_2_NUM_STATES 7 +#define ZB_MULTISTATE_APPLICATION_TYPE_2_STATE_NAMES (const char* const[]){"Auto", "Heat", "Cool", "Off", "Emergency Heat", "Fan Only", "Max Heat"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_3_INDEX 0x0003 +#define ZB_MULTISTATE_APPLICATION_TYPE_3_NUM_STATES 4 +#define ZB_MULTISTATE_APPLICATION_TYPE_3_STATE_NAMES (const char* const[]){"Occupied", "Unoccupied", "Standby", "Bypass"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_4_INDEX 0x0004 +#define ZB_MULTISTATE_APPLICATION_TYPE_4_NUM_STATES 3 +#define ZB_MULTISTATE_APPLICATION_TYPE_4_STATE_NAMES (const char* const[]){"Inactive", "Active", "Hold"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_5_INDEX 0x0005 +#define ZB_MULTISTATE_APPLICATION_TYPE_5_NUM_STATES 8 +#define ZB_MULTISTATE_APPLICATION_TYPE_5_STATE_NAMES (const char* const[]){"Auto", "Warm-up", "Water Flush", "Autocalibration", "Shutdown Open", "Shutdown Closed", "Low Limit", "Test and Balance"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_6_INDEX 0x0006 +#define ZB_MULTISTATE_APPLICATION_TYPE_6_NUM_STATES 6 +#define ZB_MULTISTATE_APPLICATION_TYPE_6_STATE_NAMES (const char* const[]){"Off", "Auto", "Heat Cool", "Heat Only", "Cool Only", "Fan Only"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_7_INDEX 0x0007 +#define ZB_MULTISTATE_APPLICATION_TYPE_7_NUM_STATES 3 +#define ZB_MULTISTATE_APPLICATION_TYPE_7_STATE_NAMES (const char* const[]){"High", "Normal", "Low"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_8_INDEX 0x0008 +#define ZB_MULTISTATE_APPLICATION_TYPE_8_NUM_STATES 4 +#define ZB_MULTISTATE_APPLICATION_TYPE_8_STATE_NAMES (const char* const[]){"Occupied", "Unoccupied", "Startup", "Shutdown"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_9_INDEX 0x0009 +#define ZB_MULTISTATE_APPLICATION_TYPE_9_NUM_STATES 3 +#define ZB_MULTISTATE_APPLICATION_TYPE_9_STATE_NAMES (const char* const[]){"Night", "Day", "Hold"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_10_INDEX 0x000A +#define ZB_MULTISTATE_APPLICATION_TYPE_10_NUM_STATES 5 +#define ZB_MULTISTATE_APPLICATION_TYPE_10_STATE_NAMES (const char* const[]){"Off", "Cool", "Heat", "Auto", "Emergency Heat"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_11_INDEX 0x000B +#define ZB_MULTISTATE_APPLICATION_TYPE_11_NUM_STATES 7 +#define ZB_MULTISTATE_APPLICATION_TYPE_11_STATE_NAMES (const char* const[]){"Shutdown Closed", "Shutdown Open", "Satisfied", "Mixing", "Cooling", "Heating", "Suppl Heat"} + +#define ZB_MULTISTATE_APPLICATION_TYPE_OTHER_INDEX 0xFFFF + +//enum for bits set to check what multistate cluster were added +enum zigbee_multistate_clusters { + MULTISTATE_INPUT = 1, + MULTISTATE_OUTPUT = 2 +}; + +typedef struct zigbee_multistate_cfg_s { + esp_zb_basic_cluster_cfg_t basic_cfg; + esp_zb_identify_cluster_cfg_t identify_cfg; +} zigbee_multistate_cfg_t; + +class ZigbeeMultistate : public ZigbeeEP { +public: + ZigbeeMultistate(uint8_t endpoint); + ~ZigbeeMultistate() {} + + // Add multistate clusters + bool addMultistateInput(); + bool addMultistateOutput(); + + // Set the application type and description for the multistate input + bool setMultistateInputApplication(uint32_t application_type); // Check esp_zigbee_zcl_multistate_input.h for application type values + bool setMultistateInputDescription(const char *description); + bool setMultistateInputStates(const char * const states[], uint16_t states_length); + + // Set the application type and description for the multistate output + bool setMultistateOutputApplication(uint32_t application_type); // Check esp_zigbee_zcl_multistate_output.h for application type values + bool setMultistateOutputDescription(const char *description); + bool setMultistateOutputStates(const char * const states[], uint16_t states_length); + + // Use to set a cb function to be called on multistate output change + void onMultistateOutputChange(void (*callback)(uint16_t state)) { + _on_multistate_output_change = callback; + } + + // Set the Multistate Input/Output value + bool setMultistateInput(uint16_t state); + bool setMultistateOutput(uint16_t state); + + // Get the Multistate Input/Output values + uint16_t getMultistateInput() { + return _input_state; + } + uint16_t getMultistateOutput() { + return _output_state; + } + + // Get state names and length + const char* const* getMultistateInputStateNames() { + return _input_state_names; + } + uint16_t getMultistateInputStateNamesLength() { + return _input_state_names_length; + } + const char* const* getMultistateOutputStateNames() { + return _output_state_names; + } + uint16_t getMultistateOutputStateNamesLength() { + return _output_state_names_length; + } + + // Report Multistate Input/Output + bool reportMultistateInput(); + bool reportMultistateOutput(); + +private: + void zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) override; + + void (*_on_multistate_output_change)(uint16_t state); + void multistateOutputChanged(); + + uint8_t _multistate_clusters; + uint16_t _output_state; + uint16_t _input_state; + + // Local storage for state names + const char* const* _input_state_names; + uint16_t _input_state_names_length; + const char* const* _output_state_names; + uint16_t _output_state_names_length; +}; + +#endif // CONFIG_ZB_ENABLED