mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 09:20:22 +00:00
[zigbee] Implement switch support for esp32 (#17579)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
Jonathan Swoboda
pre-commit-ci-lite[bot]
parent
cb47dfcf0e
commit
568c1ae494
@@ -43,6 +43,7 @@ from .zigbee_esp32 import (
|
||||
final_validate_esp32,
|
||||
validate_binary_sensor_esp32,
|
||||
validate_sensor_esp32,
|
||||
validate_switch_esp32,
|
||||
zigbee_require_vfs_select,
|
||||
)
|
||||
from .zigbee_zephyr import (
|
||||
@@ -67,34 +68,58 @@ def _check_report_deprecation(value: str) -> str:
|
||||
return value
|
||||
|
||||
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_REPORT): cv.All(
|
||||
cv.requires_component("zigbee"),
|
||||
cv.requires_component("esp32"),
|
||||
_check_report_deprecation,
|
||||
cv.enum(REPORT, lower=True),
|
||||
),
|
||||
cv.OnlyWith(CONF_CLUSTER, ["esp32", "zigbee"], default="generic"): cv.All(
|
||||
cv.requires_component("zigbee"),
|
||||
cv.requires_component("esp32"),
|
||||
cv.one_of(*["generic", "device_class"], lower=True),
|
||||
),
|
||||
cv.Optional(CONF_ENDPOINT): cv.All(
|
||||
cv.requires_component("zigbee"),
|
||||
cv.requires_component("esp32"),
|
||||
cv.int_range(1, CONF_MAX_EP_NUMBER),
|
||||
),
|
||||
cv.Optional(CONF_USE_DEVICE_TYPE): cv.All(
|
||||
cv.requires_component("zigbee"),
|
||||
cv.requires_component("esp32"),
|
||||
cv.boolean,
|
||||
),
|
||||
}
|
||||
def _get_base_schema(cluster_options: list[str] | None = None) -> cv.Schema:
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_REPORT): cv.All(
|
||||
cv.requires_component("zigbee"),
|
||||
cv.requires_component("esp32"),
|
||||
_check_report_deprecation,
|
||||
cv.enum(REPORT, lower=True),
|
||||
),
|
||||
cv.Optional(CONF_ENDPOINT): cv.All(
|
||||
cv.requires_component("zigbee"),
|
||||
cv.requires_component("esp32"),
|
||||
cv.int_range(1, CONF_MAX_EP_NUMBER),
|
||||
),
|
||||
cv.Optional(CONF_USE_DEVICE_TYPE): cv.All(
|
||||
cv.requires_component("zigbee"),
|
||||
cv.requires_component("esp32"),
|
||||
cv.boolean,
|
||||
),
|
||||
}
|
||||
)
|
||||
if cluster_options:
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.OnlyWith(
|
||||
CONF_CLUSTER, ["esp32", "zigbee"], default=cluster_options[0]
|
||||
): cv.All(
|
||||
cv.requires_component("zigbee"),
|
||||
cv.requires_component("esp32"),
|
||||
cv.one_of(*cluster_options, lower=True),
|
||||
),
|
||||
}
|
||||
).extend(schema)
|
||||
return schema
|
||||
|
||||
|
||||
# set BASE_SCHEMA for CI and backwards compatibility
|
||||
BASE_SCHEMA = _get_base_schema()
|
||||
|
||||
BINARY_SENSOR_SCHEMA = (
|
||||
cv.Schema({})
|
||||
.extend(_get_base_schema(["generic", "device_class"]))
|
||||
.extend(zephyr_binary_sensor)
|
||||
)
|
||||
SENSOR_SCHEMA = (
|
||||
cv.Schema({})
|
||||
.extend(_get_base_schema(["generic", "device_class"]))
|
||||
.extend(zephyr_sensor)
|
||||
)
|
||||
SWITCH_SCHEMA = (
|
||||
cv.Schema({}).extend(_get_base_schema(["generic", "on_off"])).extend(zephyr_switch)
|
||||
)
|
||||
BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(BASE_SCHEMA).extend(zephyr_binary_sensor)
|
||||
SENSOR_SCHEMA = cv.Schema({}).extend(BASE_SCHEMA).extend(zephyr_sensor)
|
||||
SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch)
|
||||
NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number)
|
||||
|
||||
|
||||
@@ -226,12 +251,15 @@ async def setup_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
||||
|
||||
|
||||
async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
||||
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
|
||||
if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL):
|
||||
return
|
||||
if CORE.using_zephyr:
|
||||
from .zigbee_zephyr import zephyr_setup_switch
|
||||
|
||||
await zephyr_setup_switch(entity, config)
|
||||
if not config.get(CONF_ZIGBEE_ID):
|
||||
return
|
||||
from .zigbee_zephyr import add_switch
|
||||
else:
|
||||
from .zigbee_esp32 import add_component as add_switch
|
||||
CORE.add_job(add_switch, entity, config)
|
||||
|
||||
|
||||
async def setup_number(
|
||||
@@ -284,7 +312,7 @@ def validate_switch(config: ConfigType) -> ConfigType:
|
||||
if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL):
|
||||
return config
|
||||
if CORE.is_esp32:
|
||||
return config
|
||||
return validate_switch_esp32(config)
|
||||
return consume_endpoint(config)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ KEY_ZIGBEE_EP_NO_NUM = "zigbee_ep_no_num"
|
||||
KEY_ZIGBEE_FIRST_EP_CL = "zigbee_first_ep_cl"
|
||||
|
||||
DEVICE_ID = {
|
||||
"ON_OFF_OUTPUT": cg.RawExpression("EZB_ZHA_ON_OFF_OUTPUT_DEVICE_ID"),
|
||||
"RANGE_EXTENDER": cg.RawExpression("EZB_ZHA_RANGE_EXTENDER_DEVICE_ID"),
|
||||
"MAINS_POWER_OUTLET": cg.RawExpression("EZB_ZHA_MAINS_POWER_OUTLET_DEVICE_ID"),
|
||||
"SIMPLE_SENSOR": cg.RawExpression("EZB_ZHA_SIMPLE_SENSOR_DEVICE_ID"),
|
||||
"LIGHT_SENSOR": cg.RawExpression("EZB_ZHA_LIGHT_SENSOR_DEVICE_ID"),
|
||||
"OCCUPANCY_SENSOR": cg.RawExpression("EZB_ZHA_OCCUPANCY_SENSOR_DEVICE_ID"),
|
||||
@@ -26,8 +28,10 @@ DEVICE_ID = {
|
||||
cluster_id = cg.esphome_ns.enum("ezb_zcl_cluster_id_e")
|
||||
CLUSTER_ID = {
|
||||
"BASIC": cluster_id.EZB_ZCL_CLUSTER_ID_BASIC,
|
||||
"ON_OFF": cluster_id.EZB_ZCL_CLUSTER_ID_ON_OFF,
|
||||
"TIME": cluster_id.EZB_ZCL_CLUSTER_ID_TIME,
|
||||
"BINARY_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_BINARY_INPUT,
|
||||
"BINARY_OUTPUT": cluster_id.EZB_ZCL_CLUSTER_ID_BINARY_OUTPUT,
|
||||
"ANALOG_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
|
||||
"ILLUMINANCE_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT,
|
||||
"TEMPERATURE_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_TEMPERATURE_MEASUREMENT,
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
@@ -47,12 +50,23 @@ class ZigbeeAttribute final : public Component {
|
||||
template<typename T> void set_attr(const T &value);
|
||||
uint8_t attr_type() { return attr_type_; }
|
||||
void set_report(ZigbeeReportT report);
|
||||
|
||||
template<typename F> void add_on_value_callback(F &&callback) { on_value_callback_.add(std::forward<F>(callback)); }
|
||||
void on_value(ezb_zcl_attribute_t attribute) {
|
||||
if (attribute.data.type == this->attr_type() && attribute.data.value) {
|
||||
this->on_value_callback_.call(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
template<typename T> void connect(sensor::Sensor *sensor);
|
||||
template<typename T, typename F> void connect(sensor::Sensor *sensor, F &&f);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
template<typename T> void connect(binary_sensor::BinarySensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
template<typename T> void connect(switch_::Switch *device);
|
||||
#endif
|
||||
bool report_enabled = false;
|
||||
|
||||
@@ -71,6 +85,7 @@ class ZigbeeAttribute final : public Component {
|
||||
bool set_attr_requested_{false};
|
||||
bool report_requested_{false};
|
||||
bool force_report_{false};
|
||||
LazyCallbackManager<void(ezb_zcl_attribute_t attribute)> on_value_callback_{};
|
||||
template<typename T> T scale_value_(float value);
|
||||
template<typename T> T invalid_value_();
|
||||
};
|
||||
@@ -135,6 +150,13 @@ template<typename T> void ZigbeeAttribute::connect(binary_sensor::BinarySensor *
|
||||
sensor->add_on_state_callback([this](bool value) { this->set_attr((T) (this->scale_ * value)); });
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
template<typename T> void ZigbeeAttribute::connect(switch_::Switch *device) {
|
||||
this->add_on_value_callback(
|
||||
[device](ezb_zcl_attribute_t attribute) { device->control(*(T *) attribute.data.value); });
|
||||
device->add_on_state_callback([this](bool value) { this->set_attr((T) (this->scale_ * value)); });
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
|
||||
|
||||
@@ -108,6 +108,35 @@ BINARY_INPUT_EP = {
|
||||
],
|
||||
}
|
||||
|
||||
BINARY_OUTPUT_EP = {
|
||||
CONF_CLUSTERS: [
|
||||
{
|
||||
CONF_ID: "BINARY_OUTPUT",
|
||||
ROLE: "SERVER",
|
||||
CONF_ATTRIBUTES: [
|
||||
{
|
||||
CONF_ATTRIBUTE_ID: 0x55,
|
||||
CONF_TYPE: "BOOL",
|
||||
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||
CONNECT: True,
|
||||
},
|
||||
{
|
||||
CONF_ATTRIBUTE_ID: 0x51,
|
||||
CONF_TYPE: "BOOL",
|
||||
},
|
||||
{
|
||||
CONF_ATTRIBUTE_ID: 0x6F,
|
||||
CONF_TYPE: "MAP8",
|
||||
},
|
||||
{
|
||||
CONF_ATTRIBUTE_ID: 0x1C,
|
||||
CONF_TYPE: "STRING",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _pressure_ep(device_type: bool = False) -> dict[str, Any]:
|
||||
ep = {
|
||||
@@ -300,6 +329,26 @@ BINARY_SENSOR_EP_CONFIGS: dict[str, dict[str, Any]] = {
|
||||
},
|
||||
}
|
||||
|
||||
SWITCH_EP_CONFIGS: dict[str, dict[str, Any]] = {
|
||||
"on_off": {
|
||||
DEVICE_TYPE: "ON_OFF_OUTPUT",
|
||||
CONF_CLUSTERS: [
|
||||
{
|
||||
CONF_ID: "ON_OFF",
|
||||
ROLE: "SERVER",
|
||||
CONF_ATTRIBUTES: [
|
||||
{
|
||||
CONF_ATTRIBUTE_ID: 0x0,
|
||||
CONF_TYPE: "BOOL",
|
||||
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||
CONNECT: True,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _get_next_ep_num(eps: list[int]) -> int:
|
||||
try:
|
||||
|
||||
@@ -154,6 +154,31 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void load_zb_event(ZBEvent *event, ezb_zcl_message_info_t info, ezb_zcl_attribute_t attribute) {
|
||||
event->load_set_attr_value_event(info, attribute);
|
||||
}
|
||||
|
||||
template<typename... Args> void enqueue_zb_event(Args... args) {
|
||||
// Allocate an event from the pool
|
||||
ZBEvent *event = global_zigbee->zb_event_pool_.allocate();
|
||||
if (event == nullptr) {
|
||||
// No events available - queue is full or we're out of memory
|
||||
global_zigbee->zb_events_.increment_dropped_count();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load new event data (replaces previous event)
|
||||
load_zb_event(event, args...);
|
||||
|
||||
// Push the event to the queue
|
||||
global_zigbee->zb_events_.push(event);
|
||||
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
|
||||
global_zigbee->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
// Explicit template instantiations for the friend function
|
||||
template void enqueue_zb_event(ezb_zcl_message_info_t info, ezb_zcl_attribute_t attribute);
|
||||
|
||||
static void zb_attribute_handler(ezb_zcl_set_attr_value_message_t *message) {
|
||||
ESP_RETURN_ON_FALSE(message, , TAG, "Empty message");
|
||||
ESP_RETURN_ON_FALSE(message->info.status == EZB_ZCL_STATUS_SUCCESS, , TAG, "Received message: error status(%d)",
|
||||
@@ -161,6 +186,7 @@ static void zb_attribute_handler(ezb_zcl_set_attr_value_message_t *message) {
|
||||
ESP_LOGD(TAG, "ZCL SetAttributeValue message for endpoint(%d) cluster(0x%04x) %s with status(0x%02x)",
|
||||
message->info.dst_ep, message->info.cluster_id,
|
||||
message->info.cluster_role == EZB_ZCL_CLUSTER_SERVER ? "server" : "client", message->info.status);
|
||||
enqueue_zb_event(message->info, message->in.attribute);
|
||||
}
|
||||
|
||||
static void zb_action_handler(ezb_zcl_core_action_callback_id_t callback_id, void *message) {
|
||||
@@ -180,6 +206,13 @@ static void zb_action_handler(ezb_zcl_core_action_callback_id_t callback_id, voi
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeComponent::handle_attribute_(ezb_zcl_message_info_t info, ezb_zcl_attribute_t attribute) {
|
||||
auto it = this->attributes_.find({info.dst_ep, info.cluster_id, info.cluster_role, attribute.id});
|
||||
if (it != this->attributes_.end()) {
|
||||
it->second->on_value(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeComponent::create_default_cluster(uint8_t endpoint_id, uint16_t device_id) {
|
||||
ezb_af_ep_desc_t ep_desc =
|
||||
esphome_zb_zha_default_ep_desc_create(endpoint_id, device_id, this->basic_cluster_data_.power_source);
|
||||
@@ -349,6 +382,30 @@ void ZigbeeComponent::setup() {
|
||||
}
|
||||
|
||||
void ZigbeeComponent::loop() {
|
||||
// Process all pending events
|
||||
ZBEvent *event = this->zb_events_.pop();
|
||||
while (event != nullptr) {
|
||||
// Handle the event
|
||||
switch (event->callback_id_) {
|
||||
case EZB_ZCL_CORE_SET_ATTR_VALUE_CB_ID:
|
||||
this->handle_attribute_(event->event_.set_attr.info, event->event_.set_attr.attribute);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Received event with unhandled callback id: 0x%x", static_cast<unsigned>(event->callback_id_));
|
||||
break;
|
||||
}
|
||||
|
||||
// Free the event back to the pool
|
||||
this->zb_event_pool_.release(event);
|
||||
// Get the next event
|
||||
event = this->zb_events_.pop();
|
||||
}
|
||||
// Log dropped events periodically
|
||||
uint16_t dropped = this->zb_events_.get_and_reset_dropped_count();
|
||||
if (dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u Zigbee events due to buffer overflow", dropped);
|
||||
}
|
||||
|
||||
if (!this->start_reported_ && this->started_) {
|
||||
this->start_cb_.call();
|
||||
this->start_reported_ = true;
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
#include "ezbee/zha.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
#include "esphome/core/event_pool.h"
|
||||
#include "zigbee_helpers_esp32.h"
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#include "zigbee_event_esp32.h"
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
@@ -24,6 +23,7 @@ namespace esphome::zigbee {
|
||||
static const uint16_t ED_KEEP_ALIVE = 3000; /* 3000 millisecond */
|
||||
static const uint8_t MAX_CHILDREN = 10;
|
||||
static const uint32_t EZB_PRIMARY_CHANNEL_MASK = 0x07FFF800U; /* channels 11-26 */
|
||||
static constexpr uint8_t MAX_ZB_QUEUE_SIZE = 32;
|
||||
|
||||
#define EZB_DEFAULT_RADIO_CONFIG() \
|
||||
{ .radio_mode = ESP_ZIGBEE_RADIO_MODE_NATIVE, }
|
||||
@@ -88,6 +88,7 @@ class ZigbeeComponent final : public Component {
|
||||
template<typename T>
|
||||
void add_attr_(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id,
|
||||
T *value_p);
|
||||
void handle_attribute_(ezb_zcl_message_info_t info, ezb_zcl_attribute_t attribute);
|
||||
// attributes_ will be used during operation in zigbee callbacks to update the attribute values and trigger
|
||||
// automations
|
||||
// key tuple could be replaced by single 64 (48) bit int with bit fields for endpoint, cluster, role and attr_id
|
||||
@@ -95,6 +96,15 @@ class ZigbeeComponent final : public Component {
|
||||
std::vector<AttrValue> attr_values_;
|
||||
ezb_af_device_desc_t dev_desc_ = ezb_af_create_device_desc();
|
||||
CallbackManager<void(bool)> join_cb_{};
|
||||
template<typename... Args> friend void enqueue_zb_event(Args... args);
|
||||
esphome::LockFreeQueue<ZBEvent, MAX_ZB_QUEUE_SIZE> zb_events_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements (one slot distinguishes full from empty).
|
||||
// This guarantees allocate() returns nullptr before push() can fail, which:
|
||||
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
|
||||
// 2. Avoids needing release() on the producer path after a failed push(),
|
||||
// preserving the SPSC contract on the pool's internal free list
|
||||
esphome::EventPool<ZBEvent, MAX_ZB_QUEUE_SIZE - 1> zb_event_pool_;
|
||||
LazyCallbackManager<void()> start_cb_{};
|
||||
bool start_reported_{false};
|
||||
std::atomic<bool> started_ = false;
|
||||
|
||||
@@ -27,6 +27,7 @@ from esphome.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE,
|
||||
CONF_WIFI,
|
||||
DEVICE_CLASS_OUTLET,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
@@ -67,8 +68,10 @@ from .const_esp32 import (
|
||||
from .zigbee_ep_esp32 import (
|
||||
ANALOG_INPUT_EP,
|
||||
BINARY_INPUT_EP,
|
||||
BINARY_OUTPUT_EP,
|
||||
BINARY_SENSOR_EP_CONFIGS,
|
||||
SENSOR_EP_CONFIGS,
|
||||
SWITCH_EP_CONFIGS,
|
||||
add_ep,
|
||||
create_ep,
|
||||
)
|
||||
@@ -275,6 +278,23 @@ def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def validate_switch_esp32(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_CLUSTER] == "on_off":
|
||||
ep = copy.deepcopy(SWITCH_EP_CONFIGS["on_off"])
|
||||
if config.get(CONF_DEVICE_CLASS) == DEVICE_CLASS_OUTLET:
|
||||
ep[DEVICE_TYPE] = "MAINS_POWER_OUTLET"
|
||||
else:
|
||||
if config.get(CONF_DEVICE_CLASS) == DEVICE_CLASS_OUTLET:
|
||||
_LOGGER.warning(
|
||||
"'device_class: outlet' has no effect with 'cluster: generic', "
|
||||
"use 'cluster: on_off' to expose a MAINS_POWER_OUTLET device type"
|
||||
)
|
||||
ep = copy.deepcopy(BINARY_OUTPUT_EP)
|
||||
setup_attributes(config, ep[CONF_CLUSTERS])
|
||||
add_ep(ep, config.get(CONF_ENDPOINT), config.get(CONF_USE_DEVICE_TYPE))
|
||||
return config
|
||||
|
||||
|
||||
def zigbee_require_vfs_select(config: ConfigType) -> ConfigType:
|
||||
"""Register VFS select requirement during config validation."""
|
||||
# Zigbee uses esp_vfs_eventfd which requires VFS select support
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ZIGBEE
|
||||
|
||||
#include <cstddef> // for offsetof
|
||||
#include <cstring> // for memcpy
|
||||
#include "esp_zigbee.h"
|
||||
#include "ezbee/zha.h"
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
class ZBEvent {
|
||||
public:
|
||||
// IMPORTANT: We MUST copy all values because the pointer from ESP-IDF
|
||||
// is only valid during the callback execution. Since ZB events are processed
|
||||
// asynchronously in the main loop, we store our own copy inline to ensure
|
||||
// the data remains valid until the event is processed.
|
||||
ZBEvent(ezb_zcl_message_info_t info, ezb_zcl_attribute_t attribute)
|
||||
: event_{}, callback_id_(EZB_ZCL_CORE_SET_ATTR_VALUE_CB_ID) {
|
||||
this->init_set_attr_value_data_(info, attribute);
|
||||
}
|
||||
|
||||
// Destructor to clean up heap allocations
|
||||
~ZBEvent() { this->release(); }
|
||||
|
||||
// Default constructor for pre-allocation in pool
|
||||
ZBEvent() : event_{}, callback_id_(EZB_ZCL_CORE_CB_ID_END) {}
|
||||
|
||||
// Invoked on return to EventPool - clean up any heap-allocated data
|
||||
void release() {
|
||||
// Free any allocated memory within the event
|
||||
switch (this->callback_id_) {
|
||||
case EZB_ZCL_CORE_SET_ATTR_VALUE_CB_ID:
|
||||
if (!this->event_.set_attr.is_inline && this->event_.set_attr.data.heap_data != nullptr) {
|
||||
delete[] this->event_.set_attr.data.heap_data;
|
||||
this->event_.set_attr.data.heap_data = nullptr;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load new event data for reuse (replaces previous event data)
|
||||
// Note: release() is NOT called here because EventPool::release() already
|
||||
// calls event->release() before returning to the free list. Every event
|
||||
// from allocate() is already in a clean state.
|
||||
void load_set_attr_value_event(ezb_zcl_message_info_t info, ezb_zcl_attribute_t attribute) {
|
||||
this->callback_id_ = EZB_ZCL_CORE_SET_ATTR_VALUE_CB_ID;
|
||||
this->init_set_attr_value_data_(info, attribute);
|
||||
}
|
||||
|
||||
// Disable copy to prevent double-delete
|
||||
ZBEvent(const ZBEvent &) = delete;
|
||||
ZBEvent &operator=(const ZBEvent &) = delete;
|
||||
|
||||
union {
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
struct set_attr_event {
|
||||
ezb_zcl_message_info_t info;
|
||||
ezb_zcl_attribute_t attribute;
|
||||
union {
|
||||
uint8_t *heap_data;
|
||||
uint8_t inline_data[4]; // For small data types (<= 32 bit)
|
||||
} data;
|
||||
bool is_inline;
|
||||
} set_attr;
|
||||
} event_;
|
||||
|
||||
ezb_zcl_core_action_callback_id_t callback_id_;
|
||||
|
||||
private:
|
||||
void init_set_attr_value_data_(ezb_zcl_message_info_t info, ezb_zcl_attribute_t attribute) {
|
||||
this->event_.set_attr.info = info;
|
||||
this->event_.set_attr.attribute = attribute;
|
||||
// get attribute.data.value with correct type
|
||||
if (attribute.data.value != nullptr) {
|
||||
// Copy the attribute value to avoid dangling pointer issues
|
||||
size_t value_size = ezb_zcl_get_attr_value_size(attribute.data.type, attribute.data.value);
|
||||
if (value_size > 4) {
|
||||
this->event_.set_attr.data.heap_data = new uint8_t[value_size];
|
||||
memcpy(this->event_.set_attr.data.heap_data, attribute.data.value, value_size);
|
||||
this->event_.set_attr.attribute.data.value = this->event_.set_attr.data.heap_data;
|
||||
this->event_.set_attr.is_inline = false;
|
||||
} else {
|
||||
memcpy(this->event_.set_attr.data.inline_data, attribute.data.value, value_size);
|
||||
this->event_.set_attr.attribute.data.value = this->event_.set_attr.data.inline_data;
|
||||
this->event_.set_attr.is_inline = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace esphome::zigbee
|
||||
#endif // USE_ZIGBEE
|
||||
#endif // USE_ESP32
|
||||
@@ -27,6 +27,12 @@ ezb_err_t esphome_zb_add_or_update_cluster(uint16_t cluster_id, ezb_af_ep_desc_t
|
||||
ezb_af_ep_desc_t esphome_zb_zha_default_ep_desc_create(uint8_t ep_id, uint16_t device_id, uint8_t power_source) {
|
||||
ezb_af_ep_desc_t ep_desc;
|
||||
switch (device_id) {
|
||||
case EZB_ZHA_MAINS_POWER_OUTLET_DEVICE_ID: {
|
||||
ezb_zha_mains_power_outlet_config_t config = EZB_ZHA_MAINS_POWER_OUTLET_CONFIG();
|
||||
config.basic_cfg.power_source = power_source;
|
||||
ep_desc = ezb_zha_create_mains_power_outlet(ep_id, &config);
|
||||
break;
|
||||
}
|
||||
case EZB_ZHA_LIGHT_SENSOR_DEVICE_ID: {
|
||||
ezb_zha_light_sensor_config_t config = EZB_ZHA_LIGHT_SENSOR_CONFIG();
|
||||
config.basic_cfg.power_source = power_source;
|
||||
@@ -60,12 +66,16 @@ ezb_zcl_cluster_desc_t esphome_zb_default_cluster_dscr_create(uint16_t cluster_i
|
||||
return ezb_zcl_basic_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_IDENTIFY:
|
||||
return ezb_zcl_identify_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_ON_OFF:
|
||||
return ezb_zcl_on_off_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_TIME:
|
||||
return ezb_zcl_time_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
|
||||
return ezb_zcl_analog_input_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
|
||||
return ezb_zcl_binary_input_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_BINARY_OUTPUT:
|
||||
return ezb_zcl_binary_output_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT:
|
||||
return ezb_zcl_illuminance_measurement_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_TEMPERATURE_MEASUREMENT:
|
||||
@@ -97,12 +107,16 @@ ezb_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, ezb_zcl_cluster_desc_
|
||||
return ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_IDENTIFY:
|
||||
return ezb_zcl_identify_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_ON_OFF:
|
||||
return ezb_zcl_on_off_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_TIME:
|
||||
return ezb_zcl_time_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
|
||||
return ezb_zcl_analog_input_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
|
||||
return ezb_zcl_binary_input_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_BINARY_OUTPUT:
|
||||
return ezb_zcl_binary_output_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT:
|
||||
return ezb_zcl_illuminance_measurement_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_TEMPERATURE_MEASUREMENT:
|
||||
|
||||
@@ -314,10 +314,6 @@ async def _ctx_to_code(config: ConfigType) -> None:
|
||||
cg.add(cg.RawExpression("ZB_AF_REGISTER_DEVICE_CTX(&zb_device_ctx)"))
|
||||
|
||||
|
||||
async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
||||
CORE.add_job(_add_switch, entity, config)
|
||||
|
||||
|
||||
async def zephyr_setup_number(
|
||||
entity: cg.MockObj,
|
||||
config: ConfigType,
|
||||
@@ -425,7 +421,7 @@ async def add_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _add_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
||||
async def add_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
||||
await _add_zigbee_ep(
|
||||
entity,
|
||||
config,
|
||||
|
||||
@@ -45,6 +45,17 @@ sensor:
|
||||
device_class: volume_flow_rate
|
||||
cluster: device_class
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
name: "Template Switch 1"
|
||||
optimistic: true
|
||||
cluster: on_off
|
||||
- platform: template
|
||||
name: "Template Switch 2"
|
||||
optimistic: true
|
||||
cluster: on_off
|
||||
device_class: outlet
|
||||
|
||||
zigbee:
|
||||
model: zigbee_test
|
||||
router: false
|
||||
|
||||
Reference in New Issue
Block a user