[nrf52, zigbee] Add sensor (#12187)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
tomaszduda23
2026-01-07 19:20:01 +01:00
committed by GitHub
parent d6554702d8
commit ada4e6d5e9
12 changed files with 471 additions and 54 deletions

View File

@@ -3,7 +3,7 @@ import math
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
from esphome.components import mqtt, web_server, zigbee
import esphome.config_validation as cv
from esphome.const import (
CONF_ABOVE,
@@ -295,6 +295,7 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(zigbee.SENSOR_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent),
@@ -335,6 +336,7 @@ _SENSOR_SCHEMA = (
)
_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("sensor"))
_SENSOR_SCHEMA.add_extra(zigbee.validate_sensor)
def sensor_schema(
@@ -918,6 +920,8 @@ async def setup_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_sensor(var, config)
async def register_sensor(var, config):
if not CORE.has_id(config[CONF_ID]):

View File

@@ -13,14 +13,16 @@ from esphome.types import ConfigType
from .const_zephyr import (
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_zephyr import zephyr_binary_sensor
from .zigbee_zephyr import zephyr_binary_sensor, zephyr_sensor
CODEOWNERS = ["@tomaszduda23"]
@@ -35,6 +37,7 @@ def zigbee_set_core_data(config: ConfigType) -> ConfigType:
BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor)
SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor)
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -42,9 +45,15 @@ CONFIG_SCHEMA = cv.All(
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.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
),
}
).extend(cv.COMPONENT_SCHEMA),
zigbee_set_core_data,
@@ -86,7 +95,16 @@ async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
await zephyr_setup_binary_sensor(entity, config)
def validate_binary_sensor(config: ConfigType) -> ConfigType:
async def setup_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_sensor
await zephyr_setup_sensor(entity, config)
def consume_endpoint(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, {})
@@ -95,6 +113,14 @@ def validate_binary_sensor(config: ConfigType) -> ConfigType:
return config
def validate_binary_sensor(config: ConfigType) -> ConfigType:
return consume_endpoint(config)
def validate_sensor(config: ConfigType) -> ConfigType:
return consume_endpoint(config)
ZIGBEE_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema(
{

View File

@@ -3,12 +3,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")
AnalogAttrs = zigbee_ns.struct("AnalogAttrs")
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_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",
}
# Keys for CORE.data storage
KEY_ZIGBEE = "zigbee"
@@ -22,3 +34,4 @@ ZB_ZCL_IDENTIFY_ATTRS_T = "zb_zcl_identify_attrs_t"
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"
ZB_ZCL_CLUSTER_ID_ANALOG_INPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_INPUT"

View File

@@ -17,9 +17,9 @@ ZigbeeBinarySensor::ZigbeeBinarySensor(binary_sensor::BinarySensor *binary_senso
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_,
ESP_LOGD(TAG, "Set attribute endpoint: %d, present_value %d", this->endpoint_,
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_SET_ATTRIBUTE(this->endpoint_, 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();
@@ -29,8 +29,8 @@ void ZigbeeBinarySensor::setup() {
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);
" Endpoint: %d, present_value %u",
this->endpoint_, this->cluster_attributes_->present_value);
}
} // namespace esphome::zigbee

View File

@@ -0,0 +1,76 @@
#include "zigbee_sensor_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_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.sensor";
ZigbeeSensor::ZigbeeSensor(sensor::Sensor *sensor) : sensor_(sensor) {}
void ZigbeeSensor::setup() {
this->sensor_->add_on_state_callback([this](float state) {
this->cluster_attributes_->present_value = state;
ESP_LOGD(TAG, "Set attribute endpoint: %d, present_value %f", this->endpoint_, state);
ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID,
(zb_uint8_t *) &this->cluster_attributes_->present_value, ZB_FALSE);
this->parent_->flush();
});
}
void ZigbeeSensor::dump_config() {
ESP_LOGCONFIG(TAG,
"Zigbee Sensor\n"
" Endpoint: %d, present_value %f",
this->endpoint_, this->cluster_attributes_->present_value);
}
const zb_uint8_t ZB_ZCL_ANALOG_INPUT_STATUS_FLAG_MAX_VALUE = 0x0F;
static zb_ret_t check_value_analog_server(zb_uint16_t attr_id, zb_uint8_t endpoint,
zb_uint8_t *value) { // NOLINT(readability-non-const-parameter)
zb_ret_t ret = RET_OK;
ZVUNUSED(endpoint);
switch (attr_id) {
case ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID:
ret = ZB_ZCL_CHECK_BOOL_VALUE(*value) ? RET_OK : RET_ERROR;
break;
case ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID:
break;
case ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID:
if (*value > ZB_ZCL_ANALOG_INPUT_STATUS_FLAG_MAX_VALUE) {
ret = RET_ERROR;
}
break;
default:
break;
}
return ret;
}
} // namespace esphome::zigbee
void zb_zcl_analog_input_init_server() {
zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
esphome::zigbee::check_value_analog_server, (zb_zcl_cluster_write_attr_hook_t) NULL,
(zb_zcl_cluster_handler_t) NULL);
}
void zb_zcl_analog_input_init_client() {
zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_CLIENT_ROLE,
(zb_zcl_cluster_check_value_t) NULL, (zb_zcl_cluster_write_attr_hook_t) NULL,
(zb_zcl_cluster_handler_t) NULL);
}
#endif

View File

@@ -0,0 +1,86 @@
#pragma once
#include "esphome/components/zigbee/zigbee_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_SENSOR)
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
}
enum {
ZB_ZCL_ATTR_ANALOG_INPUT_DESCRIPTION_ID = 0x001C,
ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID = 0x0051,
ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID = 0x0055,
ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID = 0x006F,
ZB_ZCL_ATTR_ANALOG_INPUT_ENGINEERING_UNITS_ID = 0x0075,
};
#define ZB_ZCL_ANALOG_INPUT_CLUSTER_REVISION_DEFAULT ((zb_uint16_t) 0x0001u)
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_DESCRIPTION_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_INPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \
(ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID, ZB_ZCL_ATTR_TYPE_BOOL, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL | ZB_ZCL_ATTR_ACCESS_REPORTING, \
(ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID, ZB_ZCL_ATTR_TYPE_8BITMAP, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_ENGINEERING_UNITS_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_INPUT_ENGINEERING_UNITS_ID, ZB_ZCL_ATTR_TYPE_16BIT_ENUM, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \
(ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \
}
#define ESPHOME_ZB_ZCL_DECLARE_ANALOG_INPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \
engineering_units, description) \
ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_ANALOG_INPUT) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID, (out_of_service)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID, (present_value)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID, (status_flag)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_ENGINEERING_UNITS_ID, (engineering_units)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_DESCRIPTION_ID, (description)) \
ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST
void zb_zcl_analog_input_init_server();
void zb_zcl_analog_input_init_client();
#define ZB_ZCL_CLUSTER_ID_ANALOG_INPUT_SERVER_ROLE_INIT zb_zcl_analog_input_init_server
#define ZB_ZCL_CLUSTER_ID_ANALOG_INPUT_CLIENT_ROLE_INIT zb_zcl_analog_input_init_client
namespace esphome::zigbee {
class ZigbeeSensor : public ZigbeeEntity, public Component {
public:
explicit ZigbeeSensor(sensor::Sensor *sensor);
void set_cluster_attributes(AnalogAttrs &cluster_attributes) { this->cluster_attributes_ = &cluster_attributes; }
void setup() override;
void dump_config() override;
protected:
AnalogAttrs *cluster_attributes_{nullptr};
sensor::Sensor *sensor_{nullptr};
};
} // namespace esphome::zigbee
#endif

View File

@@ -138,9 +138,26 @@ void ZigbeeComponent::setup() {
}
#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));
bool wipe = true;
#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC
// unique hash to store preferences for this component
uint32_t hash = 88498616UL;
uint32_t wipe_value = 0;
auto wipe_pref = global_preferences->make_preference<uint32_t>(hash, true);
if (wipe_pref.load(&wipe_value)) {
wipe = wipe_value != USE_ZIGBEE_WIPE_ON_BOOT_MAGIC;
ESP_LOGD(TAG, "Wipe value in preferences %u, in firmware %u", wipe_value, USE_ZIGBEE_WIPE_ON_BOOT_MAGIC);
}
#endif
if (wipe) {
erase_flash_(FIXED_PARTITION_ID(ZBOSS_NVRAM));
erase_flash_(FIXED_PARTITION_ID(ZBOSS_PRODUCT_CONFIG));
erase_flash_(FIXED_PARTITION_ID(SETTINGS_STORAGE));
#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC
wipe_value = USE_ZIGBEE_WIPE_ON_BOOT_MAGIC;
wipe_pref.save(&wipe_value);
#endif
}
#endif
ZB_ZCL_REGISTER_DEVICE_CB(zcl_device_cb);
@@ -152,15 +169,54 @@ void ZigbeeComponent::setup() {
zigbee_enable();
}
void ZigbeeComponent::dump_config() {
bool wipe = false;
static const char *role() {
switch (zb_get_network_role()) {
case ZB_NWK_DEVICE_TYPE_COORDINATOR:
return "coordinator";
case ZB_NWK_DEVICE_TYPE_ROUTER:
return "router";
case ZB_NWK_DEVICE_TYPE_ED:
return "end device";
}
return "unknown";
}
static const char *get_wipe_on_boot() {
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
wipe = true;
#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC
return "ONCE";
#else
return "YES";
#endif
#else
return "NO";
#endif
}
void ZigbeeComponent::dump_config() {
char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0};
zb_ieee_addr_t addr;
zb_get_long_address(addr);
ieee_addr_to_str(ieee_addr_buf, sizeof(ieee_addr_buf), addr);
zb_ext_pan_id_t extended_pan_id;
char extended_pan_id_buf[IEEE_ADDR_BUF_SIZE] = {0};
zb_get_extended_pan_id(extended_pan_id);
ieee_addr_to_str(extended_pan_id_buf, sizeof(extended_pan_id_buf), extended_pan_id);
ESP_LOGCONFIG(TAG,
"Zigbee\n"
" Wipe on boot: %s",
YESNO(wipe));
" Wipe on boot: %s\n"
" Device is joined to the network: %s\n"
" Current channel: %d\n"
" Current page: %d\n"
" Sleep threshold: %ums\n"
" Role: %s\n"
" Long addr: 0x%s\n"
" Short addr: 0x%04X\n"
" Long pan id: 0x%s\n"
" Short pan id: 0x%04X",
get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(),
zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf,
zb_get_pan_id());
}
static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) {

View File

@@ -28,16 +28,15 @@ extern "C" {
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, ...) \
#define ESPHOME_ZB_ZCL_DECLARE_SIMPLE_DESC(ep_name, ep_id, in_clust_num, out_clust_num, app_device_id, ...) \
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__}}
simple_desc_##ep_name = {ep_id, ZB_AF_HA_PROFILE_ID, app_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__); \
app_device_id, ...) \
ESPHOME_ZB_ZCL_DECLARE_SIMPLE_DESC(ep_name, ep_id, in_cluster_num, out_cluster_num, app_device_id, __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, \
@@ -57,10 +56,8 @@ struct AnalogAttrs {
zb_bool_t out_of_service;
float present_value;
zb_uint8_t status_flags;
zb_uint16_t engineering_units;
zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE];
float max_present_value;
float min_present_value;
float resolution;
};
class ZigbeeComponent : public Component {
@@ -93,10 +90,10 @@ class ZigbeeComponent : public Component {
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; }
void set_endpoint(zb_uint8_t endpoint) { this->endpoint_ = endpoint; }
protected:
zb_uint8_t end_point_{0};
zb_uint8_t endpoint_{0};
ZigbeeComponent *parent_{nullptr};
};

View File

@@ -1,10 +1,45 @@
from datetime import datetime
import random
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.const import (
CONF_ID,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
UNIT_AMPERE,
UNIT_CELSIUS,
UNIT_CENTIMETER,
UNIT_DECIBEL,
UNIT_HECTOPASCAL,
UNIT_HERTZ,
UNIT_HOUR,
UNIT_KELVIN,
UNIT_KILOMETER,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_LUX,
UNIT_METER,
UNIT_MICROGRAMS_PER_CUBIC_METER,
UNIT_MILLIAMP,
UNIT_MILLIGRAMS_PER_CUBIC_METER,
UNIT_MILLIMETER,
UNIT_MILLISECOND,
UNIT_MILLIVOLT,
UNIT_MINUTE,
UNIT_OHM,
UNIT_PARTS_PER_BILLION,
UNIT_PARTS_PER_MILLION,
UNIT_PASCAL,
UNIT_PERCENT,
UNIT_SECOND,
UNIT_VOLT,
UNIT_WATT,
UNIT_WATT_HOURS,
__version__,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import (
AssignmentExpression,
@@ -15,22 +50,63 @@ from esphome.types import ConfigType
from .const_zephyr import (
CONF_ON_JOIN,
CONF_POWER_SOURCE,
CONF_WIPE_ON_BOOT,
CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID,
CONF_ZIGBEE_SENSOR,
KEY_EP_NUMBER,
KEY_ZIGBEE,
POWER_SOURCE,
ZB_ZCL_BASIC_ATTRS_EXT_T,
ZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
ZB_ZCL_CLUSTER_ID_BASIC,
ZB_ZCL_CLUSTER_ID_BINARY_INPUT,
ZB_ZCL_CLUSTER_ID_IDENTIFY,
ZB_ZCL_IDENTIFY_ATTRS_T,
AnalogAttrs,
BinaryAttrs,
ZigbeeComponent,
zigbee_ns,
)
ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component)
ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component)
# BACnet engineering units mapping (ZCL uses BACnet unit codes)
# See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py
BACNET_UNITS = {
UNIT_CELSIUS: 62,
UNIT_KELVIN: 63,
UNIT_VOLT: 5,
UNIT_MILLIVOLT: 124,
UNIT_AMPERE: 3,
UNIT_MILLIAMP: 2,
UNIT_OHM: 4,
UNIT_WATT: 47,
UNIT_KILOWATT: 48,
UNIT_WATT_HOURS: 18,
UNIT_KILOWATT_HOURS: 19,
UNIT_PASCAL: 53,
UNIT_HECTOPASCAL: 133,
UNIT_HERTZ: 27,
UNIT_MILLIMETER: 30,
UNIT_CENTIMETER: 118,
UNIT_METER: 31,
UNIT_KILOMETER: 193,
UNIT_MILLISECOND: 159,
UNIT_SECOND: 73,
UNIT_MINUTE: 72,
UNIT_HOUR: 71,
UNIT_PARTS_PER_MILLION: 96,
UNIT_PARTS_PER_BILLION: 97,
UNIT_MICROGRAMS_PER_CUBIC_METER: 219,
UNIT_MILLIGRAMS_PER_CUBIC_METER: 218,
UNIT_LUX: 37,
UNIT_DECIBEL: 199,
UNIT_PERCENT: 98,
}
BACNET_UNIT_NO_UNITS = 95
zephyr_binary_sensor = cv.Schema(
{
@@ -41,6 +117,15 @@ zephyr_binary_sensor = cv.Schema(
}
)
zephyr_sensor = cv.Schema(
{
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent),
cv.OnlyWith(CONF_ZIGBEE_SENSOR, ["nrf52", "zigbee"]): cv.declare_id(
ZigbeeSensor
),
}
)
async def zephyr_to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("ZIGBEE", True)
@@ -56,6 +141,10 @@ async def zephyr_to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("NET_UDP", False)
if config[CONF_WIPE_ON_BOOT]:
if config[CONF_WIPE_ON_BOOT] == "once":
cg.add_define(
"USE_ZIGBEE_WIPE_ON_BOOT_MAGIC", random.randint(0x000001, 0xFFFFFF)
)
cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT")
var = cg.new_Pvariable(config[CONF_ID])
@@ -85,7 +174,7 @@ async def _attr_to_code(config: ConfigType) -> None:
),
zigbee_assign(
basic_attrs.power_source,
cg.RawExpression("ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE"),
cg.RawExpression(POWER_SOURCE[config[CONF_POWER_SOURCE]]),
),
zigbee_set_string(basic_attrs.location_id, ""),
zigbee_assign(
@@ -191,6 +280,7 @@ def zigbee_register_ep(
report_attr_count: int,
clusters: list[ZigbeeClusterDesc],
slot_index: int,
app_device_id: str,
) -> None:
"""Register a Zigbee endpoint."""
in_cluster_num = sum(1 for c in clusters if c.has_attrs)
@@ -204,7 +294,7 @@ def zigbee_register_ep(
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)})"
f"{in_cluster_num}, {out_cluster_num}, {report_attr_count}, {app_device_id}, {', '.join(cluster_ids)})"
)
CORE.add_global(obj)
@@ -224,42 +314,102 @@ async def zephyr_setup_binary_sensor(entity: cg.MockObj, config: ConfigType) ->
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(
async def zephyr_setup_sensor(entity: cg.MockObj, config: ConfigType) -> None:
CORE.add_job(_add_sensor, entity, config)
def _slot_index() -> int:
"""Find the next available endpoint slot"""
slot = next(
(i for i, v in enumerate(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if v == ""), None
)
if slot is None:
raise cv.Invalid(
f"Not found empty slot, size ({len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])})"
)
return slot
async def _add_zigbee_input(
entity: cg.MockObj,
config: ConfigType,
component_key,
attrs_type,
zcl_macro: str,
cluster_id: str,
app_device_id: str,
extra_field_values: dict[str, int] | None = None,
) -> None:
slot_index = _slot_index()
# 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"
attrs_name = f"{prefix}_attrs"
attr_list_name = f"{prefix}_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 attribute struct
attrs = zigbee_new_variable(attrs_name, attrs_type)
# Build attribute list args
attr_args = [
zigbee_assign(attrs.out_of_service, 0),
zigbee_assign(attrs.present_value, 0),
zigbee_assign(attrs.status_flags, 0),
]
# Add extra field assignments (e.g., engineering_units for sensors)
if extra_field_values:
for field_name, value in extra_field_values.items():
attr_args.append(zigbee_assign(getattr(attrs, field_name), value))
attr_args.append(zigbee_set_string(attrs.description, config[CONF_NAME]))
# Create attribute list
attr_list = zigbee_new_attr_list(attr_list_name, zcl_macro, *attr_args)
# 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)],
[ZigbeeClusterDesc(cluster_id, attr_list)],
)
zigbee_register_ep(
ep_name, cluster_list_name, 2, clusters, slot_index, app_device_id
)
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)
# Create ESPHome component
var = cg.new_Pvariable(config[component_key], entity)
await cg.register_component(var, {})
cg.add(var.set_endpoint(slot_index + 1))
cg.add(var.set_cluster_attributes(attrs))
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))
async def _add_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
await _add_zigbee_input(
entity,
config,
CONF_ZIGBEE_BINARY_SENSOR,
BinaryAttrs,
"ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST",
ZB_ZCL_CLUSTER_ID_BINARY_INPUT,
"ZB_HA_SIMPLE_SENSOR_DEVICE_ID",
)
async def _add_sensor(entity: cg.MockObj, config: ConfigType) -> None:
# Get BACnet engineering unit from unit_of_measurement
unit = config.get(CONF_UNIT_OF_MEASUREMENT, "")
bacnet_unit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS)
await _add_zigbee_input(
entity,
config,
CONF_ZIGBEE_SENSOR,
AnalogAttrs,
"ESPHOME_ZB_ZCL_DECLARE_ANALOG_INPUT_ATTRIB_LIST",
ZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
"ZB_HA_CUSTOM_ATTR_DEVICE_ID",
extra_field_values={"engineering_units": bacnet_unit},
)

View File

@@ -305,6 +305,7 @@
#define USE_SOFTDEVICE_VERSION 1
#define USE_ZIGBEE
#define USE_ZIGBEE_WIPE_ON_BOOT
#define USE_ZIGBEE_WIPE_ON_BOOT_MAGIC 1
#define ZIGBEE_ENDPOINTS_COUNT 8
#endif

View File

@@ -15,10 +15,14 @@ binary_sensor:
- platform: template
name: "Garage Door Open 7"
internal: True
sensor:
- platform: template
name: "Garage Door Open 8"
name: "Analog 1"
lambda: return 10.0;
- platform: template
name: "Garage Door Open 9"
name: "Analog 2"
lambda: return 11.0;
zigbee:
wipe_on_boot: true

View File

@@ -1 +1,5 @@
<<: !include common.yaml
zigbee:
wipe_on_boot: once
power_source: battery