[zigbee] Improve basic cluster and add manufacturer and app version setting (#20519)

This commit is contained in:
luar123
2026-10-11 11:19:43 -10:00
committed by GitHub
parent 9b34db14d4
commit 3035a08b0a
9 changed files with 140 additions and 40 deletions
+62 -2
View File
@@ -3,6 +3,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_MANUFACTURER
from esphome.components.esp32 import DOMAIN as ESP32_DOMAIN, only_on_variant
from esphome.components.esp32.const import (
VARIANT_ESP32C5,
@@ -14,14 +15,19 @@ from esphome.components.esp32.const import (
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ESPHOME,
CONF_ID,
CONF_INTERNAL,
CONF_MODEL,
CONF_NAME,
CONF_ON_START,
CONF_POLLING_INTERVAL,
CONF_PROJECT,
CONF_VERSION,
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.types import ConfigType
from .const import (
@@ -36,6 +42,8 @@ from .const import (
CONF_USE_DEVICE_TYPE,
CONF_WIPE_ON_BOOT,
KEY_ZIGBEE,
KEY_ZIGBEE_AREA_STR,
KEY_ZIGBEE_VERSION_STR,
POWER_SOURCE,
REPORT,
ZigbeeComponent,
@@ -143,12 +151,33 @@ def _validate_router_sleepy(config: ConfigType) -> ConfigType:
return config
def _validate_ascii(value):
"""Validate that a string contains only ascii characters."""
if not isinstance(value, str):
raise cv.Invalid("expected string")
if not value.isascii():
raise cv.Invalid("Only ASCII characters are allowed")
return value
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(ZigbeeComponent),
cv.Optional(CONF_MODEL, default=CORE.name): cv.All(
cv.string, cv.Length(max=31)
cv.string,
cv.Length(max=31),
_validate_ascii,
),
cv.Optional(
CONF_MANUFACTURER, default="esphome", visibility=cv.Visibility.YAML_ONLY
): cv.All(
cv.string,
cv.Length(max=31),
_validate_ascii,
),
cv.Optional(CONF_VERSION, default=0): cv.All(
cv.int_range(min=0x00, max=0xFF),
),
cv.Optional(CONF_ROUTER, default=False): cv.boolean,
cv.Optional(CONF_ON_JOIN): automation.validate_automation({}),
@@ -207,7 +236,7 @@ CONFIG_SCHEMA = cv.All(
def validate_number_of_ep(config: ConfigType) -> ConfigType:
if not CORE.is_nrf52:
return config
if KEY_ZIGBEE not in CORE.data:
if KEY_EP_NUMBER not in CORE.data.get(KEY_ZIGBEE, {}):
raise cv.Invalid("At least one zigbee device need to be included")
count = len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])
if count == 1:
@@ -220,7 +249,38 @@ def validate_number_of_ep(config: ConfigType) -> ConfigType:
return config
def final_validate_strings(config: ConfigType) -> ConfigType:
version_str = ESPHOME_VERSION
if (
proj_version := fv.full_config.get()
.get(CONF_ESPHOME, {})
.get(CONF_PROJECT, {})
.get(CONF_VERSION)
):
version_str = proj_version.encode("ascii", "ignore")[:15].decode()
if version_str != proj_version:
_LOGGER.warning(
"esphome.project.version %s was changed to %s for use in Zigbee attributes. Use only a maximum of 15 ASCII characters",
proj_version,
version_str,
)
area_str = ""
if CORE.area:
area_str = CORE.area.encode("ascii", "ignore")[:15].decode()
if area_str != CORE.area:
_LOGGER.warning(
"esphome.area %s was changed to %s for use in Zigbee attributes. Use only a maximum of 15 ASCII characters",
CORE.area,
area_str,
)
data: dict[str, Any] = CORE.data.setdefault(KEY_ZIGBEE, {})
data[KEY_ZIGBEE_VERSION_STR] = version_str
data[KEY_ZIGBEE_AREA_STR] = area_str
return config
FINAL_VALIDATE_SCHEMA = cv.All(
final_validate_strings,
validate_number_of_ep,
final_validate_esp32,
)
+2
View File
@@ -85,6 +85,8 @@ POWER_SOURCE = {
}
KEY_ZIGBEE = "zigbee"
KEY_ZIGBEE_AREA_STR = "zigbee_area_str"
KEY_ZIGBEE_VERSION_STR = "zigbee_version_str"
# BACnet engineering units mapping (ZCL uses BACnet unit codes)
# See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py
+42 -30
View File
@@ -238,7 +238,8 @@ void ZigbeeComponent::add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint
ESP_LOGD(TAG, "Endpoint %u: Added cluster 0x%04X with role %u", endpoint_id, cluster_id, role);
}
void ZigbeeComponent::set_basic_cluster(const char *model, const char *manufacturer, uint8_t power_source) {
void ZigbeeComponent::set_basic_cluster(const char *model, const char *manufacturer, const char *location,
const char *sw_build_id, uint8_t power_source, uint8_t app_version) {
char date_buf[16];
time_t time_val = App.get_build_time();
struct tm *timeinfo = localtime(&time_val);
@@ -247,7 +248,10 @@ void ZigbeeComponent::set_basic_cluster(const char *model, const char *manufactu
.model = get_zcl_string(model, 31),
.manufacturer = get_zcl_string(manufacturer, 31),
.date = get_zcl_string(date_buf, 15),
.location = get_zcl_string(location, 15),
.sw_build_id = get_zcl_string(sw_build_id, 15),
.power_source = power_source,
.app_version = app_version,
};
}
@@ -267,6 +271,16 @@ void ZigbeeComponent::update_basic_cluster_(ezb_af_ep_desc_t ep_desc) {
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID,
this->basic_cluster_data_.model);
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_DATE_CODE_ID, this->basic_cluster_data_.date);
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_APPLICATION_VERSION_ID,
&(this->basic_cluster_data_.app_version));
uint8_t stack_version = 0;
uint8_t hw_version = 0;
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_STACK_VERSION_ID, &stack_version);
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_HW_VERSION_ID, &hw_version);
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_LOCATION_DESCRIPTION_ID,
this->basic_cluster_data_.location);
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_SW_BUILD_ID_ID,
this->basic_cluster_data_.sw_build_id);
}
bool ZigbeeComponent::register_device() {
@@ -420,37 +434,35 @@ void ZigbeeComponent::loop() {
}
void ZigbeeComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Zigbee\n"
" Model: %.*s\n"
" Manufacturer: %.*s\n"
#ifdef CONFIG_ZB_ZCZR
" Router: %s\n"
#else
" Sleepy end device: %s\n"
" Poll interval: %" PRIu32 " ms\n"
#endif
" App Version: %u",
this->basic_cluster_data_.model[0], reinterpret_cast<const char *>(this->basic_cluster_data_.model + 1),
this->basic_cluster_data_.manufacturer[0],
reinterpret_cast<const char *>(this->basic_cluster_data_.manufacturer + 1),
#ifdef CONFIG_ZB_ZCZR
YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER),
#else
YESNO(this->sleepy_), this->keep_alive_,
#endif
this->basic_cluster_data_.app_version);
if (esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
ESP_LOGCONFIG(
TAG,
"Zigbee\n"
" Model: %.*s\n"
#ifdef CONFIG_ZB_ZCZR
" Router: %s\n"
#else
" Sleepy end device: %s\n"
" Poll interval: %" PRIu32 " ms\n"
#endif
" 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[0], reinterpret_cast<const char *>(this->basic_cluster_data_.model + 1),
#ifdef CONFIG_ZB_ZCZR
YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER),
#else
YESNO(this->sleepy_), this->keep_alive_,
#endif
YESNO(ezb_bdb_dev_joined()), ezb_nwk_get_current_channel(), ezb_nwk_get_short_address(), ezb_nwk_get_panid());
esp_zigbee_lock_release();
} else {
ESP_LOGCONFIG(TAG,
"Zigbee\n"
" Model: %.*s\n"
" Router: %s\n",
this->basic_cluster_data_.model[0],
reinterpret_cast<const char *>(this->basic_cluster_data_.model + 1),
YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER));
" Device is joined to the network: %s\n"
" Current channel: %d\n"
" Short addr: 0x%04X\n"
" Short pan id: 0x%04X",
YESNO(ezb_bdb_dev_joined()), ezb_nwk_get_current_channel(), ezb_nwk_get_short_address(),
ezb_nwk_get_panid());
esp_zigbee_lock_release();
}
}
+5 -1
View File
@@ -44,7 +44,8 @@ class ZigbeeComponent final : public Component {
void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; }
void set_keep_alive(uint32_t keep_alive) { this->keep_alive_ = keep_alive; }
void set_basic_cluster(const char *model, const char *manufacturer, uint8_t power_source);
void set_basic_cluster(const char *model, const char *manufacturer, const char *location, const char *sw_build_id,
uint8_t power_source, uint8_t app_version);
void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role);
void create_default_cluster(uint8_t endpoint_id, uint16_t device_id);
bool register_device();
@@ -80,7 +81,10 @@ class ZigbeeComponent final : public Component {
uint8_t *model;
uint8_t *manufacturer;
uint8_t *date;
uint8_t *location;
uint8_t *sw_build_id;
uint8_t power_source;
uint8_t app_version;
} basic_cluster_data_;
#ifdef CONFIG_ZB_ZED
ezb_nwk_device_type_t device_role_ = EZB_NWK_DEVICE_TYPE_END_DEVICE;
+8 -2
View File
@@ -4,6 +4,7 @@ import re
from typing import Any
import esphome.codegen as cg
from esphome.components.const import CONF_MANUFACTURER
from esphome.components.esp32 import (
CONF_PARTITIONS,
add_idf_component,
@@ -28,6 +29,7 @@ from esphome.const import (
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE,
CONF_VERSION,
CONF_WIFI,
DEVICE_CLASS_OUTLET,
)
@@ -49,6 +51,8 @@ from .const import (
CONF_SLEEPY,
CONF_USE_DEVICE_TYPE,
KEY_ZIGBEE,
KEY_ZIGBEE_AREA_STR,
KEY_ZIGBEE_VERSION_STR,
POWER_SOURCE,
ZigbeeAttribute,
)
@@ -418,12 +422,14 @@ async def esp32_to_code(config: ConfigType) -> "MockObj":
cg.add(var.set_sleepy(True))
if (keep_alive := config.get(CONF_POLLING_INTERVAL, 3000)) != 3000:
cg.add(var.set_keep_alive(keep_alive))
cg.add(
var.set_basic_cluster(
config[CONF_MODEL],
"esphome",
config[CONF_MANUFACTURER],
zb_data[KEY_ZIGBEE_AREA_STR],
zb_data[KEY_ZIGBEE_VERSION_STR],
POWER_SOURCE[config[CONF_POWER_SOURCE]],
config[CONF_VERSION],
)
)
for ep_num, ep in ep_dict.items():
+9 -5
View File
@@ -2,6 +2,7 @@ import datetime
import random
import esphome.codegen as cg
from esphome.components.const import CONF_MANUFACTURER
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import (
@@ -9,7 +10,7 @@ from esphome.const import (
CONF_MODEL,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
__version__,
CONF_VERSION,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import (
@@ -27,6 +28,8 @@ from .const import (
CONF_SLEEPY,
CONF_WIPE_ON_BOOT,
KEY_ZIGBEE,
KEY_ZIGBEE_AREA_STR,
KEY_ZIGBEE_VERSION_STR,
POWER_SOURCE,
AnalogAttrs,
AnalogAttrsOutput,
@@ -158,15 +161,16 @@ async def zephyr_to_code(config: ConfigType) -> "MockObj":
async def _attr_to_code(config: ConfigType) -> None:
# Create the basic attributes structure and attribute list
zb_data = CORE.data.get(KEY_ZIGBEE, {})
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.app_version, config[CONF_VERSION]),
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.mf_name, config[CONF_MANUFACTURER]),
zigbee_set_string(basic_attrs.model_id, config[CONF_MODEL]),
zigbee_set_string(
basic_attrs.date_code,
@@ -178,11 +182,11 @@ async def _attr_to_code(config: ConfigType) -> None:
basic_attrs.power_source,
POWER_SOURCE[config[CONF_POWER_SOURCE]],
),
zigbee_set_string(basic_attrs.location_id, ""),
zigbee_set_string(basic_attrs.location_id, zb_data[KEY_ZIGBEE_AREA_STR]),
zigbee_assign(
basic_attrs.ph_env, cg.RawExpression("ZB_ZCL_BASIC_ENV_UNSPECIFIED")
),
zigbee_set_string(basic_attrs.sw_ver, __version__),
zigbee_set_string(basic_attrs.sw_ver, zb_data[KEY_ZIGBEE_VERSION_STR]),
)
# Create the identify attributes structure and attribute list
+8
View File
@@ -1,3 +1,11 @@
esphome:
area:
id: testing_area
name: Testing Area
project:
name: esphome.test
version: "1.1"
binary_sensor:
- platform: template
name: "Garage Door Open 1"
@@ -58,6 +58,8 @@ switch:
zigbee:
model: zigbee_test
manufacturer: test
version: 1
sleepy: true
polling_interval: 2s
power_source: BATTERY
@@ -3,6 +3,8 @@ packages:
zigbee:
model: zigbee_test
manufacturer: test
version: 1
wipe_on_boot: true
on_join:
then: