From 254fd8de751ffb040145b233006baba8f22c65b7 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, 14 Apr 2025 12:18:28 +0200
Subject: [PATCH 1/3] feat(zigbee): Add sernsor type and configuration

---
 .../Zigbee_Occupancy_Sensor.ino               |  16 +-
 .../Zigbee/src/ep/ZigbeeOccupancySensor.cpp   | 201 +++++++++++++++++-
 .../Zigbee/src/ep/ZigbeeOccupancySensor.h     |  40 +++-
 3 files changed, 250 insertions(+), 7 deletions(-)

diff --git a/libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino b/libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino
index 46afdf3d273..7316655370e 100644
--- a/libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino
+++ b/libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino
@@ -34,7 +34,7 @@
 #include "Zigbee.h"
 
 /* Zigbee occupancy sensor configuration */
-#define OCCUPANCY_SENSOR_ENDPOINT_NUMBER 10
+#define OCCUPANCY_SENSOR_ENDPOINT_NUMBER 1
 uint8_t button = BOOT_PIN;
 uint8_t sensor_pin = 4;
 
@@ -47,9 +47,21 @@ void setup() {
   pinMode(button, INPUT_PULLUP);
   pinMode(sensor_pin, INPUT);
 
-  // Optional: set Zigbee device name and model
+  // Set Zigbee device name and model
   zbOccupancySensor.setManufacturerAndModel("Espressif", "ZigbeeOccupancyPIRSensor");
 
+  // Optional: Set sensor type (PIR, Ultrasonic, PIR and Ultrasonic or Physical Contact)
+  zbOccupancySensor.setSensorType(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR, ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PIR);
+
+  // Optional: Set occupied to unoccupied delay (if your sensor supports it) to PIR sensor as its set by setSensorType
+  zbOccupancySensor.setOccupiedToUnoccupiedDelay(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR, 1); // 1 second delay
+
+  // Optional: Set unoccupied to occupied delay (if your sensor supports it) to PIR sensor as its set by setSensorType
+  zbOccupancySensor.setUnoccupiedToOccupiedDelay(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR, 1); // 1 second delay
+  
+  // Optional: Set unoccupied to occupied threshold (if your sensor supports it) to PIR sensor as its set by setSensorType
+  zbOccupancySensor.setUnoccupiedToOccupiedThreshold(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR, 1); // 1 movement event threshold
+
   // Add endpoint to Zigbee Core
   Zigbee.addEndpoint(&zbOccupancySensor);
 
diff --git a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.cpp b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.cpp
index b8f88fed4a4..648cba91486 100644
--- a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.cpp
+++ b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.cpp
@@ -22,8 +22,11 @@ ZigbeeOccupancySensor::ZigbeeOccupancySensor(uint8_t endpoint) : ZigbeeEP(endpoi
   _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};
 }
 
-bool ZigbeeOccupancySensor::setSensorType(uint8_t sensor_type) {
-  uint8_t sensor_type_bitmap = 1 << sensor_type;
+bool ZigbeeOccupancySensor::setSensorType(ZigbeeOccupancySensorType sensor_type, ZigbeeOccupancySensorTypeBitmap sensor_type_bitmap) {
+  uint8_t sensor_type_bitmap_value = sensor_type_bitmap;
+  if(sensor_type_bitmap == ZIGBEE_OCCUPANCY_SENSOR_BITMAP_DEFAULT) {
+    sensor_type_bitmap_value = (1 << sensor_type); // Default to single sensor type if bitmap is default
+  }
   esp_zb_attribute_list_t *occupancy_sens_cluster =
     esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
   esp_err_t ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_OCCUPANCY_SENSOR_TYPE_ID, (void *)&sensor_type);
@@ -31,11 +34,152 @@ bool ZigbeeOccupancySensor::setSensorType(uint8_t sensor_type) {
     log_e("Failed to set sensor type: 0x%x: %s", ret, esp_err_to_name(ret));
     return false;
   }
-  ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_OCCUPANCY_SENSOR_TYPE_BITMAP_ID, (void *)&sensor_type_bitmap);
+  ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_OCCUPANCY_SENSOR_TYPE_BITMAP_ID, (void *)&sensor_type_bitmap_value);
   if (ret != ESP_OK) {
     log_e("Failed to set sensor type bitmap: 0x%x: %s", ret, esp_err_to_name(ret));
     return false;
   }
+
+  // Handle PIR attributes if PIR bit is set (bit 0)
+  if (sensor_type_bitmap & ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PIR) {
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_OCC_TO_UNOCC_DELAY_ID, ESP_ZB_ZCL_OCCUPANCY_SENSING_PIR_OCC_TO_UNOCC_DELAY_DEFAULT_VALUE);
+    if (ret != ESP_OK) {
+      log_e("Failed to set PIR occupied to unoccupied delay: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_DELAY_ID, ESP_ZB_ZCL_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_DELAY_DEFAULT_VALUE);
+    if (ret != ESP_OK) {
+      log_e("Failed to set PIR unoccupied to occupied delay: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+    uint8_t pir_threshold = ESP_ZB_ZCL_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_THRESHOLD_DEFAULT_VALUE;
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_THRESHOLD_ID, &pir_threshold);
+    if (ret != ESP_OK) {
+      log_e("Failed to set PIR unoccupied to occupied threshold: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+  }
+
+  // Handle Ultrasonic attributes if Ultrasonic bit is set (bit 1)
+  if (sensor_type_bitmap & ZIGBEE_OCCUPANCY_SENSOR_BITMAP_ULTRASONIC) {
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_OCCUPIED_TO_UNOCCUPIED_DELAY_ID, ESP_ZB_ZCL_OCCUPANCY_SENSING_ULTRASONIC_OCCUPIED_TO_UNOCCUPIED_DELAY_DEFAULT_VALUE);
+    if (ret != ESP_OK) {
+      log_e("Failed to set ultrasonic occupied to unoccupied delay: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_DELAY_ID, ESP_ZB_ZCL_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_DELAY_DEFAULT_VALUE);
+    if (ret != ESP_OK) {
+      log_e("Failed to set ultrasonic unoccupied to occupied delay: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+    uint8_t ultrasonic_threshold = ESP_ZB_ZCL_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_DEFAULT_VALUE;
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_ID, &ultrasonic_threshold);
+    if (ret != ESP_OK) {
+      log_e("Failed to set ultrasonic unoccupied to occupied threshold: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+  }
+
+  // Handle Physical Contact attributes if Physical Contact bit is set (bit 2)
+  if (sensor_type_bitmap & ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PHYSICAL_CONTACT_AND_PIR) {
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_OCCUPIED_TO_UNOCCUPIED_DELAY_ID, ESP_ZB_ZCL_OCCUPANCY_SENSING_PHYSICAL_CONTACT_OCCUPIED_TO_UNOCCUPIED_DELAY_DEFAULT_VALUE);
+    if (ret != ESP_OK) {
+      log_e("Failed to set physical contact occupied to unoccupied delay: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_DELAY_ID, ESP_ZB_ZCL_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_DELAY_DEFAULT_VALUE);
+    if (ret != ESP_OK) {
+      log_e("Failed to set physical contact unoccupied to occupied delay: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+    uint8_t physical_contact_threshold = ESP_ZB_ZCL_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_MIN_VALUE;
+    ret = esp_zb_occupancy_sensing_cluster_add_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_ID, &physical_contact_threshold);
+    if (ret != ESP_OK) {
+      log_e("Failed to set physical contact unoccupied to occupied threshold: 0x%x: %s", ret, esp_err_to_name(ret));
+      return false;
+    }
+  }
+  return true;
+}
+
+bool ZigbeeOccupancySensor::setOccupiedToUnoccupiedDelay(ZigbeeOccupancySensorType sensor_type, uint16_t delay) {
+  esp_zb_attribute_list_t *occupancy_sens_cluster =
+    esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
+  
+  esp_err_t ret;
+  switch (sensor_type) {
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_OCC_TO_UNOCC_DELAY_ID, (void *)&delay);
+      break;
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_OCCUPIED_TO_UNOCCUPIED_DELAY_ID, (void *)&delay);
+      break;
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_OCCUPIED_TO_UNOCCUPIED_DELAY_ID, (void *)&delay);
+      break;
+    default:
+      log_e("Invalid sensor type for delay setting: 0x%x", sensor_type);
+      return false;
+  }
+  
+  if (ret != ESP_OK) {
+    log_e("Failed to set occupied to unoccupied delay: 0x%x: %s", ret, esp_err_to_name(ret));
+    return false;
+  }
+  return true;
+}
+
+bool ZigbeeOccupancySensor::setUnoccupiedToOccupiedDelay(ZigbeeOccupancySensorType sensor_type, uint16_t delay) {
+  esp_zb_attribute_list_t *occupancy_sens_cluster =
+    esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
+  
+  esp_err_t ret;
+  switch (sensor_type) {
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_DELAY_ID, (void *)&delay);
+      break;
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_DELAY_ID, (void *)&delay);
+      break;
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_DELAY_ID, (void *)&delay);
+      break;
+    default:
+      log_e("Invalid sensor type for delay setting: 0x%x", sensor_type);
+      return false;
+  }
+  
+  if (ret != ESP_OK) {
+    log_e("Failed to set unoccupied to occupied delay: 0x%x: %s", ret, esp_err_to_name(ret));
+    return false;
+  }
+  return true;
+}
+
+bool ZigbeeOccupancySensor::setUnoccupiedToOccupiedThreshold(ZigbeeOccupancySensorType sensor_type, uint8_t threshold) {
+  esp_zb_attribute_list_t *occupancy_sens_cluster =
+    esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
+  
+  esp_err_t ret;
+  switch (sensor_type) {
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_THRESHOLD_ID, (void *)&threshold);
+      break;
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_ID, (void *)&threshold);
+      break;
+    case ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT:
+      ret = esp_zb_cluster_update_attr(occupancy_sens_cluster, ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_ID, (void *)&threshold);
+      break;
+    default:
+      log_e("Invalid sensor type for threshold setting: 0x%x", sensor_type);
+      return false;
+  }
+  
+  if (ret != ESP_OK) {
+    log_e("Failed to set unoccupied to occupied threshold: 0x%x: %s", ret, esp_err_to_name(ret));
+    return false;
+  }
   return true;
 }
 
@@ -77,4 +221,55 @@ bool ZigbeeOccupancySensor::report() {
   return true;
 }
 
+//set attribute method -> method overridden in child class
+void ZigbeeOccupancySensor::zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) {
+  if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING) {
+    //PIR
+    if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_OCC_TO_UNOCC_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
+      uint16_t pir_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR);
+    } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
+      uint16_t pir_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR);
+    } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_THRESHOLD_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U8) {
+      uint8_t pir_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR);
+    } 
+    //Ultrasonic
+    else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_OCCUPIED_TO_UNOCCUPIED_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
+      uint16_t ultrasonic_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC);
+    } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
+      uint16_t ultrasonic_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC);
+    } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U8) {
+      uint8_t ultrasonic_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC);
+    } 
+    //Physical Contact  
+    else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_OCCUPIED_TO_UNOCCUPIED_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
+      uint16_t physical_contact_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT);
+    } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
+      uint16_t physical_contact_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT);
+    } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U8) {
+      uint8_t physical_contact_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
+      occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT);
+    } else {
+      log_w("Received message ignored. Attribute ID: %d not supported for Occupancy Sensor endpoint", message->attribute.id);
+    }
+  } else {
+    log_w("Received message ignored. Cluster ID: %d not supported for Occupancy Sensor endpoint", message->info.cluster);
+  }
+}
+
+void ZigbeeOccupancySensor::occupancyConfigChanged(ZigbeeOccupancySensorType sensor_type) {
+  if (_on_occupancy_config_change) {
+    _on_occupancy_config_change(sensor_type); //sensor type, delay, delay, threshold
+  } else {
+    log_w("No callback function set for occupancy config change");
+  }
+}
+
 #endif  // CONFIG_ZB_ENABLED
diff --git a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
index 7408e10a76b..69a2ca0634b 100644
--- a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
+++ b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
@@ -30,6 +30,24 @@
   }
 // clang-format on
 
+enum ZigbeeOccupancySensorType{
+  ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR = 0,
+  ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC = 1,
+  ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR_AND_ULTRASONIC = 2,
+  ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT = 3
+};
+
+enum ZigbeeOccupancySensorTypeBitmap{
+  ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PIR = 0x01,
+  ZIGBEE_OCCUPANCY_SENSOR_BITMAP_ULTRASONIC = 0x02,
+  ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PIR_AND_ULTRASONIC = 0x03,
+  // ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PHYSICAL_CONTACT = 0x04, // No info in cluster specification R8
+  ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PHYSICAL_CONTACT_AND_PIR = 0x05,
+  ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PHYSICAL_CONTACT_AND_ULTRASONIC = 0x06,
+  ZIGBEE_OCCUPANCY_SENSOR_BITMAP_PHYSICAL_CONTACT_AND_PIR_AND_ULTRASONIC = 0x07,
+  ZIGBEE_OCCUPANCY_SENSOR_BITMAP_DEFAULT = 0xff
+};
+
 typedef struct zigbee_occupancy_sensor_cfg_s {
   esp_zb_basic_cluster_cfg_t basic_cfg;
   esp_zb_identify_cluster_cfg_t identify_cfg;
@@ -44,11 +62,29 @@ class ZigbeeOccupancySensor : public ZigbeeEP {
   // Set the occupancy value. True for occupied, false for unoccupied
   bool setOccupancy(bool occupied);
 
-  // Set the sensor type, see esp_zb_zcl_occupancy_sensing_occupancy_sensor_type_t
-  bool setSensorType(uint8_t sensor_type);
+  // Set the sensor type, see ZigbeeOccupancySensorType
+  bool setSensorType(ZigbeeOccupancySensorType sensor_type, ZigbeeOccupancySensorTypeBitmap sensor_type_bitmap = ZIGBEE_OCCUPANCY_SENSOR_BITMAP_DEFAULT);
+
+  // Set the occupied to unoccupied delay
+  // Specifies the time delay, in seconds, before the sensor changes to its unoccupied state after the last detection of movement in the sensed area.
+  bool setOccupiedToUnoccupiedDelay(ZigbeeOccupancySensorType sensor_type, uint16_t delay);
+
+  // Set the unoccupied to occupied delay
+  // Specifies the time delay, in seconds, before the sensor changes to its occupied state after the detection of movement in the sensed area.
+  bool setUnoccupiedToOccupiedDelay(ZigbeeOccupancySensorType sensor_type, uint16_t delay);
+
+  // Set the unoccupied to occupied threshold
+  // Specifies the number of movement detection events that must occur in the period unoccupied to occupied delay, before the sensor changes to its occupied state.
+  bool setUnoccupiedToOccupiedThreshold(ZigbeeOccupancySensorType sensor_type, uint8_t threshold);
 
   // Report the occupancy value
   bool report();
+
+private:
+  void zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) override;
+
+  void (*_on_occupancy_config_change)(bool);
+  void occupancyConfigChanged(ZigbeeOccupancySensorType sensor_type);
 };
 
 #endif  // CONFIG_ZB_ENABLED

From ff76843cbad5ec796f5dc6e052dc6eb5a30e4f22 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, 14 Apr 2025 12:34:42 +0200
Subject: [PATCH 2/3] feat(zigbee): Add callback for config change

---
 .../Zigbee_Occupancy_Sensor.ino               |  9 +++++
 .../Zigbee/src/ep/ZigbeeOccupancySensor.cpp   | 33 +++++++++++++------
 .../Zigbee/src/ep/ZigbeeOccupancySensor.h     | 19 +++++++++--
 3 files changed, 49 insertions(+), 12 deletions(-)

diff --git a/libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino b/libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino
index 7316655370e..a7208b744d2 100644
--- a/libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino
+++ b/libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino
@@ -62,6 +62,9 @@ void setup() {
   // Optional: Set unoccupied to occupied threshold (if your sensor supports it) to PIR sensor as its set by setSensorType
   zbOccupancySensor.setUnoccupiedToOccupiedThreshold(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR, 1); // 1 movement event threshold
 
+  // Optional: Set callback function for occupancy config change
+  zbOccupancySensor.onOccupancyConfigChange(occupancyConfigChange);
+
   // Add endpoint to Zigbee Core
   Zigbee.addEndpoint(&zbOccupancySensor);
 
@@ -113,3 +116,9 @@ void loop() {
   }
   delay(100);
 }
+
+// Callback function for occupancy config change
+void occupancyConfigChange(ZigbeeOccupancySensorType sensor_type, uint16_t occ_to_unocc_delay, uint16_t unocc_to_occ_delay, uint8_t unocc_to_occ_threshold) {
+  // Handle sensor configuration here
+  Serial.printf("Occupancy config change: sensor type: %d, occ to unocc delay: %d, unocc to occ delay: %d, unocc to occ threshold: %d\n", sensor_type, occ_to_unocc_delay, unocc_to_occ_delay, unocc_to_occ_threshold);
+}
\ No newline at end of file
diff --git a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.cpp b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.cpp
index 648cba91486..7f8c4647779 100644
--- a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.cpp
+++ b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.cpp
@@ -226,35 +226,35 @@ void ZigbeeOccupancySensor::zbAttributeSet(const esp_zb_zcl_set_attr_value_messa
   if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING) {
     //PIR
     if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_OCC_TO_UNOCC_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
-      uint16_t pir_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
+      _pir_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR);
     } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
-      uint16_t pir_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
+      _pir_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR);
     } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PIR_UNOCC_TO_OCC_THRESHOLD_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U8) {
-      uint8_t pir_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
+      _pir_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR);
     } 
     //Ultrasonic
     else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_OCCUPIED_TO_UNOCCUPIED_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
-      uint16_t ultrasonic_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
+      _ultrasonic_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC);
     } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
-      uint16_t ultrasonic_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
+      _ultrasonic_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC);
     } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_ULTRASONIC_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U8) {
-      uint8_t ultrasonic_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
+      _ultrasonic_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC);
     } 
     //Physical Contact  
     else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_OCCUPIED_TO_UNOCCUPIED_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
-      uint16_t physical_contact_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
+      _physical_contact_occ_to_unocc_delay = *(uint16_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT);
     } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_DELAY_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) {
-      uint16_t physical_contact_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
+      _physical_contact_unocc_to_occ_delay = *(uint16_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT);
     } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_OCCUPANCY_SENSING_PHYSICAL_CONTACT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U8) {
-      uint8_t physical_contact_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
+      _physical_contact_unocc_to_occ_threshold = *(uint8_t *)message->attribute.data.value;
       occupancyConfigChanged(ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT);
     } else {
       log_w("Received message ignored. Attribute ID: %d not supported for Occupancy Sensor endpoint", message->attribute.id);
@@ -266,7 +266,20 @@ void ZigbeeOccupancySensor::zbAttributeSet(const esp_zb_zcl_set_attr_value_messa
 
 void ZigbeeOccupancySensor::occupancyConfigChanged(ZigbeeOccupancySensorType sensor_type) {
   if (_on_occupancy_config_change) {
-    _on_occupancy_config_change(sensor_type); //sensor type, delay, delay, threshold
+    switch (sensor_type) {
+      case ZIGBEE_OCCUPANCY_SENSOR_TYPE_PIR:
+        _on_occupancy_config_change(sensor_type, _pir_occ_to_unocc_delay, _pir_unocc_to_occ_delay, _pir_unocc_to_occ_threshold);
+        break;
+      case ZIGBEE_OCCUPANCY_SENSOR_TYPE_ULTRASONIC:
+        _on_occupancy_config_change(sensor_type, _ultrasonic_occ_to_unocc_delay, _ultrasonic_unocc_to_occ_delay, _ultrasonic_unocc_to_occ_threshold);
+        break;
+      case ZIGBEE_OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT:
+        _on_occupancy_config_change(sensor_type, _physical_contact_occ_to_unocc_delay, _physical_contact_unocc_to_occ_delay, _physical_contact_unocc_to_occ_threshold);
+        break;
+      default:
+        log_e("Invalid sensor type for occupancy config change: 0x%x", sensor_type);
+        break;
+    }
   } else {
     log_w("No callback function set for occupancy config change");
   }
diff --git a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
index 69a2ca0634b..95d4096a308 100644
--- a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
+++ b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
@@ -83,8 +83,23 @@ class ZigbeeOccupancySensor : public ZigbeeEP {
 private:
   void zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) override;
 
-  void (*_on_occupancy_config_change)(bool);
-  void occupancyConfigChanged(ZigbeeOccupancySensorType sensor_type);
+  void (*_on_occupancy_config_change)(ZigbeeOccupancySensorType sensor_type, uint16_t occ_to_unocc_delay, uint16_t unocc_to_occ_delay, uint8_t unocc_to_occ_threshold);
+  void occupancyConfigChanged(ZigbeeOccupancySensorType sensor_type, uint16_t occ_to_unocc_delay, uint16_t unocc_to_occ_delay, uint8_t unocc_to_occ_threshold); 
+
+  // PIR sensor configuration
+  uint16_t _pir_occ_to_unocc_delay;
+  uint16_t _pir_unocc_to_occ_delay;
+  uint8_t _pir_unocc_to_occ_threshold;
+
+  // Ultrasonic sensor configuration
+  uint16_t _ultrasonic_occ_to_unocc_delay;
+  uint16_t _ultrasonic_unocc_to_occ_delay;
+  uint8_t _ultrasonic_unocc_to_occ_threshold;
+
+  // Physical contact sensor configuration
+  uint16_t _physical_contact_occ_to_unocc_delay;
+  uint16_t _physical_contact_unocc_to_occ_delay;
+  uint8_t _physical_contact_unocc_to_occ_threshold;
 };
 
 #endif  // CONFIG_ZB_ENABLED

From 428b7fa0ed5b3b8fc76f33df7ccc104f66281369 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, 14 Apr 2025 12:52:49 +0200
Subject: [PATCH 3/3] fix: callback method parameters

---
 libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
index 95d4096a308..432f2b9257b 100644
--- a/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
+++ b/libraries/Zigbee/src/ep/ZigbeeOccupancySensor.h
@@ -77,6 +77,9 @@ class ZigbeeOccupancySensor : public ZigbeeEP {
   // Specifies the number of movement detection events that must occur in the period unoccupied to occupied delay, before the sensor changes to its occupied state.
   bool setUnoccupiedToOccupiedThreshold(ZigbeeOccupancySensorType sensor_type, uint8_t threshold);
 
+  void onOccupancyConfigChange(void (*callback)(ZigbeeOccupancySensorType sensor_type, uint16_t occ_to_unocc_delay, uint16_t unocc_to_occ_delay, uint8_t unocc_to_occ_threshold)) {
+    _on_occupancy_config_change = callback;
+  }
   // Report the occupancy value
   bool report();
 
@@ -84,7 +87,7 @@ class ZigbeeOccupancySensor : public ZigbeeEP {
   void zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) override;
 
   void (*_on_occupancy_config_change)(ZigbeeOccupancySensorType sensor_type, uint16_t occ_to_unocc_delay, uint16_t unocc_to_occ_delay, uint8_t unocc_to_occ_threshold);
-  void occupancyConfigChanged(ZigbeeOccupancySensorType sensor_type, uint16_t occ_to_unocc_delay, uint16_t unocc_to_occ_delay, uint8_t unocc_to_occ_threshold); 
+  void occupancyConfigChanged(ZigbeeOccupancySensorType sensor_type); 
 
   // PIR sensor configuration
   uint16_t _pir_occ_to_unocc_delay;