mirror of
https://github.com/esphome/esphome.git
synced 2026-10-04 02:05:59 +00:00
[zigbee] Add cluster: device_class to expose ESP32 sensors via measurement clusters instead of Analog/Binary Input (#17388)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
Jonathan Swoboda
parent
23afdc1d9b
commit
cd0a0f3fca
@@ -18,6 +18,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
|||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_CLUSTER,
|
||||||
CONF_ENDPOINT,
|
CONF_ENDPOINT,
|
||||||
CONF_MAX_EP_NUMBER,
|
CONF_MAX_EP_NUMBER,
|
||||||
CONF_ON_JOIN,
|
CONF_ON_JOIN,
|
||||||
@@ -74,6 +75,11 @@ BASE_SCHEMA = cv.Schema(
|
|||||||
_check_report_deprecation,
|
_check_report_deprecation,
|
||||||
cv.enum(REPORT, lower=True),
|
cv.enum(REPORT, lower=True),
|
||||||
),
|
),
|
||||||
|
cv.OnlyWith(CONF_CLUSTER, ["esp32", "zigbee"], default="generic"): cv.All(
|
||||||
|
cv.requires_component("zigbee"),
|
||||||
|
cv.requires_component("esp32"),
|
||||||
|
cv.one_of(*["generic", "device_class"], lower=True),
|
||||||
|
),
|
||||||
cv.Optional(CONF_ENDPOINT): cv.All(
|
cv.Optional(CONF_ENDPOINT): cv.All(
|
||||||
cv.requires_component("zigbee"),
|
cv.requires_component("zigbee"),
|
||||||
cv.requires_component("esp32"),
|
cv.requires_component("esp32"),
|
||||||
@@ -196,21 +202,27 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
||||||
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
|
if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL):
|
||||||
return
|
return
|
||||||
if CORE.using_zephyr:
|
if CORE.using_zephyr:
|
||||||
from .zigbee_zephyr import zephyr_setup_binary_sensor
|
if not config.get(CONF_ZIGBEE_ID):
|
||||||
|
return
|
||||||
await zephyr_setup_binary_sensor(entity, config)
|
from .zigbee_zephyr import add_binary_sensor
|
||||||
|
else:
|
||||||
|
from .zigbee_esp32 import add_component as add_binary_sensor
|
||||||
|
CORE.add_job(add_binary_sensor, entity, config)
|
||||||
|
|
||||||
|
|
||||||
async def setup_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
async def setup_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
||||||
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
|
if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL):
|
||||||
return
|
return
|
||||||
if CORE.using_zephyr:
|
if CORE.using_zephyr:
|
||||||
from .zigbee_zephyr import zephyr_setup_sensor
|
if not config.get(CONF_ZIGBEE_ID):
|
||||||
|
return
|
||||||
await zephyr_setup_sensor(entity, config)
|
from .zigbee_zephyr import add_sensor
|
||||||
|
else:
|
||||||
|
from .zigbee_esp32 import add_component as add_sensor
|
||||||
|
CORE.add_job(add_sensor, entity, config)
|
||||||
|
|
||||||
|
|
||||||
async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ REPORT = {
|
|||||||
"default": report.ZIGBEE_REPORT_DEFAULT,
|
"default": report.ZIGBEE_REPORT_DEFAULT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CONF_CLUSTER = "cluster"
|
||||||
CONF_ENDPOINT = "endpoint"
|
CONF_ENDPOINT = "endpoint"
|
||||||
CONF_MAX_EP_NUMBER = 239
|
CONF_MAX_EP_NUMBER = 239
|
||||||
CONF_ON_JOIN = "on_join"
|
CONF_ON_JOIN = "on_join"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
|
||||||
|
ALLOWED_UNITS = "allowed_units"
|
||||||
DEVICE_TYPE = "device_type"
|
DEVICE_TYPE = "device_type"
|
||||||
ROLE = "role"
|
ROLE = "role"
|
||||||
CONF_CLUSTERS = "clusters"
|
CONF_CLUSTERS = "clusters"
|
||||||
CONF_ATTRIBUTES = "attributes"
|
CONF_ATTRIBUTES = "attributes"
|
||||||
CONF_CLUSTER = "cluster"
|
|
||||||
SCALE = "scale"
|
|
||||||
CONF_ATTRIBUTE_ID = "attribute_id"
|
CONF_ATTRIBUTE_ID = "attribute_id"
|
||||||
|
KEY_ZIGBEE_ATTRIBUTES = "zigbee_attributes"
|
||||||
|
CONNECT = "connect"
|
||||||
|
SCALE = "scale"
|
||||||
KEY_ZIGBEE_EP = "zigbee_ep"
|
KEY_ZIGBEE_EP = "zigbee_ep"
|
||||||
KEY_ZIGBEE_EP_NO_NUM = "zigbee_ep_no_num"
|
KEY_ZIGBEE_EP_NO_NUM = "zigbee_ep_no_num"
|
||||||
KEY_ZIGBEE_FIRST_EP_CL = "zigbee_first_ep_cl"
|
KEY_ZIGBEE_FIRST_EP_CL = "zigbee_first_ep_cl"
|
||||||
@@ -14,6 +16,11 @@ KEY_ZIGBEE_FIRST_EP_CL = "zigbee_first_ep_cl"
|
|||||||
DEVICE_ID = {
|
DEVICE_ID = {
|
||||||
"RANGE_EXTENDER": cg.RawExpression("EZB_ZHA_RANGE_EXTENDER_DEVICE_ID"),
|
"RANGE_EXTENDER": cg.RawExpression("EZB_ZHA_RANGE_EXTENDER_DEVICE_ID"),
|
||||||
"SIMPLE_SENSOR": cg.RawExpression("EZB_ZHA_SIMPLE_SENSOR_DEVICE_ID"),
|
"SIMPLE_SENSOR": cg.RawExpression("EZB_ZHA_SIMPLE_SENSOR_DEVICE_ID"),
|
||||||
|
"LIGHT_SENSOR": cg.RawExpression("EZB_ZHA_LIGHT_SENSOR_DEVICE_ID"),
|
||||||
|
"OCCUPANCY_SENSOR": cg.RawExpression("EZB_ZHA_OCCUPANCY_SENSOR_DEVICE_ID"),
|
||||||
|
"TEMPERATURE_SENSOR": cg.RawExpression("EZB_ZHA_TEMPERATURE_SENSOR_DEVICE_ID"),
|
||||||
|
"PRESSURE_SENSOR": cg.RawExpression("EZB_ZHA_PRESSURE_SENSOR_DEVICE_ID"),
|
||||||
|
"FLOW_SENSOR": cg.RawExpression("EZB_ZHA_FLOW_SENSOR_DEVICE_ID"),
|
||||||
"CUSTOM_ATTR": 0xFFF2,
|
"CUSTOM_ATTR": 0xFFF2,
|
||||||
}
|
}
|
||||||
cluster_id = cg.esphome_ns.enum("ezb_zcl_cluster_id_e")
|
cluster_id = cg.esphome_ns.enum("ezb_zcl_cluster_id_e")
|
||||||
@@ -22,6 +29,14 @@ CLUSTER_ID = {
|
|||||||
"TIME": cluster_id.EZB_ZCL_CLUSTER_ID_TIME,
|
"TIME": cluster_id.EZB_ZCL_CLUSTER_ID_TIME,
|
||||||
"BINARY_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_BINARY_INPUT,
|
"BINARY_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_BINARY_INPUT,
|
||||||
"ANALOG_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
|
"ANALOG_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
|
||||||
|
"ILLUMINANCE_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT,
|
||||||
|
"TEMPERATURE_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_TEMPERATURE_MEASUREMENT,
|
||||||
|
"PRESSURE_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT,
|
||||||
|
"FLOW_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT,
|
||||||
|
"REL_HUMIDITY_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT,
|
||||||
|
"OCCUPANCY_SENSING": cluster_id.EZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING,
|
||||||
|
"CARBON_DIOXIDE_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT,
|
||||||
|
"PM2_5_MEASUREMENT": cluster_id.EZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT,
|
||||||
}
|
}
|
||||||
CLUSTER_ROLE = {
|
CLUSTER_ROLE = {
|
||||||
"SERVER": cg.RawExpression("EZB_ZCL_CLUSTER_SERVER"),
|
"SERVER": cg.RawExpression("EZB_ZCL_CLUSTER_SERVER"),
|
||||||
@@ -31,6 +46,12 @@ attr_type = cg.esphome_ns.enum("ezb_zcl_attr_type_e")
|
|||||||
ATTR_TYPE = {
|
ATTR_TYPE = {
|
||||||
"BOOL": attr_type.EZB_ZCL_ATTR_TYPE_BOOL,
|
"BOOL": attr_type.EZB_ZCL_ATTR_TYPE_BOOL,
|
||||||
"MAP8": attr_type.EZB_ZCL_ATTR_TYPE_MAP8,
|
"MAP8": attr_type.EZB_ZCL_ATTR_TYPE_MAP8,
|
||||||
|
"UINT8": attr_type.EZB_ZCL_ATTR_TYPE_UINT8,
|
||||||
|
"UINT16": attr_type.EZB_ZCL_ATTR_TYPE_UINT16,
|
||||||
|
"INT8": attr_type.EZB_ZCL_ATTR_TYPE_INT8,
|
||||||
|
"INT16": attr_type.EZB_ZCL_ATTR_TYPE_INT16,
|
||||||
|
"ENUM8": attr_type.EZB_ZCL_ATTR_TYPE_ENUM8,
|
||||||
|
"ENUM16": attr_type.EZB_ZCL_ATTR_TYPE_ENUM16,
|
||||||
"STRING": attr_type.EZB_ZCL_ATTR_TYPE_STRING,
|
"STRING": attr_type.EZB_ZCL_ATTR_TYPE_STRING,
|
||||||
"SINGLE": attr_type.EZB_ZCL_ATTR_TYPE_SINGLE,
|
"SINGLE": attr_type.EZB_ZCL_ATTR_TYPE_SINGLE,
|
||||||
"DOUBLE": attr_type.EZB_ZCL_ATTR_TYPE_DOUBLE,
|
"DOUBLE": attr_type.EZB_ZCL_ATTR_TYPE_DOUBLE,
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ void ZigbeeAttribute::set_attr_() {
|
|||||||
}
|
}
|
||||||
// Check for error
|
// Check for error
|
||||||
if (state != EZB_ZCL_STATUS_SUCCESS) {
|
if (state != EZB_ZCL_STATUS_SUCCESS) {
|
||||||
ESP_LOGE(TAG, "Setting attribute failed, ZCL status: %u", static_cast<unsigned>(state));
|
ESP_LOGE(TAG, "Setting attribute 0x%04X on cluster 0x%04X failed, ZCL status: %u", this->attr_id_,
|
||||||
|
this->cluster_id_, static_cast<unsigned>(state));
|
||||||
}
|
}
|
||||||
esp_zigbee_lock_release();
|
esp_zigbee_lock_release();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
@@ -47,6 +49,7 @@ class ZigbeeAttribute final : public Component {
|
|||||||
void set_report(ZigbeeReportT report);
|
void set_report(ZigbeeReportT report);
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
template<typename T> void connect(sensor::Sensor *sensor);
|
template<typename T> void connect(sensor::Sensor *sensor);
|
||||||
|
template<typename T, typename F> void connect(sensor::Sensor *sensor, F &&f);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
template<typename T> void connect(binary_sensor::BinarySensor *sensor);
|
template<typename T> void connect(binary_sensor::BinarySensor *sensor);
|
||||||
@@ -68,6 +71,8 @@ class ZigbeeAttribute final : public Component {
|
|||||||
bool set_attr_requested_{false};
|
bool set_attr_requested_{false};
|
||||||
bool report_requested_{false};
|
bool report_requested_{false};
|
||||||
bool force_report_{false};
|
bool force_report_{false};
|
||||||
|
template<typename T> T scale_value_(float value);
|
||||||
|
template<typename T> T invalid_value_();
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename T> void ZigbeeAttribute::add_attr(T value) {
|
template<typename T> void ZigbeeAttribute::add_attr(T value) {
|
||||||
@@ -85,9 +90,44 @@ template<typename T> void ZigbeeAttribute::set_attr(const T &value) {
|
|||||||
this->enable_loop();
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename T> T ZigbeeAttribute::scale_value_(float value) {
|
||||||
|
static_assert(sizeof(T) <= 2 || std::is_floating_point_v<T>);
|
||||||
|
if constexpr (std::is_integral<T>::value) {
|
||||||
|
const float scaled = this->scale_ * value;
|
||||||
|
if (std::isnan(value) || scaled < static_cast<float>(std::numeric_limits<T>::lowest()) ||
|
||||||
|
scaled > static_cast<float>(std::numeric_limits<T>::max())) {
|
||||||
|
return this->invalid_value_<T>(); // 0x8000 / 0xFFFF / 0 for bitmaps
|
||||||
|
}
|
||||||
|
return static_cast<T>(lroundf(scaled));
|
||||||
|
}
|
||||||
|
return static_cast<T>(this->scale_ * value);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T> T ZigbeeAttribute::invalid_value_() {
|
||||||
|
if constexpr (std::is_integral_v<T>) {
|
||||||
|
if constexpr (std::is_signed_v<T>) {
|
||||||
|
// For signed integer types, NaN is represented by the minimum value
|
||||||
|
return static_cast<T>(std::numeric_limits<T>::min());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->attr_type_ >= EZB_ZCL_ATTR_TYPE_UINT8 && this->attr_type_ <= EZB_ZCL_ATTR_TYPE_ENUM16) {
|
||||||
|
// For unsigned integer types and enum, NaN is represented by the maximum value
|
||||||
|
return static_cast<T>(std::numeric_limits<T>::max());
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other integer types, return 0 as a fallback
|
||||||
|
return static_cast<T>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::numeric_limits<T>::quiet_NaN(); // For floating-point types, return NaN
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
template<typename T> void ZigbeeAttribute::connect(sensor::Sensor *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)); });
|
sensor->add_on_state_callback([this](float value) { this->set_attr(this->scale_value_<T>(value)); });
|
||||||
|
}
|
||||||
|
template<typename T, typename F> void ZigbeeAttribute::connect(sensor::Sensor *sensor, F &&f) {
|
||||||
|
sensor->add_on_state_callback([f = std::forward<F>(f), this](float value) { this->set_attr((T) f(value)); });
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
|||||||
@@ -1,8 +1,31 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_DEVICE, CONF_ID, CONF_TYPE
|
from esphome.const import (
|
||||||
from esphome.core import CORE
|
CONF_ID,
|
||||||
|
CONF_LAMBDA,
|
||||||
|
CONF_TYPE,
|
||||||
|
CONF_VALUE,
|
||||||
|
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||||
|
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
|
DEVICE_CLASS_ILLUMINANCE,
|
||||||
|
DEVICE_CLASS_OCCUPANCY,
|
||||||
|
DEVICE_CLASS_PM25,
|
||||||
|
DEVICE_CLASS_PRESSURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_VOLUME_FLOW_RATE,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
UNIT_CUBIC_METER_PER_HOUR,
|
||||||
|
UNIT_HECTOPASCAL,
|
||||||
|
UNIT_LITRE_PER_HOUR,
|
||||||
|
UNIT_LUX,
|
||||||
|
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
UNIT_PARTS_PER_MILLION,
|
||||||
|
UNIT_PASCAL,
|
||||||
|
UNIT_PERCENT,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE, Lambda
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_MAX_EP_NUMBER,
|
CONF_MAX_EP_NUMBER,
|
||||||
@@ -12,70 +35,264 @@ from .const import (
|
|||||||
REPORT,
|
REPORT,
|
||||||
)
|
)
|
||||||
from .const_esp32 import (
|
from .const_esp32 import (
|
||||||
|
ALLOWED_UNITS,
|
||||||
CONF_ATTRIBUTE_ID,
|
CONF_ATTRIBUTE_ID,
|
||||||
CONF_ATTRIBUTES,
|
CONF_ATTRIBUTES,
|
||||||
CONF_CLUSTERS,
|
CONF_CLUSTERS,
|
||||||
|
CONNECT,
|
||||||
DEVICE_TYPE,
|
DEVICE_TYPE,
|
||||||
KEY_ZIGBEE_EP,
|
KEY_ZIGBEE_EP,
|
||||||
KEY_ZIGBEE_EP_NO_NUM,
|
KEY_ZIGBEE_EP_NO_NUM,
|
||||||
KEY_ZIGBEE_FIRST_EP_CL,
|
KEY_ZIGBEE_FIRST_EP_CL,
|
||||||
ROLE,
|
ROLE,
|
||||||
|
SCALE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# endpoint configs:
|
# endpoint configs:
|
||||||
ep_configs: dict[str, dict[str, Any]] = {
|
ANALOG_INPUT_EP = {
|
||||||
"binary_input": {
|
CONF_CLUSTERS: [
|
||||||
DEVICE_TYPE: "SIMPLE_SENSOR",
|
{
|
||||||
|
CONF_ID: "ANALOG_INPUT",
|
||||||
|
ROLE: "SERVER",
|
||||||
|
CONF_ATTRIBUTES: [
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x55,
|
||||||
|
CONF_TYPE: "SINGLE",
|
||||||
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
|
CONNECT: True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x51,
|
||||||
|
CONF_TYPE: "BOOL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x6F,
|
||||||
|
CONF_TYPE: "MAP8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x1C,
|
||||||
|
CONF_TYPE: "STRING",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
BINARY_INPUT_EP = {
|
||||||
|
DEVICE_TYPE: "SIMPLE_SENSOR",
|
||||||
|
CONF_CLUSTERS: [
|
||||||
|
{
|
||||||
|
CONF_ID: "BINARY_INPUT",
|
||||||
|
ROLE: "SERVER",
|
||||||
|
CONF_ATTRIBUTES: [
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x55,
|
||||||
|
CONF_TYPE: "BOOL",
|
||||||
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
|
CONNECT: True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x51,
|
||||||
|
CONF_TYPE: "BOOL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x6F,
|
||||||
|
CONF_TYPE: "MAP8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x1C,
|
||||||
|
CONF_TYPE: "STRING",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _pressure_ep(device_type: bool = False) -> dict[str, Any]:
|
||||||
|
ep = {
|
||||||
|
ALLOWED_UNITS: [UNIT_HECTOPASCAL, UNIT_PASCAL],
|
||||||
CONF_CLUSTERS: [
|
CONF_CLUSTERS: [
|
||||||
{
|
{
|
||||||
CONF_ID: "BINARY_INPUT",
|
CONF_ID: "PRESSURE_MEASUREMENT",
|
||||||
ROLE: "SERVER",
|
ROLE: "SERVER",
|
||||||
CONF_ATTRIBUTES: [
|
CONF_ATTRIBUTES: [
|
||||||
{
|
{
|
||||||
CONF_ATTRIBUTE_ID: 0x55,
|
CONF_ATTRIBUTE_ID: 0x0,
|
||||||
CONF_TYPE: "BOOL",
|
CONF_TYPE: "INT16",
|
||||||
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
CONF_DEVICE: None,
|
CONNECT: True,
|
||||||
|
SCALE: {
|
||||||
|
UNIT_HECTOPASCAL: 1,
|
||||||
|
UNIT_PASCAL: 0.01,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if device_type:
|
||||||
|
ep[DEVICE_TYPE] = (
|
||||||
|
"PRESSURE_SENSOR" # Sensor that measures pressure of liquids like water
|
||||||
|
)
|
||||||
|
return ep
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_EP_CONFIGS: dict[str, dict[str, Any]] = {
|
||||||
|
DEVICE_CLASS_TEMPERATURE: {
|
||||||
|
ALLOWED_UNITS: [UNIT_CELSIUS],
|
||||||
|
DEVICE_TYPE: "TEMPERATURE_SENSOR",
|
||||||
|
CONF_CLUSTERS: [
|
||||||
|
{
|
||||||
|
CONF_ID: "TEMPERATURE_MEASUREMENT",
|
||||||
|
ROLE: "SERVER",
|
||||||
|
CONF_ATTRIBUTES: [
|
||||||
{
|
{
|
||||||
CONF_ATTRIBUTE_ID: 0x51,
|
CONF_ATTRIBUTE_ID: 0x0,
|
||||||
CONF_TYPE: "BOOL",
|
CONF_TYPE: "INT16",
|
||||||
},
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
{
|
SCALE: 100,
|
||||||
CONF_ATTRIBUTE_ID: 0x6F,
|
CONNECT: True,
|
||||||
CONF_TYPE: "MAP8",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
CONF_ATTRIBUTE_ID: 0x1C,
|
|
||||||
CONF_TYPE: "STRING",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"analog_input": {
|
DEVICE_CLASS_HUMIDITY: {
|
||||||
|
ALLOWED_UNITS: [UNIT_PERCENT],
|
||||||
CONF_CLUSTERS: [
|
CONF_CLUSTERS: [
|
||||||
{
|
{
|
||||||
CONF_ID: "ANALOG_INPUT",
|
CONF_ID: "REL_HUMIDITY_MEASUREMENT",
|
||||||
ROLE: "SERVER",
|
ROLE: "SERVER",
|
||||||
CONF_ATTRIBUTES: [
|
CONF_ATTRIBUTES: [
|
||||||
{
|
{
|
||||||
CONF_ATTRIBUTE_ID: 0x55,
|
CONF_ATTRIBUTE_ID: 0x0,
|
||||||
|
CONF_TYPE: "UINT16",
|
||||||
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
|
SCALE: 100,
|
||||||
|
CONNECT: True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
DEVICE_CLASS_ATMOSPHERIC_PRESSURE: _pressure_ep(),
|
||||||
|
DEVICE_CLASS_PRESSURE: _pressure_ep(device_type=True),
|
||||||
|
DEVICE_CLASS_VOLUME_FLOW_RATE: {
|
||||||
|
ALLOWED_UNITS: [UNIT_LITRE_PER_HOUR, UNIT_CUBIC_METER_PER_HOUR],
|
||||||
|
DEVICE_TYPE: "FLOW_SENSOR",
|
||||||
|
CONF_CLUSTERS: [
|
||||||
|
{
|
||||||
|
CONF_ID: "FLOW_MEASUREMENT",
|
||||||
|
ROLE: "SERVER",
|
||||||
|
CONF_ATTRIBUTES: [
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x0,
|
||||||
|
CONF_TYPE: "UINT16",
|
||||||
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
|
CONNECT: True,
|
||||||
|
SCALE: {
|
||||||
|
UNIT_LITRE_PER_HOUR: 0.01,
|
||||||
|
UNIT_CUBIC_METER_PER_HOUR: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
DEVICE_CLASS_ILLUMINANCE: {
|
||||||
|
ALLOWED_UNITS: [UNIT_LUX],
|
||||||
|
DEVICE_TYPE: "LIGHT_SENSOR",
|
||||||
|
CONF_CLUSTERS: [
|
||||||
|
{
|
||||||
|
CONF_ID: "ILLUMINANCE_MEASUREMENT",
|
||||||
|
ROLE: "SERVER",
|
||||||
|
CONF_ATTRIBUTES: [
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x0,
|
||||||
|
CONF_TYPE: "UINT16",
|
||||||
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
|
CONF_LAMBDA: cv.lambda_(
|
||||||
|
Lambda(
|
||||||
|
"if (x < 0.0f || std::isnan(x)) return 0xFFFF;" # NaN
|
||||||
|
" if (x < 1.0f) return 0;" # too small to measure
|
||||||
|
" const float v = log10(x)*10000 + 1;"
|
||||||
|
" return v > 65534.0f ? 0xFFFE : (uint16_t) lroundf(v);" # clamp to 0xFFFE if too large
|
||||||
|
)
|
||||||
|
),
|
||||||
|
CONNECT: True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
DEVICE_CLASS_PM25: {
|
||||||
|
ALLOWED_UNITS: [UNIT_MICROGRAMS_PER_CUBIC_METER],
|
||||||
|
CONF_CLUSTERS: [
|
||||||
|
{
|
||||||
|
CONF_ID: "PM2_5_MEASUREMENT",
|
||||||
|
ROLE: "SERVER",
|
||||||
|
CONF_ATTRIBUTES: [
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x0,
|
||||||
CONF_TYPE: "SINGLE",
|
CONF_TYPE: "SINGLE",
|
||||||
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
CONF_DEVICE: None,
|
CONNECT: True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
CONF_ATTRIBUTE_ID: 0x51,
|
CONF_ATTRIBUTE_ID: 0x2,
|
||||||
CONF_TYPE: "BOOL",
|
CONF_TYPE: "SINGLE",
|
||||||
|
CONF_VALUE: 9999, # overwrite default 1.0
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
DEVICE_CLASS_CARBON_DIOXIDE: {
|
||||||
|
ALLOWED_UNITS: [UNIT_PARTS_PER_MILLION],
|
||||||
|
CONF_CLUSTERS: [
|
||||||
|
{
|
||||||
|
CONF_ID: "CARBON_DIOXIDE_MEASUREMENT",
|
||||||
|
ROLE: "SERVER",
|
||||||
|
CONF_ATTRIBUTES: [
|
||||||
{
|
{
|
||||||
CONF_ATTRIBUTE_ID: 0x6F,
|
CONF_ATTRIBUTE_ID: 0x0000,
|
||||||
|
CONF_TYPE: "SINGLE",
|
||||||
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
|
CONNECT: True,
|
||||||
|
SCALE: 0.000001,
|
||||||
|
},
|
||||||
|
{CONF_ATTRIBUTE_ID: 0x0001, CONF_TYPE: "SINGLE", CONF_VALUE: 0.0},
|
||||||
|
{CONF_ATTRIBUTE_ID: 0x0002, CONF_TYPE: "SINGLE", CONF_VALUE: 0.1},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
BINARY_SENSOR_EP_CONFIGS: dict[str, dict[str, Any]] = {
|
||||||
|
DEVICE_CLASS_OCCUPANCY: {
|
||||||
|
DEVICE_TYPE: "OCCUPANCY_SENSOR",
|
||||||
|
CONF_CLUSTERS: [
|
||||||
|
{
|
||||||
|
CONF_ID: "OCCUPANCY_SENSING",
|
||||||
|
ROLE: "SERVER",
|
||||||
|
CONF_ATTRIBUTES: [
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x0,
|
||||||
CONF_TYPE: "MAP8",
|
CONF_TYPE: "MAP8",
|
||||||
|
CONF_REPORT: cv.enum(REPORT, lower=True)("default"),
|
||||||
|
CONNECT: True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
CONF_ATTRIBUTE_ID: 0x1C,
|
CONF_ATTRIBUTE_ID: 0x1,
|
||||||
CONF_TYPE: "STRING",
|
CONF_TYPE: "ENUM8",
|
||||||
|
CONF_VALUE: 0, # hardcode PIR for now as ultrasonic or physical contact is unlikely
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x2,
|
||||||
|
CONF_TYPE: "MAP8",
|
||||||
|
CONF_VALUE: 0b00000001, # hardcode PIR for now as ultrasonic or physical contact is unlikely
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -181,13 +181,8 @@ static void zb_action_handler(ezb_zcl_core_action_callback_id_t callback_id, voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ZigbeeComponent::create_default_cluster(uint8_t endpoint_id, uint16_t device_id) {
|
void ZigbeeComponent::create_default_cluster(uint8_t endpoint_id, uint16_t device_id) {
|
||||||
ezb_af_ep_config_t config = {
|
ezb_af_ep_desc_t ep_desc =
|
||||||
.ep_id = endpoint_id,
|
esphome_zb_zha_default_ep_desc_create(endpoint_id, device_id, this->basic_cluster_data_.power_source);
|
||||||
.app_profile_id = EZB_AF_HA_PROFILE_ID,
|
|
||||||
.app_device_id = device_id,
|
|
||||||
.app_device_version = 0,
|
|
||||||
};
|
|
||||||
ezb_af_ep_desc_t ep_desc = ezb_af_create_endpoint_desc(&config);
|
|
||||||
if (ezb_af_device_add_endpoint_desc(this->dev_desc_, ep_desc) != EZB_ERR_NONE) {
|
if (ezb_af_device_add_endpoint_desc(this->dev_desc_, ep_desc) != EZB_ERR_NONE) {
|
||||||
ESP_LOGE(TAG, "Could not create endpoint %u", endpoint_id);
|
ESP_LOGE(TAG, "Could not create endpoint %u", endpoint_id);
|
||||||
}
|
}
|
||||||
@@ -232,13 +227,13 @@ void ZigbeeComponent::update_basic_cluster_(ezb_af_ep_desc_t ep_desc) {
|
|||||||
.power_source = this->basic_cluster_data_.power_source,
|
.power_source = this->basic_cluster_data_.power_source,
|
||||||
};
|
};
|
||||||
cluster_desc = ezb_zcl_basic_create_cluster_desc(&basic_cluster_cfg, EZB_ZCL_CLUSTER_SERVER);
|
cluster_desc = ezb_zcl_basic_create_cluster_desc(&basic_cluster_cfg, EZB_ZCL_CLUSTER_SERVER);
|
||||||
|
ezb_af_endpoint_add_cluster_desc(ep_desc, cluster_desc);
|
||||||
}
|
}
|
||||||
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID,
|
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID,
|
||||||
this->basic_cluster_data_.manufacturer);
|
this->basic_cluster_data_.manufacturer);
|
||||||
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID,
|
ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, EZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID,
|
||||||
this->basic_cluster_data_.model);
|
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_DATE_CODE_ID, this->basic_cluster_data_.date);
|
||||||
ezb_af_endpoint_add_cluster_desc(ep_desc, cluster_desc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ZigbeeComponent::register_device() {
|
bool ZigbeeComponent::register_device() {
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import esphome.config_validation as cv
|
|||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ACCURACY_DECIMALS,
|
CONF_ACCURACY_DECIMALS,
|
||||||
CONF_AP,
|
CONF_AP,
|
||||||
CONF_DEVICE,
|
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
|
CONF_LAMBDA,
|
||||||
CONF_MAX_LENGTH,
|
CONF_MAX_LENGTH,
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@@ -38,6 +38,7 @@ from .const import (
|
|||||||
ANALOG_INPUT_APPTYPE,
|
ANALOG_INPUT_APPTYPE,
|
||||||
BACNET_UNIT_NO_UNITS,
|
BACNET_UNIT_NO_UNITS,
|
||||||
BACNET_UNITS,
|
BACNET_UNITS,
|
||||||
|
CONF_CLUSTER,
|
||||||
CONF_ENDPOINT,
|
CONF_ENDPOINT,
|
||||||
CONF_POWER_SOURCE,
|
CONF_POWER_SOURCE,
|
||||||
CONF_REPORT,
|
CONF_REPORT,
|
||||||
@@ -48,19 +49,29 @@ from .const import (
|
|||||||
ZigbeeAttribute,
|
ZigbeeAttribute,
|
||||||
)
|
)
|
||||||
from .const_esp32 import (
|
from .const_esp32 import (
|
||||||
|
ALLOWED_UNITS,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
CLUSTER_ID,
|
CLUSTER_ID,
|
||||||
CLUSTER_ROLE,
|
CLUSTER_ROLE,
|
||||||
CONF_ATTRIBUTE_ID,
|
CONF_ATTRIBUTE_ID,
|
||||||
CONF_ATTRIBUTES,
|
CONF_ATTRIBUTES,
|
||||||
CONF_CLUSTERS,
|
CONF_CLUSTERS,
|
||||||
|
CONNECT,
|
||||||
DEVICE_ID,
|
DEVICE_ID,
|
||||||
DEVICE_TYPE,
|
DEVICE_TYPE,
|
||||||
|
KEY_ZIGBEE_ATTRIBUTES,
|
||||||
KEY_ZIGBEE_EP,
|
KEY_ZIGBEE_EP,
|
||||||
ROLE,
|
ROLE,
|
||||||
SCALE,
|
SCALE,
|
||||||
)
|
)
|
||||||
from .zigbee_ep_esp32 import add_ep, create_ep, ep_configs
|
from .zigbee_ep_esp32 import (
|
||||||
|
ANALOG_INPUT_EP,
|
||||||
|
BINARY_INPUT_EP,
|
||||||
|
BINARY_SENSOR_EP_CONFIGS,
|
||||||
|
SENSOR_EP_CONFIGS,
|
||||||
|
add_ep,
|
||||||
|
create_ep,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -79,8 +90,11 @@ def get_c_type(attr_type: str) -> Any | None:
|
|||||||
if "STRING" in attr_type:
|
if "STRING" in attr_type:
|
||||||
return cg.std_string
|
return cg.std_string
|
||||||
test = re.match(r"^(DATA|UINT|MAP|ENUM)(\d{1,2})$", attr_type)
|
test = re.match(r"^(DATA|UINT|MAP|ENUM)(\d{1,2})$", attr_type)
|
||||||
if test and test.group(2):
|
if test:
|
||||||
return getattr(cg, "uint" + get_c_size(test.group(2), [8, 16, 32, 64]))
|
return getattr(cg, "uint" + get_c_size(test.group(2), [8, 16, 32, 64]))
|
||||||
|
test = re.match(r"^INT(\d{1,2})$", attr_type)
|
||||||
|
if test:
|
||||||
|
return getattr(cg, "int" + get_c_size(test.group(1), [8, 16, 32, 64]))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -92,8 +106,11 @@ def get_cv_by_type(attr_type: str) -> Any | None:
|
|||||||
if "STRING" in attr_type:
|
if "STRING" in attr_type:
|
||||||
return cv.string
|
return cv.string
|
||||||
test = re.match(r"^(DATA|UINT|MAP|ENUM)(\d{1,2})$", attr_type)
|
test = re.match(r"^(DATA|UINT|MAP|ENUM)(\d{1,2})$", attr_type)
|
||||||
if test and test.group(2):
|
if test:
|
||||||
return cv.positive_int
|
return cv.positive_int
|
||||||
|
test = re.match(r"^INT(\d{1,2})$", attr_type)
|
||||||
|
if test:
|
||||||
|
return cv.int_
|
||||||
raise cv.Invalid(f"Zigbee: type {attr_type} not supported or implemented")
|
raise cv.Invalid(f"Zigbee: type {attr_type} not supported or implemented")
|
||||||
|
|
||||||
|
|
||||||
@@ -104,6 +121,14 @@ def get_default_by_type(attr_type: str) -> str | bool | int | float:
|
|||||||
return False
|
return False
|
||||||
if attr_type in ["SINGLE", "DOUBLE"]:
|
if attr_type in ["SINGLE", "DOUBLE"]:
|
||||||
return float("nan")
|
return float("nan")
|
||||||
|
test = re.match(r"^(UINT|ENUM)(\d{1,2})$", attr_type)
|
||||||
|
if test:
|
||||||
|
# ZCL "invalid value" sentinel for unsigned ints is the maximum (0xFFFF for UINT16)
|
||||||
|
return 2 ** (int(test.group(2))) - 1
|
||||||
|
test = re.match(r"^INT(\d{1,2})$", attr_type)
|
||||||
|
if test:
|
||||||
|
# ZCL "invalid value" sentinel for signed ints is the minimum (0x8000 for INT16)
|
||||||
|
return -(1 << (int(test.group(1)) - 1))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -167,59 +192,84 @@ def setup_attributes(config: ConfigType, clusters: list[dict[str, Any]]) -> None
|
|||||||
) # or use unidecode
|
) # or use unidecode
|
||||||
attr[CONF_VALUE] = str(name)
|
attr[CONF_VALUE] = str(name)
|
||||||
attr[CONF_MAX_LENGTH] = len(str(name))
|
attr[CONF_MAX_LENGTH] = len(str(name))
|
||||||
if CONF_DEVICE in attr: # connect device
|
if attr.get(CONNECT): # connect device
|
||||||
attr[CONF_DEVICE] = config[CONF_ID]
|
|
||||||
if CONF_REPORT in config:
|
if CONF_REPORT in config:
|
||||||
attr[CONF_REPORT] = config[CONF_REPORT]
|
attr[CONF_REPORT] = config[CONF_REPORT]
|
||||||
attr[CONF_ID] = cv.declare_id(ZigbeeAttribute)(None)
|
attr[CONF_ID] = cv.declare_id(ZigbeeAttribute)(None)
|
||||||
if "zb_attr_ids" not in config:
|
attr_list = config.setdefault(KEY_ZIGBEE_ATTRIBUTES, [])
|
||||||
config["zb_attr_ids"] = []
|
attr_list.append(attr)
|
||||||
config["zb_attr_ids"].append(attr[CONF_ID])
|
|
||||||
else:
|
else:
|
||||||
attr[CONF_ID] = None
|
attr[CONF_ID] = None
|
||||||
validate_attributes(attr)
|
validate_attributes(attr)
|
||||||
|
|
||||||
|
|
||||||
def validate_sensor_esp32(config: ConfigType) -> ConfigType:
|
def validate_sensor_esp32(config: ConfigType) -> ConfigType:
|
||||||
ep = copy.deepcopy(ep_configs["analog_input"])
|
|
||||||
# get application type from device class and meas unit
|
# get application type from device class and meas unit
|
||||||
# if none get BACNET unit from meas unit
|
# if none get BACNET unit from meas unit
|
||||||
dev_class = config.get(CONF_DEVICE_CLASS)
|
dev_class = config.get(CONF_DEVICE_CLASS)
|
||||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
|
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||||
apptype = ANALOG_INPUT_APPTYPE.get((dev_class, unit))
|
if config[CONF_CLUSTER] == "device_class":
|
||||||
bacunit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS)
|
if dev_class not in SENSOR_EP_CONFIGS:
|
||||||
accuracy = config.get(CONF_ACCURACY_DECIMALS)
|
raise cv.Invalid(
|
||||||
if apptype is not None:
|
"'cluster: device_class' requires a supported 'device_class'. "
|
||||||
|
f"Supported: {', '.join(SENSOR_EP_CONFIGS)}. Use 'cluster: generic' otherwise."
|
||||||
|
)
|
||||||
|
ep = copy.deepcopy(SENSOR_EP_CONFIGS[dev_class])
|
||||||
|
if unit not in ep[ALLOWED_UNITS]:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Device class '{dev_class}' requires one of units {', '.join(ep[ALLOWED_UNITS])}."
|
||||||
|
)
|
||||||
|
for attr in ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES]:
|
||||||
|
if isinstance(attr.get(SCALE), dict):
|
||||||
|
attr[SCALE] = attr[SCALE][unit]
|
||||||
|
if isinstance(attr.get(CONF_LAMBDA), dict):
|
||||||
|
attr[CONF_LAMBDA] = attr[CONF_LAMBDA][unit]
|
||||||
|
else:
|
||||||
|
ep = copy.deepcopy(ANALOG_INPUT_EP)
|
||||||
|
apptype = ANALOG_INPUT_APPTYPE.get((dev_class, unit))
|
||||||
|
bacunit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS)
|
||||||
|
accuracy = config.get(CONF_ACCURACY_DECIMALS)
|
||||||
|
if apptype is not None:
|
||||||
|
ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append(
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x100,
|
||||||
|
CONF_VALUE: (apptype << 16) | 0xFFFF,
|
||||||
|
CONF_TYPE: "UINT32",
|
||||||
|
},
|
||||||
|
)
|
||||||
ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append(
|
ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append(
|
||||||
{
|
{
|
||||||
CONF_ATTRIBUTE_ID: 0x100,
|
CONF_ATTRIBUTE_ID: 0x75,
|
||||||
CONF_VALUE: (apptype << 16) | 0xFFFF,
|
CONF_VALUE: bacunit,
|
||||||
CONF_TYPE: "UINT32",
|
CONF_TYPE: "ENUM16",
|
||||||
},
|
|
||||||
)
|
|
||||||
ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append(
|
|
||||||
{
|
|
||||||
CONF_ATTRIBUTE_ID: 0x75,
|
|
||||||
CONF_VALUE: bacunit,
|
|
||||||
CONF_TYPE: "ENUM16",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if accuracy is not None:
|
|
||||||
# Analog Input Resolution (0x006A): smallest reportable change
|
|
||||||
ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append(
|
|
||||||
{
|
|
||||||
CONF_ATTRIBUTE_ID: 0x6A,
|
|
||||||
CONF_VALUE: 10**-accuracy,
|
|
||||||
CONF_TYPE: "SINGLE",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if accuracy is not None:
|
||||||
|
# Analog Input Resolution (0x006A): smallest reportable change
|
||||||
|
ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append(
|
||||||
|
{
|
||||||
|
CONF_ATTRIBUTE_ID: 0x6A,
|
||||||
|
CONF_VALUE: 10**-accuracy,
|
||||||
|
CONF_TYPE: "SINGLE",
|
||||||
|
},
|
||||||
|
)
|
||||||
setup_attributes(config, ep[CONF_CLUSTERS])
|
setup_attributes(config, ep[CONF_CLUSTERS])
|
||||||
add_ep(ep, config.get(CONF_ENDPOINT), config.get(CONF_USE_DEVICE_TYPE))
|
add_ep(ep, config.get(CONF_ENDPOINT), config.get(CONF_USE_DEVICE_TYPE))
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType:
|
def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType:
|
||||||
ep = copy.deepcopy(ep_configs["binary_input"])
|
dev_class = config.get(CONF_DEVICE_CLASS)
|
||||||
|
if config[CONF_CLUSTER] == "device_class":
|
||||||
|
if dev_class in BINARY_SENSOR_EP_CONFIGS:
|
||||||
|
ep = copy.deepcopy(BINARY_SENSOR_EP_CONFIGS[dev_class])
|
||||||
|
else:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"'cluster: device_class' requires a supported 'device_class'. "
|
||||||
|
f"Supported: {', '.join(BINARY_SENSOR_EP_CONFIGS)}. Use 'cluster: generic' otherwise."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ep = copy.deepcopy(BINARY_INPUT_EP)
|
||||||
setup_attributes(config, ep[CONF_CLUSTERS])
|
setup_attributes(config, ep[CONF_CLUSTERS])
|
||||||
add_ep(ep, config.get(CONF_ENDPOINT), config.get(CONF_USE_DEVICE_TYPE))
|
add_ep(ep, config.get(CONF_ENDPOINT), config.get(CONF_USE_DEVICE_TYPE))
|
||||||
return config
|
return config
|
||||||
@@ -274,15 +324,11 @@ async def attributes_to_code(
|
|||||||
)
|
)
|
||||||
await cg.register_component(attr_var, attr)
|
await cg.register_component(attr_var, attr)
|
||||||
|
|
||||||
cg.add(attr_var.add_attr(attr[CONF_VALUE]))
|
template_arg = cg.TemplateArguments(get_c_type(attr[CONF_TYPE]))
|
||||||
|
cg.add(attr_var.add_attr(template_arg, attr[CONF_VALUE]))
|
||||||
if CONF_REPORT in attr:
|
if CONF_REPORT in attr:
|
||||||
cg.add(attr_var.set_report(attr[CONF_REPORT]))
|
cg.add(attr_var.set_report(attr[CONF_REPORT]))
|
||||||
|
|
||||||
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) -> "MockObj":
|
async def esp32_to_code(config: ConfigType) -> "MockObj":
|
||||||
add_idf_component(
|
add_idf_component(
|
||||||
@@ -332,3 +378,19 @@ async def esp32_to_code(config: ConfigType) -> "MockObj":
|
|||||||
)
|
)
|
||||||
await attributes_to_code(var, ep_num, cl)
|
await attributes_to_code(var, ep_num, cl)
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
async def add_component(entity: cg.MockObj, config: ConfigType) -> None:
|
||||||
|
attrs = config.get(KEY_ZIGBEE_ATTRIBUTES, [])
|
||||||
|
for attr in attrs:
|
||||||
|
zb_attr = await cg.get_variable(attr[CONF_ID])
|
||||||
|
template_arg = cg.TemplateArguments(get_c_type(attr[CONF_TYPE]))
|
||||||
|
if attr.get(CONF_LAMBDA) is not None:
|
||||||
|
lambda_ = await cg.process_lambda(
|
||||||
|
attr[CONF_LAMBDA],
|
||||||
|
[(cg.float_, "x")],
|
||||||
|
return_type=get_c_type(attr[CONF_TYPE]),
|
||||||
|
)
|
||||||
|
cg.add(zb_attr.connect(template_arg, entity, lambda_))
|
||||||
|
else:
|
||||||
|
cg.add(zb_attr.connect(template_arg, entity))
|
||||||
|
|||||||
@@ -24,6 +24,36 @@ ezb_err_t esphome_zb_add_or_update_cluster(uint16_t cluster_id, ezb_af_ep_desc_t
|
|||||||
return ezb_af_endpoint_add_cluster_desc(ep_desc, cluster_desc);
|
return ezb_af_endpoint_add_cluster_desc(ep_desc, cluster_desc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ezb_af_ep_desc_t esphome_zb_zha_default_ep_desc_create(uint8_t ep_id, uint16_t device_id, uint8_t power_source) {
|
||||||
|
ezb_af_ep_desc_t ep_desc;
|
||||||
|
switch (device_id) {
|
||||||
|
case EZB_ZHA_LIGHT_SENSOR_DEVICE_ID: {
|
||||||
|
ezb_zha_light_sensor_config_t config = EZB_ZHA_LIGHT_SENSOR_CONFIG();
|
||||||
|
config.basic_cfg.power_source = power_source;
|
||||||
|
ep_desc = ezb_zha_create_light_sensor(ep_id, &config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EZB_ZHA_TEMPERATURE_SENSOR_DEVICE_ID: {
|
||||||
|
ezb_zha_temperature_sensor_config_t config = EZB_ZHA_TEMPERATURE_SENSOR_CONFIG();
|
||||||
|
config.basic_cfg.power_source = power_source;
|
||||||
|
ep_desc = ezb_zha_create_temperature_sensor(ep_id, &config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// For other device IDs no macro exists, so we create an empty endpoint descriptor
|
||||||
|
ezb_af_ep_config_t config = {
|
||||||
|
.ep_id = ep_id,
|
||||||
|
.app_profile_id = EZB_AF_HA_PROFILE_ID,
|
||||||
|
.app_device_id = device_id,
|
||||||
|
.app_device_version = 0,
|
||||||
|
};
|
||||||
|
ep_desc = ezb_af_create_endpoint_desc(&config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ep_desc;
|
||||||
|
}
|
||||||
|
|
||||||
ezb_zcl_cluster_desc_t esphome_zb_default_cluster_dscr_create(uint16_t cluster_id, uint8_t role_mask) {
|
ezb_zcl_cluster_desc_t esphome_zb_default_cluster_dscr_create(uint16_t cluster_id, uint8_t role_mask) {
|
||||||
switch (cluster_id) {
|
switch (cluster_id) {
|
||||||
case EZB_ZCL_CLUSTER_ID_BASIC:
|
case EZB_ZCL_CLUSTER_ID_BASIC:
|
||||||
@@ -36,6 +66,22 @@ ezb_zcl_cluster_desc_t esphome_zb_default_cluster_dscr_create(uint16_t cluster_i
|
|||||||
return ezb_zcl_analog_input_create_cluster_desc(NULL, role_mask);
|
return ezb_zcl_analog_input_create_cluster_desc(NULL, role_mask);
|
||||||
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
|
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
|
||||||
return ezb_zcl_binary_input_create_cluster_desc(NULL, role_mask);
|
return ezb_zcl_binary_input_create_cluster_desc(NULL, role_mask);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT:
|
||||||
|
return ezb_zcl_illuminance_measurement_create_cluster_desc(NULL, role_mask);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_TEMPERATURE_MEASUREMENT:
|
||||||
|
return ezb_zcl_temperature_measurement_create_cluster_desc(NULL, role_mask);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT:
|
||||||
|
return ezb_zcl_pressure_measurement_create_cluster_desc(NULL, role_mask);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT:
|
||||||
|
return ezb_zcl_flow_measurement_create_cluster_desc(NULL, role_mask);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT:
|
||||||
|
return ezb_zcl_rel_humidity_measurement_create_cluster_desc(NULL, role_mask);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING:
|
||||||
|
return ezb_zcl_occupancy_sensing_create_cluster_desc(NULL, role_mask);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT:
|
||||||
|
return ezb_zcl_carbon_dioxide_measurement_create_cluster_desc(NULL, role_mask);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT:
|
||||||
|
return ezb_zcl_pm2_5_measurement_create_cluster_desc(NULL, role_mask);
|
||||||
default: {
|
default: {
|
||||||
ezb_zcl_custom_cluster_config_t config = {0};
|
ezb_zcl_custom_cluster_config_t config = {0};
|
||||||
config.cluster_id = cluster_id;
|
config.cluster_id = cluster_id;
|
||||||
@@ -57,6 +103,22 @@ ezb_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, ezb_zcl_cluster_desc_
|
|||||||
return ezb_zcl_analog_input_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
return ezb_zcl_analog_input_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
|
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
|
||||||
return ezb_zcl_binary_input_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
return ezb_zcl_binary_input_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT:
|
||||||
|
return ezb_zcl_illuminance_measurement_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_TEMPERATURE_MEASUREMENT:
|
||||||
|
return ezb_zcl_temperature_measurement_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT:
|
||||||
|
return ezb_zcl_pressure_measurement_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT:
|
||||||
|
return ezb_zcl_flow_measurement_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT:
|
||||||
|
return ezb_zcl_rel_humidity_measurement_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING:
|
||||||
|
return ezb_zcl_occupancy_sensing_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT:
|
||||||
|
return ezb_zcl_carbon_dioxide_measurement_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
|
case EZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT:
|
||||||
|
return ezb_zcl_pm2_5_measurement_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||||
default:
|
default:
|
||||||
return EZB_ERR_NOT_FOUND;
|
return EZB_ERR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ extern "C" {
|
|||||||
|
|
||||||
#include "esp_zigbee.h"
|
#include "esp_zigbee.h"
|
||||||
|
|
||||||
|
ezb_af_ep_desc_t esphome_zb_zha_default_ep_desc_create(uint8_t ep_id, uint16_t device_id, uint8_t power_source);
|
||||||
ezb_err_t esphome_zb_cluster_add_or_update_attr(uint16_t cluster_id, ezb_zcl_cluster_desc_t cluster_desc,
|
ezb_err_t esphome_zb_cluster_add_or_update_attr(uint16_t cluster_id, ezb_zcl_cluster_desc_t cluster_desc,
|
||||||
uint16_t attr_id, void *value_p);
|
uint16_t attr_id, void *value_p);
|
||||||
ezb_err_t esphome_zb_add_or_update_cluster(uint16_t cluster_id, ezb_af_ep_desc_t ep_desc, uint8_t role_mask);
|
ezb_err_t esphome_zb_add_or_update_cluster(uint16_t cluster_id, ezb_af_ep_desc_t ep_desc, uint8_t role_mask);
|
||||||
|
|||||||
@@ -314,14 +314,6 @@ async def _ctx_to_code(config: ConfigType) -> None:
|
|||||||
cg.add(cg.RawExpression("ZB_AF_REGISTER_DEVICE_CTX(&zb_device_ctx)"))
|
cg.add(cg.RawExpression("ZB_AF_REGISTER_DEVICE_CTX(&zb_device_ctx)"))
|
||||||
|
|
||||||
|
|
||||||
async def zephyr_setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
|
||||||
CORE.add_job(_add_binary_sensor, entity, config)
|
|
||||||
|
|
||||||
|
|
||||||
async def zephyr_setup_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
|
||||||
CORE.add_job(_add_sensor, entity, config)
|
|
||||||
|
|
||||||
|
|
||||||
async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
||||||
CORE.add_job(_add_switch, entity, config)
|
CORE.add_job(_add_switch, entity, config)
|
||||||
|
|
||||||
@@ -404,7 +396,7 @@ async def _add_zigbee_ep(
|
|||||||
cg.add(var.set_parent(hub))
|
cg.add(var.set_parent(hub))
|
||||||
|
|
||||||
|
|
||||||
async def _add_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
async def add_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
||||||
await _add_zigbee_ep(
|
await _add_zigbee_ep(
|
||||||
entity,
|
entity,
|
||||||
config,
|
config,
|
||||||
@@ -416,7 +408,7 @@ async def _add_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _add_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
async def add_sensor(entity: cg.MockObj, config: ConfigType) -> None:
|
||||||
# Get BACnet engineering unit from unit_of_measurement
|
# Get BACnet engineering unit from unit_of_measurement
|
||||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT, "")
|
unit = config.get(CONF_UNIT_OF_MEASUREMENT, "")
|
||||||
bacnet_unit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS)
|
bacnet_unit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS)
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ packages:
|
|||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: template
|
- platform: template
|
||||||
name: "Garage Door Open 10"
|
name: "Occupancy Sensor"
|
||||||
report: "default"
|
report: "default"
|
||||||
use_device_type: false
|
device_class: occupancy
|
||||||
|
cluster: device_class
|
||||||
|
endpoint: 3
|
||||||
- platform: template
|
- platform: template
|
||||||
name: "Garage Door Open 12"
|
name: "Garage Door Open 12"
|
||||||
report: "force"
|
report: "force"
|
||||||
endpoint: 1
|
endpoint: 1
|
||||||
|
- platform: template
|
||||||
|
name: "Garage Door Open 13"
|
||||||
|
endpoint: 2
|
||||||
|
use_device_type: false
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
- platform: template
|
- platform: template
|
||||||
@@ -19,6 +25,25 @@ sensor:
|
|||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
endpoint: 1
|
endpoint: 1
|
||||||
use_device_type: true
|
use_device_type: true
|
||||||
|
cluster: device_class
|
||||||
|
- platform: template
|
||||||
|
name: "Light Sensor"
|
||||||
|
lambda: return 100.0;
|
||||||
|
device_class: illuminance
|
||||||
|
unit_of_measurement: "lx"
|
||||||
|
endpoint: 2
|
||||||
|
use_device_type: true
|
||||||
|
cluster: device_class
|
||||||
|
- platform: template
|
||||||
|
name: "pressure_sensor"
|
||||||
|
unit_of_measurement: Pa
|
||||||
|
device_class: pressure
|
||||||
|
cluster: device_class
|
||||||
|
- platform: template
|
||||||
|
name: "flow_sensor"
|
||||||
|
unit_of_measurement: L/h
|
||||||
|
device_class: volume_flow_rate
|
||||||
|
cluster: device_class
|
||||||
|
|
||||||
zigbee:
|
zigbee:
|
||||||
model: zigbee_test
|
model: zigbee_test
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
packages:
|
||||||
|
zigbee: !include common_esp32.yaml
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "humidity_sensor"
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
device_class: humidity
|
||||||
|
cluster: device_class
|
||||||
|
endpoint: 2
|
||||||
|
use_device_type: false
|
||||||
|
- platform: template
|
||||||
|
name: "pm25_sensor"
|
||||||
|
unit_of_measurement: 'µg/m³'
|
||||||
|
device_class: pm25
|
||||||
|
cluster: device_class
|
||||||
|
- platform: template
|
||||||
|
name: "co2_sensor"
|
||||||
|
unit_of_measurement: ppm
|
||||||
|
device_class: carbon_dioxide
|
||||||
|
cluster: device_class
|
||||||
|
- platform: template
|
||||||
|
name: "pressure_sensor2"
|
||||||
|
unit_of_measurement: hPa
|
||||||
|
device_class: pressure
|
||||||
|
cluster: device_class
|
||||||
|
- platform: template
|
||||||
|
name: "flow_sensor2"
|
||||||
|
unit_of_measurement: m³/h
|
||||||
|
device_class: volume_flow_rate
|
||||||
|
cluster: device_class
|
||||||
|
- platform: template
|
||||||
|
name: "atmospheric_pressure_sensor"
|
||||||
|
unit_of_measurement: hPa
|
||||||
|
device_class: atmospheric_pressure
|
||||||
|
cluster: device_class
|
||||||
Reference in New Issue
Block a user