[zigbee] Add sensor support on esp32 (#16026)

This commit is contained in:
luar123
2026-05-11 10:51:47 -04:00
committed by GitHub
parent 8cf0eba043
commit 4ac7bc4606
10 changed files with 238 additions and 79 deletions
+6 -5
View File
@@ -36,6 +36,7 @@ from .const_zephyr import (
from .zigbee_esp32 import (
final_validate_esp32,
validate_binary_sensor_esp32,
validate_sensor_esp32,
zigbee_require_vfs_select,
)
from .zigbee_zephyr import (
@@ -49,8 +50,7 @@ _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@luar123", "@tomaszduda23"]
BINARY_SENSOR_SCHEMA = cv.Schema(
BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_REPORT): cv.All(
cv.requires_component("zigbee"),
@@ -58,8 +58,9 @@ BINARY_SENSOR_SCHEMA = cv.Schema(
cv.enum(REPORT, lower=True),
)
}
).extend(zephyr_binary_sensor)
SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor)
)
BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(BASE_SCHEMA).extend(zephyr_binary_sensor)
SENSOR_SCHEMA = cv.Schema({}).extend(BASE_SCHEMA).extend(zephyr_sensor)
SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch)
NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number)
@@ -227,7 +228,7 @@ def validate_sensor(config: ConfigType) -> ConfigType:
if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL):
return config
if CORE.is_esp32:
return config
return validate_sensor_esp32(config)
return consume_endpoint(config)
+113
View File
@@ -1,4 +1,47 @@
from enum import IntEnum
import esphome.codegen as cg
from esphome.const import (
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLUME_FLOW_RATE,
UNIT_AMPERE,
UNIT_CELSIUS,
UNIT_CENTIMETER,
UNIT_DECIBEL,
UNIT_HECTOPASCAL,
UNIT_HERTZ,
UNIT_HOUR,
UNIT_KELVIN,
UNIT_KILOMETER,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_LITRE_PER_SECOND,
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,
)
zigbee_ns = cg.esphome_ns.namespace("zigbee")
ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component)
@@ -30,3 +73,73 @@ POWER_SOURCE = {
}
KEY_ZIGBEE = "zigbee"
# 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
class AnalogInputType(IntEnum):
TEMP_DEGREES_C = 0x00
RELATIVE_HUMIDITY_PERCENT = 0x01
PRESSURE_PASCAL = 0x02
FLOW_LITERS_PER_SEC = 0x03
PERCENTAGE = 0x04
PARTS_PER_MILLION = 0x05
ROTATIONAL_SPEED_RPM = 0x06
CURRENT_AMPS = 0x07
FREQUENCY_HZ = 0x08
POWER_WATTS = 0x09
POWER_KILO_WATTS = 0x0A
ENERGY_KILO_WATT_HOURS = 0x0B
COUNT = 0x0C
ENTHALPY_KJOULES_PER_KG = 0x0D
TIME_SECONDS = 0x0E
ANALOG_INPUT_APPTYPE = {
(DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS): AnalogInputType.TEMP_DEGREES_C,
(DEVICE_CLASS_HUMIDITY, UNIT_PERCENT): AnalogInputType.RELATIVE_HUMIDITY_PERCENT,
(DEVICE_CLASS_PRESSURE, UNIT_PASCAL): AnalogInputType.PRESSURE_PASCAL,
(
DEVICE_CLASS_VOLUME_FLOW_RATE,
UNIT_LITRE_PER_SECOND,
): AnalogInputType.FLOW_LITERS_PER_SEC,
(DEVICE_CLASS_CURRENT, UNIT_AMPERE): AnalogInputType.CURRENT_AMPS,
(DEVICE_CLASS_FREQUENCY, UNIT_HERTZ): AnalogInputType.FREQUENCY_HZ,
(DEVICE_CLASS_POWER, UNIT_WATT): AnalogInputType.POWER_WATTS,
(DEVICE_CLASS_POWER, UNIT_KILOWATT): AnalogInputType.POWER_KILO_WATTS,
(DEVICE_CLASS_ENERGY, UNIT_KILOWATT_HOURS): AnalogInputType.ENERGY_KILO_WATT_HOURS,
(DEVICE_CLASS_DURATION, UNIT_SECOND): AnalogInputType.TIME_SECONDS,
}
+4
View File
@@ -11,6 +11,7 @@ CONF_CLUSTER = "cluster"
SCALE = "scale"
CONF_ATTRIBUTE_ID = "attribute_id"
KEY_BS_EP = "binary_sensor_ep"
KEY_SENSOR_EP = "sensor_ep"
ha_standard_devices = cg.esphome_ns.enum("zb_ha_standard_devs_e")
DEVICE_ID = {
@@ -22,6 +23,7 @@ cluster_id = cg.esphome_ns.enum("esp_zb_zcl_cluster_id_t")
CLUSTER_ID = {
"BASIC": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BASIC,
"BINARY_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT,
"ANALOG_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
}
cluster_role = cg.esphome_ns.enum("esp_zb_zcl_cluster_role_t")
CLUSTER_ROLE = {
@@ -32,4 +34,6 @@ ATTR_TYPE = {
"BOOL": attr_type.ESP_ZB_ZCL_ATTR_TYPE_BOOL,
"8BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_8BITMAP,
"CHAR_STRING": attr_type.ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING,
"SINGLE": attr_type.ESP_ZB_ZCL_ATTR_TYPE_SINGLE,
"DOUBLE": attr_type.ESP_ZB_ZCL_ATTR_TYPE_DOUBLE,
}
@@ -12,6 +12,9 @@
#include "esp_zigbee_core.h"
#include "zigbee_esp32.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
@@ -42,6 +45,9 @@ class ZigbeeAttribute : public Component {
template<typename T> void set_attr(const T &value);
uint8_t attr_type() { return attr_type_; }
void set_report(bool force);
#ifdef USE_SENSOR
template<typename T> void connect(sensor::Sensor *sensor);
#endif
#ifdef USE_BINARY_SENSOR
template<typename T> void connect(binary_sensor::BinarySensor *sensor);
#endif
@@ -78,6 +84,11 @@ template<typename T> void ZigbeeAttribute::set_attr(const T &value) {
this->enable_loop();
}
#ifdef USE_SENSOR
template<typename T> void ZigbeeAttribute::connect(sensor::Sensor *sensor) {
sensor->add_on_state_callback([this](float value) { this->set_attr((T) (this->scale_ * value)); });
}
#endif
#ifdef USE_BINARY_SENSOR
template<typename T> void ZigbeeAttribute::connect(binary_sensor::BinarySensor *sensor) {
sensor->add_on_state_callback([this](bool value) { this->set_attr((T) (this->scale_ * value)); });
@@ -46,6 +46,35 @@ ep_configs: dict[str, dict[str, Any]] = {
},
],
},
"analog_input": {
DEVICE_TYPE: "CUSTOM_ATTR",
CONF_CLUSTERS: [
{
CONF_ID: "ANALOG_INPUT",
ROLE: CLUSTER_ROLE["SERVER"],
CONF_ATTRIBUTES: [
{
CONF_ATTRIBUTE_ID: 0x55,
CONF_TYPE: "SINGLE",
CONF_REPORT: REPORT["enable"],
CONF_DEVICE: None,
},
{
CONF_ATTRIBUTE_ID: 0x51,
CONF_TYPE: "BOOL",
},
{
CONF_ATTRIBUTE_ID: 0x6F,
CONF_TYPE: "8BITMAP",
},
{
CONF_ATTRIBUTE_ID: 0x1C,
CONF_TYPE: "CHAR_STRING",
},
],
},
],
},
}
+62 -7
View File
@@ -16,11 +16,13 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_AP,
CONF_DEVICE,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_MAX_LENGTH,
CONF_MODEL,
CONF_NAME,
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE,
CONF_WIFI,
)
@@ -29,7 +31,16 @@ 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 import (
ANALOG_INPUT_APPTYPE,
BACNET_UNIT_NO_UNITS,
BACNET_UNITS,
CONF_REPORT,
CONF_ROUTER,
KEY_ZIGBEE,
REPORT,
ZigbeeAttribute,
)
from .const_esp32 import (
ATTR_TYPE,
CLUSTER_ID,
@@ -40,6 +51,7 @@ from .const_esp32 import (
DEVICE_ID,
DEVICE_TYPE,
KEY_BS_EP,
KEY_SENSOR_EP,
ROLE,
SCALE,
)
@@ -55,6 +67,10 @@ def get_c_size(bits: str, options: list[int]) -> str:
def get_c_type(attr_type: str) -> Any | None:
if attr_type == "BOOL":
return cg.bool_
if attr_type == "SINGLE":
return cg.float_
if attr_type == "DOUBLE":
return cg.double
if "STRING" in attr_type:
return cg.std_string
test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type)
@@ -66,19 +82,23 @@ def get_c_type(attr_type: str) -> Any | None:
def get_cv_by_type(attr_type: str) -> Any | None:
if attr_type == "BOOL":
return cv.boolean
if attr_type in ["SINGLE", "DOUBLE"]:
return cv.float_
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
raise cv.Invalid(f"Zigbee: type {attr_type} not supported or implemented")
def get_default_by_type(attr_type: str) -> str | bool | int:
def get_default_by_type(attr_type: str) -> str | bool | int | float:
if attr_type == "CHAR_STRING":
return ""
if attr_type == "BOOL":
return False
if attr_type in ["SINGLE", "DOUBLE"]:
return float("nan")
return 0
@@ -134,9 +154,8 @@ def final_validate_esp32(config: ConfigType) -> ConfigType:
return config
def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType:
ep = copy.deepcopy(ep_configs["binary_input"])
for cl in ep.get(CONF_CLUSTERS, []):
def setup_attributes(config: ConfigType, clusters: list[dict[str, Any]]) -> None:
for cl in clusters:
for attr in cl[CONF_ATTRIBUTES]:
if (
attr[CONF_ATTRIBUTE_ID] == 0x1C
@@ -159,6 +178,41 @@ def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType:
else:
attr[CONF_ID] = None
validate_attributes(attr)
def validate_sensor_esp32(config: ConfigType) -> ConfigType:
ep = copy.deepcopy(ep_configs["analog_input"])
# get application type from device class and meas unit
# if none get BACNET unit from meas unit
dev_class = config.get(CONF_DEVICE_CLASS)
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
apptype = ANALOG_INPUT_APPTYPE.get((dev_class, unit))
bacunit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS)
if apptype is not None:
ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append(
{
CONF_ATTRIBUTE_ID: 0x100,
CONF_VALUE: (apptype << 16) | 0xFFFF,
CONF_TYPE: "U32",
},
)
ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append(
{
CONF_ATTRIBUTE_ID: 0x75,
CONF_VALUE: bacunit,
CONF_TYPE: "16BIT_ENUM",
},
)
setup_attributes(config, ep[CONF_CLUSTERS])
zb_data = CORE.data.setdefault(KEY_ZIGBEE, {})
sensor_ep: list[dict] = zb_data.setdefault(KEY_SENSOR_EP, [])
sensor_ep.append(ep)
return config
def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType:
ep = copy.deepcopy(ep_configs["binary_input"])
setup_attributes(config, ep[CONF_CLUSTERS])
zb_data = CORE.data.setdefault(KEY_ZIGBEE, {})
binary_sensor_ep: list[dict] = zb_data.setdefault(KEY_BS_EP, [])
binary_sensor_ep.append(ep)
@@ -254,8 +308,9 @@ async def esp32_to_code(config: ConfigType) -> None:
# create endpoints
zb_data = CORE.data.get(KEY_ZIGBEE, {})
sensor_ep: list[dict] = zb_data.get(KEY_SENSOR_EP, [])
binary_sensor_ep: list[dict] = zb_data.get(KEY_BS_EP, [])
ep_list = create_ep(binary_sensor_ep, config.get(CONF_ROUTER))
ep_list = create_ep(sensor_ep + binary_sensor_ep, config.get(CONF_ROUTER))
# setup zigbee components
var = cg.new_Pvariable(config[CONF_ID])
@@ -33,6 +33,9 @@ esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp
case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY:
ret = esp_zb_cluster_list_add_identify_cluster(cluster_list, attr_list, role_mask);
break;
case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
ret = esp_zb_cluster_list_add_analog_input_cluster(cluster_list, attr_list, role_mask);
break;
case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT:
ret = esp_zb_cluster_list_add_binary_input_cluster(cluster_list, attr_list, role_mask);
break;
@@ -49,6 +52,8 @@ esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id
return esp_zb_basic_cluster_create(NULL);
case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY:
return esp_zb_identify_cluster_create(NULL);
case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
return esp_zb_analog_input_cluster_create(NULL);
case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT:
return esp_zb_binary_input_cluster_create(NULL);
default:
@@ -63,6 +68,8 @@ esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list
return esp_zb_basic_cluster_add_attr(attr_list, attr_id, value_p);
case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY:
return esp_zb_identify_cluster_add_attr(attr_list, attr_id, value_p);
case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
return esp_zb_analog_input_cluster_add_attr(attr_list, attr_id, value_p);
case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT:
return esp_zb_binary_input_cluster_add_attr(attr_list, attr_id, value_p);
default:
+2 -64
View File
@@ -10,35 +10,6 @@ from esphome.const import (
CONF_MODEL,
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
@@ -50,6 +21,8 @@ from esphome.cpp_generator import (
from esphome.types import ConfigType
from .const import (
BACNET_UNIT_NO_UNITS,
BACNET_UNITS,
CONF_ON_JOIN,
CONF_POWER_SOURCE,
CONF_ROUTER,
@@ -86,41 +59,6 @@ ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component)
ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", cg.Component)
ZigbeeNumber = zigbee_ns.class_("ZigbeeNumber", 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(
{
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent),
+1
View File
@@ -1240,6 +1240,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh"
UNIT_KILOWATT = "kW"
UNIT_KILOWATT_HOURS = "kWh"
UNIT_LITRE = "L"
UNIT_LITRE_PER_SECOND = "L/s"
UNIT_LUX = "lx"
UNIT_MEGAJOULE = "MJ"
UNIT_METER = "m"
+3 -3
View File
@@ -1,10 +1,10 @@
packages:
- !include common.yaml
binary_sensor:
- platform: template
name: "Garage Door Open 10"
report: "enable"
- platform: template
name: "Garage Door Open 11"
report: "coordinator"
- platform: template
name: "Garage Door Open 12"
report: "force"