diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 83b2656661..2ac45a55ac 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -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]): diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 2009f92d2e..1a017f2ab2 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -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( { diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py index ecd08f1f0a..8d1f229b6e 100644 --- a/esphome/components/zigbee/const_zephyr.py +++ b/esphome/components/zigbee/const_zephyr.py @@ -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" diff --git a/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp index 744d04adc5..8b7aff70a8 100644 --- a/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp @@ -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 diff --git a/esphome/components/zigbee/zigbee_sensor_zephyr.cpp b/esphome/components/zigbee/zigbee_sensor_zephyr.cpp new file mode 100644 index 0000000000..74550d6487 --- /dev/null +++ b/esphome/components/zigbee/zigbee_sensor_zephyr.cpp @@ -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 +#include +#include +#include +#include +} +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 diff --git a/esphome/components/zigbee/zigbee_sensor_zephyr.h b/esphome/components/zigbee/zigbee_sensor_zephyr.h new file mode 100644 index 0000000000..37406f21d0 --- /dev/null +++ b/esphome/components/zigbee/zigbee_sensor_zephyr.h @@ -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 +#include +} + +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 diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index c9027d0a74..9a421aaec1 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -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(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) { diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index 853c6deb4d..fa23907bf4 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -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}; }; diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index ce55675c41..d8a2716603 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -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}, + ) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 69684fd5c9..f8f86e8c55 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -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 diff --git a/tests/components/zigbee/common.yaml b/tests/components/zigbee/common.yaml index eb30205446..c91569bdbe 100644 --- a/tests/components/zigbee/common.yaml +++ b/tests/components/zigbee/common.yaml @@ -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 diff --git a/tests/components/zigbee/test.nrf52-xiao-ble.yaml b/tests/components/zigbee/test.nrf52-xiao-ble.yaml index dade44d145..d2ce552de3 100644 --- a/tests/components/zigbee/test.nrf52-xiao-ble.yaml +++ b/tests/components/zigbee/test.nrf52-xiao-ble.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +zigbee: + wipe_on_boot: once + power_source: battery