[nrf52,zigbee] add support for binary_input (#11535)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
tomaszduda23
2026-01-05 03:11:14 +01:00
committed by GitHub
parent ab0e15e4bb
commit 12027569d3
17 changed files with 866 additions and 3 deletions

View File

@@ -575,5 +575,6 @@ esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt
esphome/components/zwave_proxy/* @kbx81

View File

@@ -3,7 +3,7 @@ from logging import getLogger
from esphome import automation, core
from esphome.automation import Condition, maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
from esphome.components import mqtt, web_server, zigbee
from esphome.components.const import CONF_ON_STATE_CHANGE
import esphome.config_validation as cv
from esphome.const import (
@@ -439,6 +439,7 @@ def validate_publish_initial_state(value):
_BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(zigbee.BINARY_SENSOR_SCHEMA)
.extend(
{
cv.GenerateID(): cv.declare_id(BinarySensor),
@@ -520,6 +521,7 @@ _BINARY_SENSOR_SCHEMA = (
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
_BINARY_SENSOR_SCHEMA.add_extra(zigbee.validate_binary_sensor)
def binary_sensor_schema(
@@ -621,6 +623,8 @@ async def setup_binary_sensor_core_(var, config):
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
await zigbee.setup_binary_sensor(var, config)
async def register_binary_sensor(var, config):
if not CORE.has_id(config[CONF_ID]):

View File

@@ -26,7 +26,12 @@ void arch_init() {
if (device_is_ready(WDT)) {
static wdt_timeout_cfg wdt_config{};
wdt_config.flags = WDT_FLAG_RESET_SOC;
#ifdef USE_ZIGBEE
// zboss thread use a lot of cpu cycles during start
wdt_config.window.max = 10000;
#else
wdt_config.window.max = 2000;
#endif
wdt_channel_id = wdt_install_timeout(WDT, &wdt_config);
if (wdt_channel_id >= 0) {
uint8_t options = 0;

View File

@@ -0,0 +1,124 @@
from typing import Any
from esphome import automation, core
import esphome.codegen as cg
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
from esphome.core import CORE
from esphome.types import ConfigType
from .const_zephyr import (
CONF_MAX_EP_NUMBER,
CONF_ON_JOIN,
CONF_WIPE_ON_BOOT,
CONF_ZIGBEE_ID,
KEY_EP_NUMBER,
KEY_ZIGBEE,
ZigbeeComponent,
zigbee_ns,
)
from .zigbee_zephyr import zephyr_binary_sensor
CODEOWNERS = ["@tomaszduda23"]
def zigbee_set_core_data(config: ConfigType) -> ConfigType:
if zephyr_data()[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
zephyr_add_pm_static(
[Section("empty_after_zboss_offset", 0xF4000, 0xC000, "flash_primary")]
)
return config
BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor)
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.boolean,
cv.requires_component("nrf52"),
),
}
).extend(cv.COMPONENT_SCHEMA),
zigbee_set_core_data,
cv.only_with_framework("zephyr"),
)
def validate_number_of_ep(config: ConfigType) -> None:
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])
if count == 1:
raise cv.Invalid(
"Single endpoint is not supported https://github.com/Koenkk/zigbee2mqtt/issues/29888"
)
if count > CONF_MAX_EP_NUMBER:
raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}")
FINAL_VALIDATE_SCHEMA = cv.All(
validate_number_of_ep,
)
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_ZIGBEE")
if CORE.using_zephyr:
from .zigbee_zephyr import zephyr_to_code
await zephyr_to_code(config)
async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
return
if CORE.using_zephyr:
from .zigbee_zephyr import zephyr_setup_binary_sensor
await zephyr_setup_binary_sensor(entity, config)
def validate_binary_sensor(config: ConfigType) -> ConfigType:
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
return config
data: dict[str, Any] = CORE.data.setdefault(KEY_ZIGBEE, {})
slots: list[str] = data.setdefault(KEY_EP_NUMBER, [])
slots.extend([""])
return config
ZIGBEE_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema(
{
cv.GenerateID(): cv.use_id(ZigbeeComponent),
}
)
)
FactoryResetAction = zigbee_ns.class_(
"FactoryResetAction", automation.Action, cg.Parented.template(ZigbeeComponent)
)
@automation.register_action(
"zigbee.factory_reset",
FactoryResetAction,
ZIGBEE_ACTION_SCHEMA,
)
async def reset_zigbee_to_code(
config: ConfigType,
action_id: core.ID,
template_arg: cg.TemplateArguments,
args: list[tuple],
) -> cg.Pvariable:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@@ -0,0 +1,16 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ZIGBEE
#ifdef USE_NRF52
#include "zigbee_zephyr.h"
#endif
namespace esphome::zigbee {
template<typename... Ts> class FactoryResetAction : public Action<Ts...>, public Parented<ZigbeeComponent> {
public:
void play(const Ts &...x) override { this->parent_->factory_reset(); }
};
} // namespace esphome::zigbee
#endif

View File

@@ -0,0 +1,24 @@
import esphome.codegen as cg
zigbee_ns = cg.esphome_ns.namespace("zigbee")
ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component)
BinaryAttrs = zigbee_ns.struct("BinaryAttrs")
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"
# Keys for CORE.data storage
KEY_ZIGBEE = "zigbee"
KEY_EP_NUMBER = "ep_number"
# External ZBOSS SDK types (just strings for codegen)
ZB_ZCL_BASIC_ATTRS_EXT_T = "zb_zcl_basic_attrs_ext_t"
ZB_ZCL_IDENTIFY_ATTRS_T = "zb_zcl_identify_attrs_t"
# Cluster IDs
ZB_ZCL_CLUSTER_ID_BASIC = "ZB_ZCL_CLUSTER_ID_BASIC"
ZB_ZCL_CLUSTER_ID_IDENTIFY = "ZB_ZCL_CLUSTER_ID_IDENTIFY"
ZB_ZCL_CLUSTER_ID_BINARY_INPUT = "ZB_ZCL_CLUSTER_ID_BINARY_INPUT"

View File

@@ -0,0 +1,37 @@
#include "zigbee_binary_sensor_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_BINARY_SENSOR)
#include "esphome/core/log.h"
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
#include <zb_nrf_platform.h>
#include <zigbee/zigbee_app_utils.h>
#include <zb_error_to_string.h>
}
namespace esphome::zigbee {
static const char *const TAG = "zigbee.binary_sensor";
ZigbeeBinarySensor::ZigbeeBinarySensor(binary_sensor::BinarySensor *binary_sensor) : binary_sensor_(binary_sensor) {}
void ZigbeeBinarySensor::setup() {
this->binary_sensor_->add_on_state_callback([this](bool state) {
this->cluster_attributes_->present_value = state ? ZB_TRUE : ZB_FALSE;
ESP_LOGD(TAG, "Set attribute end point: %d, present_value %d", this->end_point_,
this->cluster_attributes_->present_value);
ZB_ZCL_SET_ATTRIBUTE(this->end_point_, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value,
ZB_FALSE);
this->parent_->flush();
});
}
void ZigbeeBinarySensor::dump_config() {
ESP_LOGCONFIG(TAG,
"Zigbee Binary Sensor\n"
" End point: %d, present_value %u",
this->end_point_, this->cluster_attributes_->present_value);
}
} // namespace esphome::zigbee
#endif

View File

@@ -0,0 +1,45 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_BINARY_SENSOR)
#include "esphome/components/zigbee/zigbee_zephyr.h"
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
}
// it should have been defined inside of sdk. It is missing though
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \
(ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \
}
// copy of ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST + description
#define ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \
description) \
ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_BINARY_INPUT) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_OUT_OF_SERVICE_ID, (out_of_service)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, (present_value)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_STATUS_FLAG_ID, (status_flag)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID, (description)) \
ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST
namespace esphome::zigbee {
class ZigbeeBinarySensor : public ZigbeeEntity, public Component {
public:
explicit ZigbeeBinarySensor(binary_sensor::BinarySensor *binary_sensor);
void set_cluster_attributes(BinaryAttrs &cluster_attributes) { this->cluster_attributes_ = &cluster_attributes; }
void setup() override;
void dump_config() override;
protected:
BinaryAttrs *cluster_attributes_{nullptr};
binary_sensor::BinarySensor *binary_sensor_;
};
} // namespace esphome::zigbee
#endif

View File

@@ -0,0 +1,190 @@
#include "zigbee_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52)
#include "esphome/core/log.h"
#include <zephyr/settings/settings.h>
#include <zephyr/storage/flash_map.h>
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
#include <zb_nrf_platform.h>
#include <zigbee/zigbee_app_utils.h>
#include <zb_error_to_string.h>
}
namespace esphome::zigbee {
static const char *const TAG = "zigbee";
ZigbeeComponent *global_zigbee = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
const uint8_t IEEE_ADDR_BUF_SIZE = 17;
void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) {
zb_zdo_app_signal_hdr_t *sig_hndler = nullptr;
zb_zdo_app_signal_type_t sig = zb_get_app_signal(bufid, &sig_hndler);
zb_ret_t status = ZB_GET_APP_SIGNAL_STATUS(bufid);
switch (sig) {
case ZB_ZDO_SIGNAL_SKIP_STARTUP:
ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_SKIP_STARTUP, status: %d", status);
break;
case ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY:
ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY, status: %d", status);
break;
case ZB_ZDO_SIGNAL_LEAVE:
ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_LEAVE, status: %d", status);
break;
case ZB_BDB_SIGNAL_DEVICE_REBOOT:
ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_REBOOT, status: %d", status);
if (status == RET_OK) {
on_join_();
}
break;
case ZB_BDB_SIGNAL_STEERING:
break;
case ZB_COMMON_SIGNAL_CAN_SLEEP:
ESP_LOGV(TAG, "ZB_COMMON_SIGNAL_CAN_SLEEP, status: %d", status);
break;
case ZB_BDB_SIGNAL_DEVICE_FIRST_START:
ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_FIRST_START, status: %d", status);
break;
case ZB_NLME_STATUS_INDICATION:
ESP_LOGD(TAG, "ZB_NLME_STATUS_INDICATION, status: %d", status);
break;
case ZB_BDB_SIGNAL_TC_REJOIN_DONE:
ESP_LOGD(TAG, "ZB_BDB_SIGNAL_TC_REJOIN_DONE, status: %d", status);
break;
default:
ESP_LOGD(TAG, "zboss_signal_handler sig: %d, status: %d", sig, status);
break;
}
auto err = zigbee_default_signal_handler(bufid);
if (err != RET_OK) {
ESP_LOGE(TAG, "Zigbee_default_signal_handler ERROR %u [%s]", err, zb_error_to_string_get(err));
}
switch (sig) {
case ZB_BDB_SIGNAL_STEERING:
ESP_LOGD(TAG, "ZB_BDB_SIGNAL_STEERING, status: %d", status);
if (status == RET_OK) {
zb_ext_pan_id_t extended_pan_id;
char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0};
int addr_len;
zb_get_extended_pan_id(extended_pan_id);
addr_len = ieee_addr_to_str(ieee_addr_buf, sizeof(ieee_addr_buf), extended_pan_id);
for (int i = 0; i < addr_len; ++i) {
if (ieee_addr_buf[i] != '0') {
on_join_();
break;
}
}
}
break;
}
/* All callbacks should either reuse or free passed buffers.
* If bufid == 0, the buffer is invalid (not passed).
*/
if (bufid) {
zb_buf_free(bufid);
}
}
void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t);
zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id;
zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id;
zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id;
auto endpoint = p_device_cb_param->endpoint;
ESP_LOGI(TAG, "Zcl_device_cb %s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id,
attr_id, endpoint);
// endpoints are enumerated from 1
if (global_zigbee->callbacks_.size() >= endpoint) {
global_zigbee->callbacks_[endpoint - 1](bufid);
return;
}
p_device_cb_param->status = RET_ERROR;
}
void ZigbeeComponent::on_join_() {
this->defer([this]() {
ESP_LOGD(TAG, "Joined the network");
this->join_trigger_.trigger();
this->join_cb_.call();
});
}
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
void ZigbeeComponent::erase_flash_(int area) {
const struct flash_area *fap;
flash_area_open(area, &fap);
flash_area_erase(fap, 0, fap->fa_size);
flash_area_close(fap);
}
#endif
void ZigbeeComponent::setup() {
global_zigbee = this;
auto err = settings_subsys_init();
if (err) {
ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err);
return;
}
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
erase_flash_(FIXED_PARTITION_ID(ZBOSS_NVRAM));
erase_flash_(FIXED_PARTITION_ID(ZBOSS_PRODUCT_CONFIG));
erase_flash_(FIXED_PARTITION_ID(SETTINGS_STORAGE));
#endif
ZB_ZCL_REGISTER_DEVICE_CB(zcl_device_cb);
err = settings_load();
if (err) {
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
return;
}
zigbee_enable();
}
void ZigbeeComponent::dump_config() {
bool wipe = false;
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
wipe = true;
#endif
ESP_LOGCONFIG(TAG,
"Zigbee\n"
" Wipe on boot: %s",
YESNO(wipe));
}
static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) {
ESP_LOGD(TAG, "Force zboss scheduler to wake and send attribute report");
zb_buf_free(bufid);
}
void ZigbeeComponent::flush() { this->need_flush_ = true; }
void ZigbeeComponent::loop() {
if (this->need_flush_) {
this->need_flush_ = false;
zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0);
}
}
void ZigbeeComponent::factory_reset() {
ESP_LOGD(TAG, "Factory reset");
ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0);
}
} // namespace esphome::zigbee
extern "C" void zboss_signal_handler(zb_uint8_t param) {
esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param);
}
#endif

View File

@@ -0,0 +1,104 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52)
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
}
// copy of ZB_DECLARE_SIMPLE_DESC. Due to https://github.com/nrfconnect/sdk-nrfxlib/pull/666
#define ESPHOME_ZB_DECLARE_SIMPLE_DESC(ep_name, in_clusters_count, out_clusters_count) \
typedef ZB_PACKED_PRE struct zb_af_simple_desc_##ep_name##_##in_clusters_count##_##out_clusters_count##_s { \
zb_uint8_t endpoint; /* Endpoint */ \
zb_uint16_t app_profile_id; /* Application profile identifier */ \
zb_uint16_t app_device_id; /* Application device identifier */ \
zb_bitfield_t app_device_version : 4; /* Application device version */ \
zb_bitfield_t reserved : 4; /* Reserved */ \
zb_uint8_t app_input_cluster_count; /* Application input cluster count */ \
zb_uint8_t app_output_cluster_count; /* Application output cluster count */ \
/* Application input and output cluster list */ \
zb_uint16_t app_cluster_list[(in_clusters_count) + (out_clusters_count)]; \
} ZB_PACKED_STRUCT zb_af_simple_desc_##ep_name##_##in_clusters_count##_##out_clusters_count##_t
#define ESPHOME_CAT7(a, b, c, d, e, f, g) a##b##c##d##e##f##g
// needed to use ESPHOME_ZB_DECLARE_SIMPLE_DESC
#define ESPHOME_ZB_AF_SIMPLE_DESC_TYPE(ep_name, in_num, out_num) \
ESPHOME_CAT7(zb_af_simple_desc_, ep_name, _, in_num, _, out_num, _t)
// needed to use ESPHOME_ZB_DECLARE_SIMPLE_DESC
#define ESPHOME_ZB_ZCL_DECLARE_SIMPLE_DESC(ep_name, ep_id, in_clust_num, out_clust_num, ...) \
ESPHOME_ZB_DECLARE_SIMPLE_DESC(ep_name, in_clust_num, out_clust_num); \
ESPHOME_ZB_AF_SIMPLE_DESC_TYPE(ep_name, in_clust_num, out_clust_num) \
simple_desc_##ep_name = {ep_id, ZB_AF_HA_PROFILE_ID, ZB_HA_SIMPLE_SENSOR_DEVICE_ID, 0, 0, in_clust_num, \
out_clust_num, {__VA_ARGS__}}
// needed to use ESPHOME_ZB_ZCL_DECLARE_SIMPLE_DESC
#define ESPHOME_ZB_HA_DECLARE_EP(ep_name, ep_id, cluster_list, in_cluster_num, out_cluster_num, report_attr_count, \
...) \
ESPHOME_ZB_ZCL_DECLARE_SIMPLE_DESC(ep_name, ep_id, in_cluster_num, out_cluster_num, __VA_ARGS__); \
ZBOSS_DEVICE_DECLARE_REPORTING_CTX(reporting_info##ep_name, report_attr_count); \
ZB_AF_DECLARE_ENDPOINT_DESC(ep_name, ep_id, ZB_AF_HA_PROFILE_ID, 0, NULL, \
ZB_ZCL_ARRAY_SIZE(cluster_list, zb_zcl_cluster_desc_t), cluster_list, \
(zb_af_simple_desc_1_1_t *) &simple_desc_##ep_name, report_attr_count, \
reporting_info##ep_name, 0, NULL)
namespace esphome::zigbee {
struct BinaryAttrs {
zb_bool_t out_of_service;
zb_bool_t present_value;
zb_uint8_t status_flags;
zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE];
};
struct AnalogAttrs {
zb_bool_t out_of_service;
float present_value;
zb_uint8_t status_flags;
zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE];
float max_present_value;
float min_present_value;
float resolution;
};
class ZigbeeComponent : public Component {
public:
void setup() override;
void dump_config() override;
void add_callback(zb_uint8_t endpoint, std::function<void(zb_bufid_t bufid)> &&cb) {
// endpoints are enumerated from 1
this->callbacks_[endpoint - 1] = std::move(cb);
}
void add_join_callback(std::function<void()> &&cb) { this->join_cb_.add(std::move(cb)); }
void zboss_signal_handler_esphome(zb_bufid_t bufid);
void factory_reset();
Trigger<> *get_join_trigger() { return &this->join_trigger_; };
void flush();
void loop() override;
protected:
static void zcl_device_cb(zb_bufid_t bufid);
void on_join_();
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
void erase_flash_(int area);
#endif
StaticVector<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_;
CallbackManager<void()> join_cb_;
Trigger<> join_trigger_;
bool need_flush_{false};
};
class ZigbeeEntity {
public:
void set_parent(ZigbeeComponent *parent) { this->parent_ = parent; }
void set_end_point(zb_uint8_t end_point) { this->end_point_ = end_point; }
protected:
zb_uint8_t end_point_{0};
ZigbeeComponent *parent_{nullptr};
};
} // namespace esphome::zigbee
#endif

View File

@@ -0,0 +1,265 @@
from datetime import datetime
from esphome import automation
import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, __version__
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import (
AssignmentExpression,
MockObj,
VariableDeclarationExpression,
)
from esphome.types import ConfigType
from .const_zephyr import (
CONF_ON_JOIN,
CONF_WIPE_ON_BOOT,
CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID,
KEY_EP_NUMBER,
KEY_ZIGBEE,
ZB_ZCL_BASIC_ATTRS_EXT_T,
ZB_ZCL_CLUSTER_ID_BASIC,
ZB_ZCL_CLUSTER_ID_BINARY_INPUT,
ZB_ZCL_CLUSTER_ID_IDENTIFY,
ZB_ZCL_IDENTIFY_ATTRS_T,
BinaryAttrs,
ZigbeeComponent,
zigbee_ns,
)
ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component)
zephyr_binary_sensor = cv.Schema(
{
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent),
cv.OnlyWith(CONF_ZIGBEE_BINARY_SENSOR, ["nrf52", "zigbee"]): cv.declare_id(
ZigbeeBinarySensor
),
}
)
async def zephyr_to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("ZIGBEE", True)
zephyr_add_prj_conf("ZIGBEE_APP_UTILS", True)
zephyr_add_prj_conf("ZIGBEE_ROLE_END_DEVICE", True)
zephyr_add_prj_conf("ZIGBEE_CHANNEL_SELECTION_MODE_MULTI", True)
zephyr_add_prj_conf("CRYPTO", True)
zephyr_add_prj_conf("NET_IPV6", False)
zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False)
zephyr_add_prj_conf("NET_UDP", False)
if config[CONF_WIPE_ON_BOOT]:
cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT")
var = cg.new_Pvariable(config[CONF_ID])
if on_join_config := config.get(CONF_ON_JOIN):
await automation.build_automation(var.get_join_trigger(), [], on_join_config)
await cg.register_component(var, config)
await _attr_to_code(config)
CORE.add_job(_ctx_to_code, config)
async def _attr_to_code(config: ConfigType) -> None:
# Create the basic attributes structure and attribute list
basic_attrs = zigbee_new_variable("zigbee_basic_attrs", ZB_ZCL_BASIC_ATTRS_EXT_T)
zigbee_new_attr_list(
"zigbee_basic_attrib_list",
"ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT",
zigbee_assign(basic_attrs.zcl_version, cg.RawExpression("ZB_ZCL_VERSION")),
zigbee_assign(basic_attrs.app_version, 0),
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.date_code, datetime.now().strftime("%d/%m/%y %H:%M")
),
zigbee_assign(
basic_attrs.power_source,
cg.RawExpression("ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE"),
),
zigbee_set_string(basic_attrs.location_id, ""),
zigbee_assign(
basic_attrs.ph_env, cg.RawExpression("ZB_ZCL_BASIC_ENV_UNSPECIFIED")
),
zigbee_set_string(basic_attrs.sw_ver, __version__),
)
# Create the identify attributes structure and attribute list
identify_attrs = zigbee_new_variable(
"zigbee_identify_attrs", ZB_ZCL_IDENTIFY_ATTRS_T
)
zigbee_new_attr_list(
"zigbee_identify_attrib_list",
"ZB_ZCL_DECLARE_IDENTIFY_ATTRIB_LIST",
zigbee_assign(
identify_attrs.identify_time,
cg.RawExpression("ZB_ZCL_IDENTIFY_IDENTIFY_TIME_DEFAULT_VALUE"),
),
)
def zigbee_new_variable(name: str, type_: str) -> cg.MockObj:
"""Create a global variable with the given name and type."""
decl = VariableDeclarationExpression(type_, "", name)
CORE.add_global(decl)
return MockObj(name, ".")
def zigbee_assign(target: cg.MockObj, expression: cg.RawExpression | int) -> str:
"""Assign an expression to a target and return a reference to it."""
cg.add(AssignmentExpression("", "", target, expression))
return f"&{target}"
def zigbee_set_string(target: cg.MockObj, value: str) -> str:
"""Set a ZCL string value and return the target name (arrays decay to pointers)."""
cg.add(
cg.RawExpression(
f"ZB_ZCL_SET_STRING_VAL({target}, {cg.safe_exp(value)}, ZB_ZCL_STRING_CONST_SIZE({cg.safe_exp(value)}))"
)
)
return str(target)
def zigbee_new_attr_list(name: str, macro: str, *args: str) -> str:
"""Create an attribute list using a ZBOSS macro and return the name."""
obj = cg.RawExpression(f"{macro}({name}, {', '.join(args)})")
CORE.add_global(obj)
return name
class ZigbeeClusterDesc:
"""Represents a Zigbee cluster descriptor for code generation."""
def __init__(self, cluster_id: str, attr_list_name: str | None = None) -> None:
self._cluster_id = cluster_id
self._attr_list_name = attr_list_name
@property
def cluster_id(self) -> str:
return self._cluster_id
@property
def has_attrs(self) -> bool:
return self._attr_list_name is not None
def __str__(self) -> str:
role = (
"ZB_ZCL_CLUSTER_SERVER_ROLE"
if self._attr_list_name
else "ZB_ZCL_CLUSTER_CLIENT_ROLE"
)
if self._attr_list_name:
attr_count = f"ZB_ZCL_ARRAY_SIZE({self._attr_list_name}, zb_zcl_attr_t)"
return f"ZB_ZCL_CLUSTER_DESC({self._cluster_id}, {attr_count}, {self._attr_list_name}, {role}, ZB_ZCL_MANUF_CODE_INVALID)"
return f"ZB_ZCL_CLUSTER_DESC({self._cluster_id}, 0, NULL, {role}, ZB_ZCL_MANUF_CODE_INVALID)"
def zigbee_new_cluster_list(
name: str, clusters: list[ZigbeeClusterDesc]
) -> tuple[str, list[ZigbeeClusterDesc]]:
"""Create a cluster list array and return its name and the clusters."""
# Always include basic and identify clusters first
all_clusters = [
ZigbeeClusterDesc(ZB_ZCL_CLUSTER_ID_BASIC, "zigbee_basic_attrib_list"),
ZigbeeClusterDesc(ZB_ZCL_CLUSTER_ID_IDENTIFY, "zigbee_identify_attrib_list"),
]
all_clusters.extend(clusters)
cluster_strs = [str(c) for c in all_clusters]
CORE.add_global(
cg.RawExpression(
f"zb_zcl_cluster_desc_t {name}[] = {{{', '.join(cluster_strs)}}}"
)
)
return (name, all_clusters)
def zigbee_register_ep(
ep_name: str,
cluster_list_name: str,
report_attr_count: int,
clusters: list[ZigbeeClusterDesc],
slot_index: int,
) -> None:
"""Register a Zigbee endpoint."""
in_cluster_num = sum(1 for c in clusters if c.has_attrs)
out_cluster_num = len(clusters) - in_cluster_num
cluster_ids = [c.cluster_id for c in clusters]
# Store endpoint name for device context generation
CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER][slot_index] = ep_name
# Generate the endpoint declaration
ep_id = slot_index + 1 # Endpoints are 1-indexed
obj = cg.RawExpression(
f"ESPHOME_ZB_HA_DECLARE_EP({ep_name}, {ep_id}, {cluster_list_name}, "
f"{in_cluster_num}, {out_cluster_num}, {report_attr_count}, {', '.join(cluster_ids)})"
)
CORE.add_global(obj)
@coroutine_with_priority(CoroPriority.LATE)
async def _ctx_to_code(config: ConfigType) -> None:
cg.add_define("ZIGBEE_ENDPOINTS_COUNT", len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]))
cg.add_global(
cg.RawExpression(
f"ZBOSS_DECLARE_DEVICE_CTX_EP_VA(zb_device_ctx, &{', &'.join(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])})"
)
)
cg.add(cg.RawExpression("ZB_AF_REGISTER_DEVICE_CTX(&zb_device_ctx)"))
async def zephyr_setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
CORE.add_job(_add_binary_sensor, entity, config)
async def _add_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
# Find the next available endpoint slot
slot_index = next(
(i for i, v in enumerate(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if v == ""), None
)
# Create unique names for this sensor's variables based on slot index
prefix = f"zigbee_ep{slot_index + 1}"
attrs_name = f"{prefix}_binary_attrs"
attr_list_name = f"{prefix}_binary_input_attrib_list"
cluster_list_name = f"{prefix}_cluster_list"
ep_name = f"{prefix}_ep"
# Create the binary attributes structure
binary_attrs = zigbee_new_variable(attrs_name, BinaryAttrs)
attr_list = zigbee_new_attr_list(
attr_list_name,
"ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST",
zigbee_assign(binary_attrs.out_of_service, 0),
zigbee_assign(binary_attrs.present_value, 0),
zigbee_assign(binary_attrs.status_flags, 0),
zigbee_set_string(binary_attrs.description, config[CONF_NAME]),
)
# Create cluster list and register endpoint
cluster_list_name, clusters = zigbee_new_cluster_list(
cluster_list_name,
[ZigbeeClusterDesc(ZB_ZCL_CLUSTER_ID_BINARY_INPUT, attr_list)],
)
zigbee_register_ep(ep_name, cluster_list_name, 2, clusters, slot_index)
# Create the ZigbeeBinarySensor component
var = cg.new_Pvariable(config[CONF_ZIGBEE_BINARY_SENSOR], entity)
await cg.register_component(var, config)
cg.add(var.set_end_point(slot_index + 1))
cg.add(var.set_cluster_attributes(binary_attrs))
hub = await cg.get_variable(config[CONF_ZIGBEE_ID])
cg.add(var.set_parent(hub))

View File

@@ -299,6 +299,9 @@
#define USE_NRF52_UICR_ERASE
#define USE_SOFTDEVICE_ID 7
#define USE_SOFTDEVICE_VERSION 1
#define USE_ZIGBEE
#define USE_ZIGBEE_WIPE_ON_BOOT
#define ZIGBEE_ENDPOINTS_COUNT 8
#endif
// Disabled feature flags

View File

@@ -17,6 +17,7 @@ def load_idedata(environment, temp_folder, platformio_ini):
"""
#include <zephyr/kernel.h>
int main() { return 0;}
extern "C" void zboss_signal_handler() {};
""",
encoding="utf-8",
)
@@ -27,6 +28,12 @@ int main() { return 0;}
CONFIG_NEWLIB_LIBC=y
CONFIG_BT=y
CONFIG_ADC=y
#zigbee begin
CONFIG_ZIGBEE=y
CONFIG_CRYPTO=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
#zigbee end
""",
encoding="utf-8",
)
@@ -44,10 +51,11 @@ CONFIG_ADC=y
def extract_defines(command):
define_pattern = re.compile(r"-D\s*([^\s]+)")
ignore_prefixes = ("_ASMLANGUAGE", "NRF_802154_ECB_PRIORITY=")
return [
match
match.replace("\\", "")
for match in define_pattern.findall(command)
if match not in ("_ASMLANGUAGE")
if not any(match.startswith(prefix) for prefix in ignore_prefixes)
]
def find_cxx_path(commands):

View File

@@ -0,0 +1,34 @@
---
binary_sensor:
- platform: template
name: "Garage Door Open 1"
- platform: template
name: "Garage Door Open 2"
- platform: template
name: "Garage Door Open 3"
- platform: template
name: "Garage Door Open 4"
- platform: template
name: "Garage Door Open 5"
- platform: template
name: "Garage Door Open 6"
- platform: template
name: "Garage Door Open 7"
internal: True
- platform: template
name: "Garage Door Open 8"
- platform: template
name: "Garage Door Open 9"
zigbee:
wipe_on_boot: true
on_join:
then:
- logger.log: "Joined network"
output:
- platform: template
id: output_factory
type: binary
write_action:
- zigbee.factory_reset

View File

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

View File

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

View File

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