Files
esphome-lsm6ds/esphome/components/zigbee/zigbee_esp32.py
T
f757cd1210 [zigbee][core] Add support for Zigbee binary sensors on ESP32 H2 and C6 (#11553)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 12:46:56 -04:00

275 lines
9.2 KiB
Python

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