[zigbee][core] Add support for Zigbee binary sensors on ESP32 H2 and C6 (#11553)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
luar123
2026-04-23 18:46:56 +02:00
committed by GitHub
parent 9b45b046a8
commit f757cd1210
27 changed files with 1295 additions and 67 deletions

View File

@@ -1 +1 @@
256216e144a626c8c9d1a458920a9db3de7dfc8c6a1b44b87946b9752e81026c
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324

View File

@@ -600,6 +600,6 @@ esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @tomaszduda23
esphome/components/zigbee/* @luar123 @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt
esphome/components/zwave_proxy/* @kbx81

View File

@@ -3,26 +3,42 @@ from typing import Any
from esphome import automation, core
import esphome.codegen as cg
from esphome.components.esp32 import only_on_variant
from esphome.components.esp32.const import (
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
)
from esphome.components.nrf52.boards import BOOTLOADER_CONFIG, Section
from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data
from esphome.components.zephyr.const import KEY_BOOTLOADER
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME
from esphome.const import CONF_ID, CONF_INTERNAL, CONF_MODEL, CONF_NAME
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
from .const import (
CONF_ON_JOIN,
CONF_POWER_SOURCE,
CONF_REPORT,
CONF_ROUTER,
CONF_WIPE_ON_BOOT,
KEY_ZIGBEE,
POWER_SOURCE,
REPORT,
ZigbeeComponent,
zigbee_ns,
)
from .const_zephyr import (
CONF_IEEE802154_VENDOR_OUI,
CONF_MAX_EP_NUMBER,
CONF_ON_JOIN,
CONF_POWER_SOURCE,
CONF_WIPE_ON_BOOT,
CONF_ZIGBEE_ID,
KEY_EP_NUMBER,
KEY_ZIGBEE,
POWER_SOURCE,
ZigbeeComponent,
zigbee_ns,
)
from .zigbee_esp32 import (
final_validate_esp32,
validate_binary_sensor_esp32,
zigbee_require_vfs_select,
)
from .zigbee_zephyr import (
zephyr_binary_sensor,
@@ -33,11 +49,11 @@ from .zigbee_zephyr import (
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@tomaszduda23"]
CODEOWNERS = ["@luar123", "@tomaszduda23"]
def zigbee_set_core_data(config: ConfigType) -> ConfigType:
if zephyr_data()[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
if CORE.is_nrf52 and zephyr_data()[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
zephyr_add_pm_static(
[Section("empty_after_zboss_offset", 0xF4000, 0xC000, "flash_primary")]
)
@@ -45,7 +61,15 @@ def zigbee_set_core_data(config: ConfigType) -> ConfigType:
return config
BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor)
BINARY_SENSOR_SCHEMA = cv.Schema(
{
cv.Optional(CONF_REPORT): cv.All(
cv.requires_component("zigbee"),
cv.requires_component("esp32"),
cv.enum(REPORT, lower=True),
)
}
).extend(zephyr_binary_sensor)
SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor)
SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch)
NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number)
@@ -54,16 +78,27 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(ZigbeeComponent),
cv.Optional(CONF_ON_JOIN): automation.validate_automation(single=True),
cv.Optional(CONF_WIPE_ON_BOOT, default=False): cv.All(
cv.Optional(CONF_MODEL, default=CORE.name): cv.All(
cv.string, cv.Length(max=31)
),
cv.OnlyWith(CONF_ROUTER, "esp32", default=False): cv.All(
cv.requires_component("esp32"),
cv.boolean,
),
cv.Optional(CONF_ON_JOIN): cv.All(
cv.requires_component("nrf52"),
automation.validate_automation(single=True),
),
cv.OnlyWith(CONF_WIPE_ON_BOOT, "nrf52", default=False): cv.All(
cv.Any(
cv.boolean,
cv.one_of(*["once"], lower=True),
),
cv.requires_component("nrf52"),
),
cv.Optional(CONF_POWER_SOURCE, default="DC_SOURCE"): cv.enum(
POWER_SOURCE, upper=True
cv.OnlyWith(CONF_POWER_SOURCE, "nrf52", default="DC_SOURCE"): cv.All(
cv.enum(POWER_SOURCE, upper=True),
cv.requires_component("nrf52"),
),
cv.Optional(CONF_IEEE802154_VENDOR_OUI): cv.All(
cv.Any(
@@ -74,12 +109,27 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
zigbee_require_vfs_select,
zigbee_set_core_data,
cv.only_with_framework("zephyr"),
cv.Any(
cv.All(
cv.only_on_esp32,
only_on_variant(
supported=[
VARIANT_ESP32H2,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
]
),
),
cv.only_with_framework("zephyr"),
),
)
def validate_number_of_ep(config: ConfigType) -> None:
def validate_number_of_ep(config: ConfigType) -> ConfigType:
if not CORE.is_nrf52:
return config
if KEY_ZIGBEE not in CORE.data:
raise cv.Invalid("At least one zigbee device need to be included")
count = len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])
@@ -90,9 +140,12 @@ def validate_number_of_ep(config: ConfigType) -> None:
if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode:
raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}")
return config
FINAL_VALIDATE_SCHEMA = cv.All(
validate_number_of_ep,
final_validate_esp32,
)
@@ -103,6 +156,10 @@ async def to_code(config: ConfigType) -> None:
from .zigbee_zephyr import zephyr_to_code
await zephyr_to_code(config)
if CORE.is_esp32:
from .zigbee_esp32 import esp32_to_code
await esp32_to_code(config)
async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
@@ -148,7 +205,7 @@ async def setup_number(
def consume_endpoint(config: ConfigType) -> ConfigType:
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
if not config.get(CONF_ZIGBEE_ID):
return config
if CONF_NAME in config and " " in config[CONF_NAME]:
_LOGGER.warning(
@@ -163,18 +220,34 @@ def consume_endpoint(config: ConfigType) -> ConfigType:
def validate_binary_sensor(config: ConfigType) -> ConfigType:
if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL):
return config
if CORE.is_esp32:
return validate_binary_sensor_esp32(config)
return consume_endpoint(config)
def validate_sensor(config: ConfigType) -> ConfigType:
if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL):
return config
if CORE.is_esp32:
return config
return consume_endpoint(config)
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 consume_endpoint(config)
def validate_number(config: ConfigType) -> ConfigType:
if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL):
return config
if CORE.is_esp32:
return config
return consume_endpoint(config)

View File

@@ -1,6 +1,9 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ZIGBEE
#ifdef USE_ESP32
#include "zigbee_esp32.h"
#endif
#ifdef USE_NRF52
#include "zigbee_zephyr.h"
#endif

View File

@@ -0,0 +1,32 @@
import esphome.codegen as cg
zigbee_ns = cg.esphome_ns.namespace("zigbee")
ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component)
ZigbeeAttribute = zigbee_ns.class_("ZigbeeAttribute", cg.Component)
BinaryAttrs = zigbee_ns.struct("BinaryAttrs")
AnalogAttrs = zigbee_ns.struct("AnalogAttrs")
AnalogAttrsOutput = zigbee_ns.struct("AnalogAttrsOutput")
report = zigbee_ns.enum("ZigbeeReportT")
REPORT = {
"coordinator": report.ZIGBEE_REPORT_COORDINATOR,
"enable": report.ZIGBEE_REPORT_ENABLE,
"force": report.ZIGBEE_REPORT_FORCE,
}
CONF_ON_JOIN = "on_join"
CONF_WIPE_ON_BOOT = "wipe_on_boot"
CONF_REPORT = "report"
CONF_ROUTER = "router"
CONF_POWER_SOURCE = "power_source"
POWER_SOURCE = {
"UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN",
"MAINS_SINGLE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_SINGLE_PHASE",
"MAINS_THREE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_THREE_PHASE",
"BATTERY": "ZB_ZCL_BASIC_POWER_SOURCE_BATTERY",
"DC_SOURCE": "ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE",
"EMERGENCY_MAINS_CONST": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_CONST",
"EMERGENCY_MAINS_TRANSF": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_TRANSF",
}
KEY_ZIGBEE = "zigbee"

View File

@@ -0,0 +1,35 @@
import esphome.codegen as cg
DEVICE_TYPE = "device_type"
ROLE = "role"
CONF_MAX_EP_NUMBER = 239
CONF_NUM = "num"
CONF_CLUSTERS = "clusters"
CONF_ATTRIBUTES = "attributes"
CONF_ENDPOINT = "endpoint"
CONF_CLUSTER = "cluster"
SCALE = "scale"
CONF_ATTRIBUTE_ID = "attribute_id"
KEY_BS_EP = "binary_sensor_ep"
ha_standard_devices = cg.esphome_ns.enum("zb_ha_standard_devs_e")
DEVICE_ID = {
"RANGE_EXTENDER": ha_standard_devices.ZB_HA_RANGE_EXTENDER_DEVICE_ID,
"SIMPLE_SENSOR": ha_standard_devices.ZB_HA_SIMPLE_SENSOR_DEVICE_ID,
"CUSTOM_ATTR": ha_standard_devices.ZB_HA_CUSTOM_ATTR_DEVICE_ID,
}
cluster_id = cg.esphome_ns.enum("esp_zb_zcl_cluster_id_t")
CLUSTER_ID = {
"BASIC": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BASIC,
"BINARY_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT,
}
cluster_role = cg.esphome_ns.enum("esp_zb_zcl_cluster_role_t")
CLUSTER_ROLE = {
"SERVER": cluster_role.ESP_ZB_ZCL_CLUSTER_SERVER_ROLE,
}
attr_type = cg.esphome_ns.enum("esp_zb_zcl_attr_type_t")
ATTR_TYPE = {
"BOOL": attr_type.ESP_ZB_ZCL_ATTR_TYPE_BOOL,
"8BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_8BITMAP,
"CHAR_STRING": attr_type.ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING,
}

View File

@@ -1,33 +1,12 @@
import esphome.codegen as cg
zigbee_ns = cg.esphome_ns.namespace("zigbee")
ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component)
BinaryAttrs = zigbee_ns.struct("BinaryAttrs")
AnalogAttrs = zigbee_ns.struct("AnalogAttrs")
AnalogAttrsOutput = zigbee_ns.struct("AnalogAttrsOutput")
CONF_MAX_EP_NUMBER = 8
CONF_ZIGBEE_ID = "zigbee_id"
CONF_ON_JOIN = "on_join"
CONF_WIPE_ON_BOOT = "wipe_on_boot"
CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor"
CONF_ZIGBEE_SENSOR = "zigbee_sensor"
CONF_ZIGBEE_SWITCH = "zigbee_switch"
CONF_ZIGBEE_NUMBER = "zigbee_number"
CONF_POWER_SOURCE = "power_source"
POWER_SOURCE = {
"UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN",
"MAINS_SINGLE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_SINGLE_PHASE",
"MAINS_THREE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_THREE_PHASE",
"BATTERY": "ZB_ZCL_BASIC_POWER_SOURCE_BATTERY",
"DC_SOURCE": "ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE",
"EMERGENCY_MAINS_CONST": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_CONST",
"EMERGENCY_MAINS_TRANSF": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_TRANSF",
}
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
# Keys for CORE.data storage
KEY_ZIGBEE = "zigbee"
KEY_EP_NUMBER = "ep_number"
# External ZBOSS SDK types (just strings for codegen)

View File

@@ -6,7 +6,8 @@ from esphome.core import CORE
from esphome.types import ConfigType
from .. import consume_endpoint
from ..const_zephyr import CONF_ZIGBEE_ID, zigbee_ns
from ..const import zigbee_ns
from ..const_zephyr import CONF_ZIGBEE_ID
from ..zigbee_zephyr import (
ZigbeeClusterDesc,
ZigbeeComponent,

View File

@@ -0,0 +1,89 @@
#include "zigbee_attribute_esp32.h"
#include "esphome/core/log.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#ifdef USE_ZIGBEE
namespace esphome::zigbee {
static const char *const TAG = "zigbee.attribute";
void ZigbeeAttribute::set_attr_() {
if (!this->zb_->is_connected()) {
return;
}
if (esp_zb_lock_acquire(10 / portTICK_PERIOD_MS)) {
esp_zb_zcl_status_t state = esp_zb_zcl_set_attribute_val(this->endpoint_id_, this->cluster_id_, this->role_,
this->attr_id_, this->value_p_, false);
if (this->force_report_) {
this->report_(true);
}
this->set_attr_requested_ = false;
// Check for error
if (state != ESP_ZB_ZCL_STATUS_SUCCESS) {
ESP_LOGE(TAG, "Setting attribute failed, ZCL status: %u", static_cast<unsigned>(state));
}
esp_zb_lock_release();
}
}
void ZigbeeAttribute::report_(bool has_lock) {
if (!this->zb_->is_connected()) {
return;
}
if (has_lock or esp_zb_lock_acquire(10 / portTICK_PERIOD_MS)) {
esp_zb_zcl_report_attr_cmd_t cmd = {
.address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT,
.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI,
};
cmd.zcl_basic_cmd.dst_addr_u.addr_short = 0x0000;
cmd.zcl_basic_cmd.dst_endpoint = 1;
cmd.zcl_basic_cmd.src_endpoint = this->endpoint_id_;
cmd.clusterID = this->cluster_id_;
cmd.attributeID = this->attr_id_;
esp_zb_zcl_report_attr_cmd_req(&cmd);
if (!has_lock) {
esp_zb_lock_release();
}
}
}
esp_zb_zcl_reporting_info_t ZigbeeAttribute::get_reporting_info() {
esp_zb_zcl_reporting_info_t reporting_info = {
.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV,
.ep = this->endpoint_id_,
.cluster_id = this->cluster_id_,
.cluster_role = this->role_,
.attr_id = this->attr_id_,
.manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC,
};
reporting_info.dst.profile_id = ESP_ZB_AF_HA_PROFILE_ID;
reporting_info.u.send_info.min_interval = 10; /*!< Actual minimum reporting interval */
reporting_info.u.send_info.max_interval = 0; /*!< Actual maximum reporting interval */
reporting_info.u.send_info.def_min_interval = 10; /*!< Default minimum reporting interval */
reporting_info.u.send_info.def_max_interval = 0; /*!< Default maximum reporting interval */
reporting_info.u.send_info.delta.s16 = 0; /*!< Actual reportable change */
return reporting_info;
}
void ZigbeeAttribute::set_report(bool force) {
this->report_enabled = true;
this->force_report_ = force;
}
void ZigbeeAttribute::loop() {
if (this->set_attr_requested_) {
this->set_attr_();
}
if (!this->set_attr_requested_) {
this->disable_loop();
}
}
} // namespace esphome::zigbee
#endif
#endif

View File

@@ -0,0 +1,90 @@
#pragma once
#include <type_traits>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#ifdef USE_ZIGBEE
#include "esp_zigbee_core.h"
#include "zigbee_esp32.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
namespace esphome::zigbee {
enum ZigbeeReportT {
ZIGBEE_REPORT_COORDINATOR,
ZIGBEE_REPORT_ENABLE,
ZIGBEE_REPORT_FORCE,
};
class ZigbeeAttribute : public Component {
public:
ZigbeeAttribute(ZigbeeComponent *parent, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id,
uint8_t attr_type, float scale, uint8_t max_size)
: zb_(parent),
endpoint_id_(endpoint_id),
cluster_id_(cluster_id),
role_(role),
attr_id_(attr_id),
attr_type_(attr_type),
scale_(scale),
max_size_(max_size) {}
void loop() override;
template<typename T> void add_attr(T value);
esp_zb_zcl_reporting_info_t get_reporting_info();
template<typename T> void set_attr(const T &value);
uint8_t attr_type() { return attr_type_; }
void set_report(bool force);
#ifdef USE_BINARY_SENSOR
template<typename T> void connect(binary_sensor::BinarySensor *sensor);
#endif
bool report_enabled = false;
protected:
void set_attr_();
void report_(bool has_lock);
ZigbeeComponent *zb_;
uint8_t endpoint_id_;
uint16_t cluster_id_;
uint8_t role_;
uint16_t attr_id_;
uint8_t attr_type_;
uint8_t max_size_;
float scale_;
void *value_p_{nullptr};
bool set_attr_requested_{false};
bool force_report_{false};
};
template<typename T> void ZigbeeAttribute::add_attr(T value) {
// Attribute type does never change and add_attr is only called once during startup, so this is safe.
// For now we need to support only simple numeric/bool types for (binary) sensors.
// For strings and arrays we would need to allocate a buffer of the maximum size.
this->value_p_ = (void *) (new T);
this->zb_->add_attr(this, this->endpoint_id_, this->cluster_id_, this->role_, this->attr_id_, this->max_size_,
std::move(value));
}
template<typename T> void ZigbeeAttribute::set_attr(const T &value) {
*static_cast<T *>(this->value_p_) = value;
this->set_attr_requested_ = true;
this->enable_loop();
}
#ifdef USE_BINARY_SENSOR
template<typename T> void ZigbeeAttribute::connect(binary_sensor::BinarySensor *sensor) {
sensor->add_on_state_callback([this](bool value) { this->set_attr((T) (this->scale_ * value)); });
}
#endif
} // namespace esphome::zigbee
#endif
#endif

View File

@@ -0,0 +1,70 @@
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE, CONF_ID, CONF_TYPE
from .const import CONF_REPORT, REPORT
from .const_esp32 import (
CLUSTER_ROLE,
CONF_ATTRIBUTE_ID,
CONF_ATTRIBUTES,
CONF_CLUSTERS,
CONF_MAX_EP_NUMBER,
CONF_NUM,
DEVICE_TYPE,
ROLE,
)
# endpoint configs:
ep_configs: dict[str, dict[str, Any]] = {
"binary_input": {
DEVICE_TYPE: "SIMPLE_SENSOR",
CONF_CLUSTERS: [
{
CONF_ID: "BINARY_INPUT",
ROLE: CLUSTER_ROLE["SERVER"],
CONF_ATTRIBUTES: [
{
CONF_ATTRIBUTE_ID: 0x55,
CONF_TYPE: "BOOL",
CONF_REPORT: REPORT["enable"],
CONF_DEVICE: None,
},
{
CONF_ATTRIBUTE_ID: 0x51,
CONF_TYPE: "BOOL",
},
{
CONF_ATTRIBUTE_ID: 0x6F,
CONF_TYPE: "8BITMAP",
},
{
CONF_ATTRIBUTE_ID: 0x1C,
CONF_TYPE: "CHAR_STRING",
},
],
},
],
},
}
def create_ep(ep_list: list[dict[str, Any]], router: bool) -> list[dict[str, Any]]:
# create dummy endpoint if list is empty
if not ep_list:
ep_type = "CUSTOM_ATTR"
if router:
ep_type = "RANGE_EXTENDER"
ep_list = [
{
DEVICE_TYPE: ep_type,
}
]
# enumerate endpoints
for i, ep in enumerate(ep_list, 1):
ep[CONF_NUM] = i
if len(ep_list) > CONF_MAX_EP_NUMBER:
raise cv.Invalid(
f"Too many devices. Zigbee can define only {CONF_MAX_EP_NUMBER} endpoints."
)
return ep_list

View File

@@ -0,0 +1,313 @@
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#ifdef USE_ZIGBEE
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_check.h"
#include "nvs_flash.h"
#include "zigbee_attribute_esp32.h"
#include "zigbee_esp32.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "zigbee_helpers_esp32.h"
#ifdef USE_WIFI
#include "esp_coexist.h"
#endif
namespace esphome::zigbee {
static const char *const TAG = "zigbee";
static ZigbeeComponent *global_zigbee = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size) {
uint8_t str_len = static_cast<uint8_t>(strlen(str));
uint8_t zcl_str_size = use_max_size ? max_size : std::min(max_size, str_len);
uint8_t *zcl_str = new uint8_t[zcl_str_size + 1]; // string + length octet
zcl_str[0] = zcl_str_size;
// Initialize payload to avoid leaking uninitialized heap contents and clamp copy length
memset(zcl_str + 1, 0, zcl_str_size);
uint8_t copy_len = std::min(zcl_str_size, str_len);
if (copy_len > 0) {
memcpy(zcl_str + 1, str, copy_len);
}
return zcl_str;
}
static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask) {
if (esp_zb_bdb_start_top_level_commissioning(mode_mask) != ESP_OK) {
ESP_LOGE(TAG, "Start network steering failed!");
}
}
void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) {
static uint8_t steering_retry_count = 0;
uint32_t *p_sg_p = signal_struct->p_app_signal;
esp_err_t err_status = signal_struct->esp_err_status;
esp_zb_app_signal_type_t sig_type = (esp_zb_app_signal_type_t) *p_sg_p;
esp_zb_zdo_signal_leave_params_t *leave_params = NULL;
switch (sig_type) {
case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP:
ESP_LOGD(TAG, "Zigbee stack initialized");
esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION);
break;
case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START:
case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT:
if (err_status == ESP_OK) {
ESP_LOGD(TAG, "Device started up in %sfactory-reset mode", esp_zb_bdb_is_factory_new() ? "" : "non ");
global_zigbee->started = true;
if (esp_zb_bdb_is_factory_new()) {
ESP_LOGD(TAG, "Start network steering");
esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING);
} else {
ESP_LOGD(TAG, "Device rebooted");
global_zigbee->connected = true;
}
} else {
ESP_LOGE(TAG, "FIRST_START. Device started up in %sfactory-reset mode with an error %d (%s)",
esp_zb_bdb_is_factory_new() ? "" : "non ", err_status, esp_err_to_name(err_status));
ESP_LOGW(TAG, "Failed to initialize Zigbee stack (status: %s)", esp_err_to_name(err_status));
esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb, ESP_ZB_BDB_MODE_INITIALIZATION,
1000);
}
break;
case ESP_ZB_BDB_SIGNAL_STEERING:
if (err_status == ESP_OK) {
steering_retry_count = 0;
ESP_LOGI(TAG, "Joined network successfully (PAN ID: 0x%04hx, Channel:%d)", esp_zb_get_pan_id(),
esp_zb_get_current_channel());
global_zigbee->connected = true;
} else {
ESP_LOGI(TAG, "Network steering was not successful (status: %s)", esp_err_to_name(err_status));
if (steering_retry_count < 10) {
steering_retry_count++;
esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb,
ESP_ZB_BDB_MODE_NETWORK_STEERING, 1000);
} else {
esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb,
ESP_ZB_BDB_MODE_NETWORK_STEERING, 600 * 1000);
}
}
break;
case ESP_ZB_ZDO_SIGNAL_LEAVE:
leave_params = (esp_zb_zdo_signal_leave_params_t *) esp_zb_app_signal_get_params(p_sg_p);
if (leave_params->leave_type == ESP_ZB_NWK_LEAVE_TYPE_RESET) {
esp_zb_factory_reset();
}
break;
default:
ESP_LOGD(TAG, "ZDO signal: %s (0x%x), status: %s", esp_zb_zdo_signal_to_string(sig_type), sig_type,
esp_err_to_name(err_status));
break;
}
}
static esp_err_t zb_attribute_handler(const esp_zb_zcl_set_attr_value_message_t *message) {
esp_err_t ret = ESP_OK;
ESP_RETURN_ON_FALSE(message, ESP_FAIL, TAG, "Empty message");
ESP_RETURN_ON_FALSE(message->info.status == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_ERR_INVALID_ARG, TAG,
"Received message: error status(%d)", message->info.status);
ESP_LOGD(TAG, "Received message: endpoint(%d), cluster(0x%x), attribute(0x%x), data size(%d)",
message->info.dst_endpoint, message->info.cluster, message->attribute.id, message->attribute.data.size);
return ret;
}
static esp_err_t zb_action_handler(esp_zb_core_action_callback_id_t callback_id, const void *message) {
esp_err_t ret = ESP_OK;
switch (callback_id) {
case ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID:
ret = zb_attribute_handler((esp_zb_zcl_set_attr_value_message_t *) message);
break;
default:
ESP_LOGD(TAG, "Receive Zigbee action(0x%x) callback", callback_id);
break;
}
return ret;
}
void ZigbeeComponent::create_default_cluster(uint8_t endpoint_id, zb_ha_standard_devs_e device_id) {
esp_zb_cluster_list_t *cluster_list = esp_zb_zcl_cluster_list_create();
this->endpoint_list_[endpoint_id] =
std::tuple<zb_ha_standard_devs_e, esp_zb_cluster_list_t *>(device_id, cluster_list);
// Add basic cluster
this->add_cluster(endpoint_id, ESP_ZB_ZCL_CLUSTER_ID_BASIC, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
// Add identify cluster if not already present
if (esp_zb_cluster_list_get_cluster(cluster_list, ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE) ==
nullptr) {
this->add_cluster(endpoint_id, ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
}
}
void ZigbeeComponent::add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role) {
esp_zb_attribute_list_t *attr_list;
if (cluster_id == 0) {
attr_list = create_basic_cluster_();
} else {
attr_list = esphome_zb_default_attr_list_create(cluster_id);
}
this->attribute_list_[{endpoint_id, cluster_id, role}] = attr_list;
}
void ZigbeeComponent::set_basic_cluster(const char *model, const char *manufacturer) {
char date_buf[16];
time_t time_val = App.get_build_time();
struct tm *timeinfo = localtime(&time_val);
strftime(date_buf, sizeof(date_buf), "%Y%m%d %H%M%S", timeinfo);
this->basic_cluster_data_ = {
.model = get_zcl_string(model, 31),
.manufacturer = get_zcl_string(manufacturer, 31),
.date = get_zcl_string(date_buf, 15),
};
}
esp_zb_attribute_list_t *ZigbeeComponent::create_basic_cluster_() {
esp_zb_basic_cluster_cfg_t basic_cluster_cfg = {
.zcl_version = ESP_ZB_ZCL_BASIC_ZCL_VERSION_DEFAULT_VALUE,
.power_source = 0,
};
esp_zb_attribute_list_t *attr_list = esp_zb_basic_cluster_create(&basic_cluster_cfg);
esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID,
this->basic_cluster_data_.manufacturer);
esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, this->basic_cluster_data_.model);
esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_DATE_CODE_ID, this->basic_cluster_data_.date);
return attr_list;
}
esp_err_t ZigbeeComponent::create_endpoint(uint8_t endpoint_id, zb_ha_standard_devs_e device_id,
esp_zb_cluster_list_t *esp_zb_cluster_list) {
esp_zb_endpoint_config_t endpoint_config = {.endpoint = endpoint_id,
.app_profile_id = ESP_ZB_AF_HA_PROFILE_ID,
.app_device_id = device_id,
.app_device_version = 0};
return esp_zb_ep_list_add_ep(this->esp_zb_ep_list_, esp_zb_cluster_list, endpoint_config);
}
static void esp_zb_task_(void *pvParameters) {
if (esp_zb_start(false) != ESP_OK) {
ESP_LOGE(TAG, "Could not setup Zigbee");
vTaskDelete(NULL);
}
esp_zb_set_node_descriptor_power_source(1);
esp_zb_stack_main_loop();
}
void ZigbeeComponent::setup() {
global_zigbee = this;
esp_zb_platform_config_t config = {
.radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(),
.host_config = ESP_ZB_DEFAULT_HOST_CONFIG(),
};
#ifdef USE_WIFI
if (esp_coex_wifi_i154_enable() != ESP_OK) {
this->mark_failed();
return;
}
#endif
if (esp_zb_platform_config(&config) != ESP_OK) {
this->mark_failed();
return;
}
esp_zb_zed_cfg_t zb_zed_cfg = {
.ed_timeout = ESP_ZB_ED_AGING_TIMEOUT_64MIN,
.keep_alive = ED_KEEP_ALIVE,
};
esp_zb_zczr_cfg_t zb_zczr_cfg = {
.max_children = MAX_CHILDREN,
};
esp_zb_cfg_t zb_nwk_cfg = {
.esp_zb_role = this->device_role_,
.install_code_policy = false,
};
#ifdef ZB_ROUTER_ROLE
zb_nwk_cfg.nwk_cfg.zczr_cfg = zb_zczr_cfg;
#else
zb_nwk_cfg.nwk_cfg.zed_cfg = zb_zed_cfg;
#endif
esp_zb_init(&zb_nwk_cfg);
esp_err_t ret;
for (auto const &[key, val] : this->attribute_list_) {
esp_zb_cluster_list_t *esp_zb_cluster_list = std::get<1>(this->endpoint_list_[std::get<0>(key)]);
ret = esphome_zb_cluster_list_add_or_update_cluster(std::get<1>(key), esp_zb_cluster_list, val, std::get<2>(key));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Could not create cluster 0x%04X with role %u: %s", std::get<1>(key), std::get<2>(key),
esp_err_to_name(ret));
} else {
ESP_LOGD(TAG, "Endpoint %u: Added cluster 0x%04X with role %u", std::get<0>(key), std::get<1>(key),
std::get<2>(key));
#ifdef ESPHOME_LOG_HAS_VERBOSE
// Dump cluster attributes in verbose log
ESP_LOGV(TAG, "Cluster 0x%04X attributes:", std::get<1>(key));
esp_zb_attribute_list_t *attr_list = val;
while (attr_list) {
esp_zb_zcl_attr_t *attr = &attr_list->attribute;
ESP_LOGV(TAG, " Attr ID: 0x%04X, Type: 0x%02X, Access: 0x%02X", attr->id, attr->type, attr->access);
attr_list = attr_list->next;
}
#endif
}
}
this->attribute_list_.clear();
for (auto const &[ep_id, dev_id] : this->endpoint_list_) {
if (create_endpoint(ep_id, std::get<0>(dev_id), std::get<1>(dev_id)) != ESP_OK) {
ESP_LOGE(TAG, "Could not create endpoint %u", ep_id);
}
}
this->endpoint_list_.clear();
if (esp_zb_device_register(this->esp_zb_ep_list_) != ESP_OK) {
ESP_LOGE(TAG, "Could not register the endpoint list");
this->mark_failed();
return;
}
esp_zb_core_action_handler_register(zb_action_handler);
if (esp_zb_set_primary_network_channel_set(ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK) != ESP_OK) {
ESP_LOGE(TAG, "Could not setup Zigbee");
this->mark_failed();
return;
}
for (auto &[_, attribute] : this->attributes_) {
if (attribute->report_enabled) {
esp_zb_zcl_reporting_info_t reporting_info = attribute->get_reporting_info();
ESP_LOGD(TAG, "set reporting for cluster: %u", reporting_info.cluster_id);
if (esp_zb_zcl_update_reporting_info(&reporting_info) != ESP_OK) {
ESP_LOGE(TAG, "Could not configure reporting for attribute 0x%04X in cluster 0x%04X in endpoint %u",
reporting_info.attr_id, reporting_info.cluster_id, reporting_info.ep);
}
}
}
xTaskCreate(esp_zb_task_, "Zigbee_main", 4096, NULL, 24, NULL);
}
void ZigbeeComponent::dump_config() {
if (esp_zb_lock_acquire(10 / portTICK_PERIOD_MS)) {
ESP_LOGCONFIG(TAG,
"Zigbee\n"
" Model: %s\n"
" Router: %s\n"
" Device is joined to the network: %s\n"
" Current channel: %d\n"
" Short addr: 0x%04X\n"
" Short pan id: 0x%04X",
this->basic_cluster_data_.model, YESNO(this->device_role_ == ESP_ZB_DEVICE_TYPE_ROUTER),
YESNO(esp_zb_bdb_dev_joined()), esp_zb_get_current_channel(), esp_zb_get_short_address(),
esp_zb_get_pan_id());
esp_zb_lock_release();
} else {
ESP_LOGCONFIG(TAG,
"Zigbee\n"
" Model: %s\n"
" Router: %s\n",
this->basic_cluster_data_.model, YESNO(this->device_role_ == ESP_ZB_DEVICE_TYPE_ROUTER));
}
}
} // namespace esphome::zigbee
#endif
#endif

View File

@@ -0,0 +1,134 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#ifdef USE_ZIGBEE
#include <map>
#include <tuple>
#include <atomic>
#include "esp_zigbee_core.h"
#include "zboss_api.h"
#include "ha/esp_zigbee_ha_standard.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "zigbee_helpers_esp32.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
namespace esphome::zigbee {
/* Zigbee configuration */
static const uint16_t ED_KEEP_ALIVE = 3000; /* 3000 millisecond */
static const uint8_t MAX_CHILDREN = 10;
#define ESP_ZB_DEFAULT_RADIO_CONFIG() \
{ .radio_mode = ZB_RADIO_MODE_NATIVE, }
#define ESP_ZB_DEFAULT_HOST_CONFIG() \
{ .host_connection_mode = ZB_HOST_CONNECTION_MODE_NONE, }
uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size = false);
class ZigbeeAttribute;
class ZigbeeComponent : public Component {
public:
void setup() override;
void dump_config() override;
esp_err_t create_endpoint(uint8_t endpoint_id, zb_ha_standard_devs_e device_id,
esp_zb_cluster_list_t *esp_zb_cluster_list);
void set_basic_cluster(const char *model, const char *manufacturer);
void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role);
void create_default_cluster(uint8_t endpoint_id, zb_ha_standard_devs_e device_id);
template<typename T>
void add_attr(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id,
uint8_t max_size, T value);
template<typename T>
void add_attr(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t max_size, T value);
void factory_reset() {
esp_zb_lock_acquire(portMAX_DELAY);
esp_zb_factory_reset(); // triggers a reboot
esp_zb_lock_release();
}
bool is_started() { return this->started; }
bool is_connected() { return this->connected; }
std::atomic<bool> connected = false;
std::atomic<bool> started = false;
protected:
struct {
uint8_t *model;
uint8_t *manufacturer;
uint8_t *date;
} basic_cluster_data_;
#ifdef ZB_ED_ROLE
esp_zb_nwk_device_type_t device_role_ = ESP_ZB_DEVICE_TYPE_ED;
#else
esp_zb_nwk_device_type_t device_role_ = ESP_ZB_DEVICE_TYPE_ROUTER;
#endif
esp_zb_attribute_list_t *create_basic_cluster_();
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);
// endpoint_list_ and attribute_list_ are only used during setup and are cleared afterwards
// value tuple could be replaced by struct
std::map<uint8_t, std::tuple<zb_ha_standard_devs_e, esp_zb_cluster_list_t *>> endpoint_list_;
// key tuple could be replaced by single 32 bit int with bit fields for endpoint, cluster and role
std::map<std::tuple<uint8_t, uint16_t, uint8_t>, esp_zb_attribute_list_t *> attribute_list_;
// 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
std::map<std::tuple<uint8_t, uint16_t, uint8_t, uint16_t>, ZigbeeAttribute *> attributes_;
esp_zb_ep_list_t *esp_zb_ep_list_ = esp_zb_ep_list_create();
};
extern "C" void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct);
template<typename T>
void ZigbeeComponent::add_attr(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id,
uint8_t max_size, T value) {
this->add_attr<T>(nullptr, endpoint_id, cluster_id, role, attr_id, max_size, value);
}
template<typename T>
void ZigbeeComponent::add_attr(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role,
uint16_t attr_id, uint8_t max_size, T value) {
// The size byte of the zcl_str must be set to the maximum value,
// even though the initial string may be shorter.
if constexpr (std::is_same<T, std::string>::value) {
auto zcl_str = get_zcl_string(value.c_str(), max_size, true);
add_attr_(attr, endpoint_id, cluster_id, role, attr_id, zcl_str);
delete[] zcl_str;
} else if constexpr (std::is_convertible<T, const char *>::value) {
auto zcl_str = get_zcl_string(value, max_size, true);
add_attr_(attr, endpoint_id, cluster_id, role, attr_id, zcl_str);
delete[] zcl_str;
} else {
add_attr_(attr, endpoint_id, cluster_id, role, attr_id, &value);
}
}
template<typename T>
void ZigbeeComponent::add_attr_(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role,
uint16_t attr_id, T *value_p) {
esp_zb_attribute_list_t *attr_list = this->attribute_list_[{endpoint_id, cluster_id, role}];
esp_err_t ret = esphome_zb_cluster_add_or_update_attr(cluster_id, attr_list, attr_id, value_p);
if (attr != nullptr) {
this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr;
}
}
} // namespace esphome::zigbee
#endif
#endif

View File

@@ -0,0 +1,274 @@
import copy
import logging
import re
from typing import Any
import esphome.codegen as cg
from esphome.components.esp32 import (
CONF_PARTITIONS,
add_idf_component,
add_idf_sdkconfig_option,
add_partition,
require_vfs_select,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AP,
CONF_DEVICE,
CONF_ID,
CONF_MAX_LENGTH,
CONF_MODEL,
CONF_NAME,
CONF_TYPE,
CONF_VALUE,
CONF_WIFI,
)
from esphome.core import CORE
from esphome.coroutine import CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.types import ConfigType
from .const import CONF_REPORT, CONF_ROUTER, KEY_ZIGBEE, REPORT, ZigbeeAttribute
from .const_esp32 import (
ATTR_TYPE,
CLUSTER_ID,
CONF_ATTRIBUTE_ID,
CONF_ATTRIBUTES,
CONF_CLUSTERS,
CONF_NUM,
DEVICE_ID,
DEVICE_TYPE,
KEY_BS_EP,
ROLE,
SCALE,
)
from .zigbee_ep_esp32 import create_ep, ep_configs
_LOGGER = logging.getLogger(__name__)
def get_c_size(bits: str, options: list[int]) -> str:
return str([n for n in options if n >= int(bits)][0])
def get_c_type(attr_type: str) -> Any | None:
if attr_type == "BOOL":
return cg.bool_
if "STRING" in attr_type:
return cg.std_string
test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type)
if test and test.group(2):
return getattr(cg, "uint" + get_c_size(test.group(2), [8, 16, 32, 64]))
return None
def get_cv_by_type(attr_type: str) -> Any | None:
if attr_type == "BOOL":
return cv.boolean
if "STRING" in attr_type:
return cv.string
test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type)
if test and test.group(2):
return cv.positive_int
return None
def get_default_by_type(attr_type: str) -> str | bool | int:
if attr_type == "CHAR_STRING":
return ""
if attr_type == "BOOL":
return False
return 0
def validate_attributes(config: ConfigType) -> ConfigType:
if CONF_VALUE not in config:
config[CONF_VALUE] = get_default_by_type(config[CONF_TYPE])
config[CONF_VALUE] = get_cv_by_type(config[CONF_TYPE])(config[CONF_VALUE])
return config
def final_validate_esp32(config: ConfigType) -> ConfigType:
if not CORE.is_esp32:
return config
if CONF_WIFI in fv.full_config.get():
if config[CONF_ROUTER] and CONF_AP in fv.full_config.get()[CONF_WIFI]:
raise cv.Invalid(
"Only Zigbee End Device can be used together with a Wifi Access Point."
)
if CONF_AP in fv.full_config.get()[CONF_WIFI]:
_LOGGER.warning(
"Wifi Access Point might be unstable while Zigbee is active, use only as fallback."
)
elif config[CONF_ROUTER]:
_LOGGER.warning(
"The Zigbee Router might miss packets while Wifi is active and could destabilize "
"your network. Use only if Wifi is off most of the time."
)
if CONF_PARTITIONS in fv.full_config.get() and not isinstance(
fv.full_config.get()[CONF_PARTITIONS], list
):
with open(
CORE.relative_config_path(fv.full_config.get()[CONF_PARTITIONS]),
encoding="utf8",
) as f:
partitions_tab = f.read()
for partition, types in [
("zb_storage", {"type": "data", "subtype": "fat", "size": 0x4000}),
("zb_fct", {"type": "data", "subtype": "fat", "size": 0x1000}),
]:
if partition not in partitions_tab:
raise cv.Invalid(
f"Add '{partition}, {types['type']}, {types['subtype']}, , {types['size']},' to your custom partition table."
)
if not re.search(
rf"^{partition},\s*{types['type']},\s*{types['subtype']}",
partitions_tab,
re.MULTILINE,
):
raise cv.Invalid(
f"Partition '{partition}' in your custom partition table has wrong format. It should be: '{partition}, {types['type']}, {types['subtype']}, , {types['size']},'"
)
return config
def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType:
ep = copy.deepcopy(ep_configs["binary_input"])
for cl in ep.get(CONF_CLUSTERS, []):
for attr in cl[CONF_ATTRIBUTES]:
if (
attr[CONF_ATTRIBUTE_ID] == 0x1C
and CONF_VALUE not in attr
and CONF_NAME in config
): # set name
name = (
config[CONF_NAME].encode("ascii", "ignore").decode()
) # or use unidecode
attr[CONF_VALUE] = str(name)
attr[CONF_MAX_LENGTH] = len(str(name))
if CONF_DEVICE in attr: # connect device
attr[CONF_DEVICE] = config[CONF_ID]
if CONF_REPORT in config:
attr[CONF_REPORT] = config[CONF_REPORT]
attr[CONF_ID] = cv.declare_id(ZigbeeAttribute)(None)
if "zb_attr_ids" not in config:
config["zb_attr_ids"] = []
config["zb_attr_ids"].append(attr[CONF_ID])
else:
attr[CONF_ID] = None
validate_attributes(attr)
zb_data = CORE.data.setdefault(KEY_ZIGBEE, {})
binary_sensor_ep: list[dict] = zb_data.setdefault(KEY_BS_EP, [])
binary_sensor_ep.append(ep)
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
if CORE.is_esp32:
require_vfs_select()
return config
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
async def _zigbee_add_sdkconfigs(config: ConfigType) -> None:
"""Add sdkconfigs late so they can overwrite esp32 defaults"""
add_idf_sdkconfig_option("CONFIG_ZB_ENABLED", True)
if config.get(CONF_ROUTER):
add_idf_sdkconfig_option("CONFIG_ZB_ZCZR", True)
else:
add_idf_sdkconfig_option("CONFIG_ZB_ZED", True)
add_idf_sdkconfig_option("CONFIG_ZB_RADIO_NATIVE", True)
if CONF_WIFI in CORE.config:
add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE", 4096)
# The pre-built Zigbee library uses esp_log_default_level which requires
# dynamic log level control to be enabled
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", True)
async def attributes_to_code(
var: cg.Pvariable, ep_num: int, cl: dict[str, Any]
) -> None:
for attr in cl.get(CONF_ATTRIBUTES, []):
if attr.get(CONF_ID) is None:
cg.add(
var.add_attr(
ep_num,
CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]),
cl[ROLE],
attr[CONF_ATTRIBUTE_ID],
attr.get(CONF_MAX_LENGTH, 0),
attr[CONF_VALUE],
)
)
continue
attr_var = cg.new_Pvariable(
attr[CONF_ID],
var,
ep_num,
CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]),
cl[ROLE],
attr[CONF_ATTRIBUTE_ID],
ATTR_TYPE[attr[CONF_TYPE]],
attr.get(SCALE, 1),
attr.get(CONF_MAX_LENGTH, 0),
)
await cg.register_component(attr_var, attr)
cg.add(attr_var.add_attr(attr[CONF_VALUE]))
if CONF_REPORT in attr and attr[CONF_REPORT] in [
REPORT["enable"],
REPORT["force"],
]:
cg.add(attr_var.set_report(attr[CONF_REPORT] == REPORT["force"]))
if CONF_DEVICE in attr:
device = await cg.get_variable(attr[CONF_DEVICE])
template_arg = cg.TemplateArguments(get_c_type(attr[CONF_TYPE]))
cg.add(attr_var.connect(template_arg, device))
async def esp32_to_code(config: ConfigType) -> None:
add_idf_component(
name="espressif/esp-zboss-lib",
ref="1.6.4",
)
add_idf_component(
name="espressif/esp-zigbee-lib",
ref="1.6.8",
)
# add sdkconfigs later so they can overwrite esp32 defaults
CORE.add_job(_zigbee_add_sdkconfigs, config)
# add partitions for zigbee
add_partition("zb_storage", "data", "fat", 0x4000) # 16KB
add_partition("zb_fct", "data", "fat", 0x1000) # 4KB, minimum size
# create endpoints
zb_data = CORE.data.get(KEY_ZIGBEE, {})
binary_sensor_ep: list[dict] = zb_data.get(KEY_BS_EP, [])
ep_list = create_ep(binary_sensor_ep, config.get(CONF_ROUTER))
# setup zigbee components
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(
var.set_basic_cluster(
config[CONF_MODEL],
"esphome",
)
)
for ep in ep_list:
cg.add(var.create_default_cluster(ep[CONF_NUM], DEVICE_ID[ep[DEVICE_TYPE]]))
for cl in ep.get(CONF_CLUSTERS, []):
cg.add(
var.add_cluster(
ep[CONF_NUM],
CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]),
cl[ROLE],
)
)
await attributes_to_code(var, ep[CONF_NUM], cl)

View File

@@ -0,0 +1,74 @@
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#ifdef USE_ZIGBEE
#include "ha/esp_zigbee_ha_standard.h"
#include "zigbee_helpers_esp32.h"
esp_err_t esphome_zb_cluster_add_or_update_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list,
uint16_t attr_id, void *value_p) {
esp_err_t ret;
ret = esp_zb_cluster_update_attr(attr_list, attr_id, value_p);
if (ret != ESP_OK) {
ESP_LOGE("zigbee_helper", "Ignore previous attribute not found error");
ret = esphome_zb_cluster_add_attr(cluster_id, attr_list, attr_id, value_p);
}
if (ret != ESP_OK) {
ESP_LOGE("zigbee_helper", "Could not add attribute 0x%04X to cluster 0x%04X: %s", attr_id, cluster_id,
esp_err_to_name(ret));
}
return ret;
}
esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp_zb_cluster_list_t *cluster_list,
esp_zb_attribute_list_t *attr_list, uint8_t role_mask) {
esp_err_t ret;
ret = esp_zb_cluster_list_update_cluster(cluster_list, attr_list, cluster_id, role_mask);
if (ret != ESP_OK) {
ESP_LOGE("zigbee_helper", "Ignore previous cluster not found error");
switch (cluster_id) {
case ESP_ZB_ZCL_CLUSTER_ID_BASIC:
ret = esp_zb_cluster_list_add_basic_cluster(cluster_list, attr_list, role_mask);
break;
case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY:
ret = esp_zb_cluster_list_add_identify_cluster(cluster_list, attr_list, role_mask);
break;
case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT:
ret = esp_zb_cluster_list_add_binary_input_cluster(cluster_list, attr_list, role_mask);
break;
default:
ret = esp_zb_cluster_list_add_custom_cluster(cluster_list, attr_list, role_mask);
}
}
return ret;
}
esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id) {
switch (cluster_id) {
case ESP_ZB_ZCL_CLUSTER_ID_BASIC:
return esp_zb_basic_cluster_create(NULL);
case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY:
return esp_zb_identify_cluster_create(NULL);
case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT:
return esp_zb_binary_input_cluster_create(NULL);
default:
return esp_zb_zcl_attr_list_create(cluster_id);
}
}
esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id,
void *value_p) {
switch (cluster_id) {
case ESP_ZB_ZCL_CLUSTER_ID_BASIC:
return esp_zb_basic_cluster_add_attr(attr_list, attr_id, value_p);
case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY:
return esp_zb_identify_cluster_add_attr(attr_list, attr_id, value_p);
case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT:
return esp_zb_binary_input_cluster_add_attr(attr_list, attr_id, value_p);
default:
return ESP_FAIL;
}
}
#endif
#endif

View File

@@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#ifdef USE_ZIGBEE
#ifdef __cplusplus
extern "C" {
#endif
#include "esp_zigbee_core.h"
esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp_zb_cluster_list_t *cluster_list,
esp_zb_attribute_list_t *attr_list, uint8_t role_mask);
esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id);
esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id,
void *value_p);
esp_err_t esphome_zb_cluster_add_or_update_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list,
uint16_t attr_id, void *value_p);
#ifdef __cplusplus
}
namespace esphome::zigbee {} // namespace esphome::zigbee
#endif
#endif
#endif

View File

@@ -1,4 +1,4 @@
from datetime import datetime
import datetime
import random
from esphome import automation
@@ -7,6 +7,7 @@ from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_MODEL,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
UNIT_AMPERE,
@@ -48,19 +49,26 @@ from esphome.cpp_generator import (
)
from esphome.types import ConfigType
from .const_zephyr import (
CONF_IEEE802154_VENDOR_OUI,
from .const import (
CONF_ON_JOIN,
CONF_POWER_SOURCE,
CONF_WIPE_ON_BOOT,
KEY_ZIGBEE,
POWER_SOURCE,
AnalogAttrs,
AnalogAttrsOutput,
BinaryAttrs,
ZigbeeComponent,
zigbee_ns,
)
from .const_zephyr import (
CONF_IEEE802154_VENDOR_OUI,
CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID,
CONF_ZIGBEE_NUMBER,
CONF_ZIGBEE_SENSOR,
CONF_ZIGBEE_SWITCH,
KEY_EP_NUMBER,
KEY_ZIGBEE,
POWER_SOURCE,
ZB_ZCL_BASIC_ATTRS_EXT_T,
ZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT,
@@ -69,11 +77,6 @@ from .const_zephyr import (
ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT,
ZB_ZCL_CLUSTER_ID_IDENTIFY,
ZB_ZCL_IDENTIFY_ATTRS_T,
AnalogAttrs,
AnalogAttrsOutput,
BinaryAttrs,
ZigbeeComponent,
zigbee_ns,
)
ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component)
@@ -209,9 +212,9 @@ async def _attr_to_code(config: ConfigType) -> None:
zigbee_assign(basic_attrs.stack_version, 0),
zigbee_assign(basic_attrs.hw_version, 0),
zigbee_set_string(basic_attrs.mf_name, "esphome"),
zigbee_set_string(basic_attrs.model_id, CORE.name),
zigbee_set_string(basic_attrs.model_id, config[CONF_MODEL]),
zigbee_set_string(
basic_attrs.date_code, datetime.now().strftime("%d/%m/%y %H:%M")
basic_attrs.date_code, datetime.datetime.now().strftime("%Y%m%d %H%M%S")
),
zigbee_assign(
basic_attrs.power_source,

View File

@@ -322,6 +322,7 @@
#define USE_MICRO_WAKE_WORD_VAD
#if defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2)
#define USE_OPENTHREAD
#define USE_ZIGBEE
#endif
#endif

View File

@@ -37,6 +37,14 @@ dependencies:
version: "2.0.0"
rules:
- if: "target in [esp32, esp32p4]"
espressif/esp-zboss-lib:
version: 1.6.4
rules:
- if: "target in [esp32h2, esp32c5, esp32c6]"
espressif/esp-zigbee-lib:
version: 1.6.8
rules:
- if: "target in [esp32h2, esp32c5, esp32c6]"
espressif/lan87xx:
version: "1.0.0"
rules:

View File

@@ -20,3 +20,8 @@ CONFIG_BT_ENABLED=y
# esp32_camera
CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC=y
CONFIG_ESP32_SPIRAM_SUPPORT=y
# zigbee
CONFIG_ZB_ENABLED=y
CONFIG_ZB_ZED=y
CONFIG_ZB_RADIO_NATIVE=y

View File

@@ -1,4 +1,3 @@
---
binary_sensor:
- platform: template
name: "Garage Door Open 1"
@@ -22,12 +21,6 @@ sensor:
lambda: return 12.0;
internal: True
zigbee:
wipe_on_boot: true
on_join:
then:
- logger.log: "Joined network"
output:
- platform: template
id: output_factory
@@ -35,9 +28,6 @@ output:
write_action:
- zigbee.factory_reset
time:
- platform: zigbee
switch:
- platform: template
name: "Template Switch"

View File

@@ -0,0 +1,14 @@
binary_sensor:
- platform: template
name: "Garage Door Open 10"
report: "enable"
- platform: template
name: "Garage Door Open 11"
report: "coordinator"
- platform: template
name: "Garage Door Open 12"
report: "force"
zigbee:
model: zigbee_test
router: true

View File

@@ -0,0 +1,12 @@
packages:
- !include common.yaml
zigbee:
model: zigbee_test
wipe_on_boot: true
on_join:
then:
- logger.log: "Joined network"
time:
- platform: zigbee

View File

@@ -0,0 +1 @@
<<: !include common_esp32.yaml

View File

@@ -1 +1 @@
<<: !include common.yaml
<<: !include common_nrf52.yaml

View File

@@ -1 +1 @@
<<: !include common.yaml
<<: !include common_nrf52.yaml

View File

@@ -1,4 +1,4 @@
<<: !include common.yaml
<<: !include common_nrf52.yaml
zigbee:
wipe_on_boot: once