diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py new file mode 100644 index 0000000000..dcaac83e50 --- /dev/null +++ b/esphome/components/zigbee/__init__.py @@ -0,0 +1,263 @@ +from datetime import datetime +import random + +from esphome import automation +import esphome.codegen as cg +from esphome.components.zephyr import zephyr_add_prj_conf +from esphome.components.zigbee_ctx import KEY_EP, KEY_ZIGBEE, zigbee_set_core_data +import esphome.config_validation as cv +from esphome.const import CONF_BINARY_SENSOR, CONF_ID, CONF_PLATFORM, __version__ +from esphome.core import CORE, ID, coroutine_with_priority +from esphome.cpp_generator import ( + AssignmentExpression, + MockObj, + VariableDeclarationExpression, +) +import esphome.final_validate as fv + +from .const import ( + CONF_BASIC_ATTRIB_LIST_EXT, + CONF_BASIC_ATTRS_EXT, + CONF_GROUPS_ATTRIB_LIST, + CONF_GROUPS_ATTRS, + CONF_IDENTIFY_ATTRIB_LIST, + CONF_IDENTIFY_ATTRS, + CONF_MAX_EP_NUMBER, + CONF_SCENES_ATTRIB_LIST, + CONF_SCENES_ATTRS, + CONF_SWITCH, + CONF_ZIGBEE_ID, + ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT, + ZB_ZCL_DECLARE_GROUPS_ATTRIB_LIST, + ZB_ZCL_DECLARE_IDENTIFY_ATTRIB_LIST, + ZB_ZCL_DECLARE_SCENES_ATTRIB_LIST, + Zigbee, + zb_char_t_ptr, + zb_zcl_basic_attrs_ext_t, + zb_zcl_groups_attrs_t, + zb_zcl_identify_attrs_t, + zb_zcl_scenes_attrs_t, + zigbee_ns, +) + +AUTO_LOAD = ["zigbee_ctx"] + +CONF_ON_JOIN = "on_join" + +ZigbeeBaseSchema = cv.Schema( + { + cv.GenerateID(CONF_ZIGBEE_ID): cv.use_id(Zigbee), + cv.GenerateID(CONF_BASIC_ATTRIB_LIST_EXT): cv.use_id( + ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT + ), + cv.GenerateID(CONF_IDENTIFY_ATTRIB_LIST): cv.use_id( + ZB_ZCL_DECLARE_IDENTIFY_ATTRIB_LIST + ), + cv.GenerateID(CONF_GROUPS_ATTRIB_LIST): cv.use_id( + ZB_ZCL_DECLARE_GROUPS_ATTRIB_LIST + ), + cv.GenerateID(CONF_SCENES_ATTRIB_LIST): cv.use_id( + ZB_ZCL_DECLARE_SCENES_ATTRIB_LIST + ), + }, +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(Zigbee), + cv.GenerateID(CONF_BASIC_ATTRS_EXT): cv.declare_id( + zb_zcl_basic_attrs_ext_t + ), + cv.GenerateID(CONF_BASIC_ATTRIB_LIST_EXT): cv.declare_id( + ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT + ), + cv.GenerateID(CONF_IDENTIFY_ATTRS): cv.declare_id(zb_zcl_identify_attrs_t), + cv.GenerateID(CONF_IDENTIFY_ATTRIB_LIST): cv.declare_id( + ZB_ZCL_DECLARE_IDENTIFY_ATTRIB_LIST + ), + cv.GenerateID(CONF_GROUPS_ATTRS): cv.declare_id(zb_zcl_groups_attrs_t), + cv.GenerateID(CONF_GROUPS_ATTRIB_LIST): cv.declare_id( + ZB_ZCL_DECLARE_GROUPS_ATTRIB_LIST + ), + cv.GenerateID(CONF_SCENES_ATTRS): cv.declare_id(zb_zcl_scenes_attrs_t), + cv.GenerateID(CONF_SCENES_ATTRIB_LIST): cv.declare_id( + ZB_ZCL_DECLARE_SCENES_ATTRIB_LIST + ), + cv.Optional(CONF_ON_JOIN): automation.validate_automation(single=True), + } + ).extend(cv.COMPONENT_SCHEMA), + zigbee_set_core_data, +) + + +def count_ep_by_type(fconf, type): + count = 0 + if type in fconf: + for entity in fconf[type]: + if CONF_PLATFORM in entity and entity[CONF_PLATFORM] == KEY_ZIGBEE: + count += 1 + return count + + +def validate_number_of_ep(config): + count = 0 + fconf = fv.full_config.get() + count += count_ep_by_type(fconf, CONF_SWITCH) + count += count_ep_by_type(fconf, CONF_BINARY_SENSOR) + if count > 8: + raise cv.Invalid(f"Maximum number of EP is {CONF_MAX_EP_NUMBER}") + + +FINAL_VALIDATE_SCHEMA = cv.All( + validate_number_of_ep, +) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_ZIGBEE") + # zigbee + zephyr_add_prj_conf("ZIGBEE", True) + zephyr_add_prj_conf("ZIGBEE_APP_UTILS", True) + zephyr_add_prj_conf("ZIGBEE_ROLE_END_DEVICE", True) + + zephyr_add_prj_conf("ZIGBEE_CHANNEL_SELECTION_MODE_MULTI", True) + + # TODO zigbee2mqtt do not update configuration of device without this + zephyr_add_prj_conf("IEEE802154_VENDOR_OUI_ENABLE", True) + random_number = random.randint(0x000000, 0xFFFFFF) + zephyr_add_prj_conf("IEEE802154_VENDOR_OUI", random_number) + + # crypto + zephyr_add_prj_conf("CRYPTO", True) + + # networking + zephyr_add_prj_conf("NET_IPV6", False) + zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False) + zephyr_add_prj_conf("NET_UDP", False) + + basic_attrs_ext = zigbee_new_variable(config[CONF_BASIC_ATTRS_EXT]) + zigbee_new_attr_list( + config[CONF_BASIC_ATTRIB_LIST_EXT], + zigbee_assign( + basic_attrs_ext.zcl_version, cg.global_ns.namespace("ZB_ZCL_VERSION") + ), + zigbee_assign(basic_attrs_ext.app_version, 0), + zigbee_assign(basic_attrs_ext.stack_version, 0), + zigbee_assign(basic_attrs_ext.hw_version, 0), + zigbee_set_string(basic_attrs_ext.mf_name, "esphome"), + zigbee_set_string(basic_attrs_ext.model_id, "v1"), + zigbee_set_string( + basic_attrs_ext.date_code, datetime.now().strftime("%d/%m/%y %H:%M") + ), + zigbee_assign( + basic_attrs_ext.power_source, + cg.global_ns.namespace("ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE"), + ), + zigbee_set_string(basic_attrs_ext.location_id, ""), + zigbee_assign( + basic_attrs_ext.ph_env, + cg.global_ns.namespace("ZB_ZCL_BASIC_ENV_UNSPECIFIED"), + ), + zigbee_set_string(basic_attrs_ext.sw_ver, __version__), + ) + + identify_attrs = zigbee_new_variable(config[CONF_IDENTIFY_ATTRS]) + zigbee_new_attr_list( + config[CONF_IDENTIFY_ATTRIB_LIST], + zigbee_assign( + identify_attrs.identify_time, + cg.global_ns.namespace("ZB_ZCL_IDENTIFY_IDENTIFY_TIME_DEFAULT_VALUE"), + ), + ) + + groups_attrs = zigbee_new_variable(config[CONF_GROUPS_ATTRS]) + zigbee_new_attr_list( + config[CONF_GROUPS_ATTRIB_LIST], + zigbee_assign(groups_attrs.name_support, 0), + ) + + scenes_attrs = zigbee_new_variable(config[CONF_SCENES_ATTRS]) + zigbee_new_attr_list( + config[CONF_SCENES_ATTRIB_LIST], + zigbee_assign(scenes_attrs.scene_count, 0), + zigbee_assign(scenes_attrs.current_scene, 0), + zigbee_assign(scenes_attrs.current_group, 0), + zigbee_assign(scenes_attrs.scene_valid, 0), + zigbee_assign(scenes_attrs.name_support, 0), + ) + + # the rest + var = cg.new_Pvariable(config[CONF_ID]) + + if on_join_config := config.get(CONF_ON_JOIN): + await automation.build_automation(var.get_join_trigger(), [], on_join_config) + await cg.register_component(var, config) + + +FactoryResetAction = zigbee_ns.class_("FactoryResetAction", automation.Action) + + +@automation.register_action("zigbee.factory_reset", FactoryResetAction, cv.Schema({})) +async def zigbee_factory_reset_to_code(config, action_id, template_arg, args): + return cg.new_Pvariable(action_id, template_arg) + + +def zigbee_new_variable(id_: ID, type_: "MockObj" = None) -> "MockObj": + assert isinstance(id_, ID) + obj = MockObj(id_, ".") + if type_ is not None: + id_.type = type_ + decl = VariableDeclarationExpression(id_.type, "", id_) + CORE.add_global(decl) + CORE.register_variable(id_, obj) + return obj + + +def zigbee_assign(target, expression): + cg.add(AssignmentExpression("", "", target, expression)) + return target + + +def zigbee_set_string(target, value: str): + cg.add( + cg.RawExpression( + f"ZB_ZCL_SET_STRING_VAL({target}, {cg.safe_exp(value)}, ZB_ZCL_STRING_CONST_SIZE({cg.safe_exp(value)}))" + ) + ) + return ID(str(target), True, zb_char_t_ptr) + + +def zigbee_new_attr_list(id_: ID, *args): + assert isinstance(id_, ID) + list = [] + for arg in args: + if str(zb_char_t_ptr) == str(arg.type): + list.append(f"{arg}") + else: + list.append(f"&{arg}") + + obj = cg.RawExpression(f'{id_.type}({id_}, {", ".join(list)})') + CORE.add_global(obj) + CORE.register_variable(id_, obj) + return id_ + + +def zigbee_new_cluster_list(id_: ID, *args): + assert isinstance(id_, ID) + list = [] + for arg in args: + list.append(f"{arg}") + obj = cg.RawExpression(f'{id_.type}({id_}, {", ".join(list)})') + CORE.add_global(obj) + return id_ + + +def zigbee_register_ep(id_: ID, cluster): + assert isinstance(id_, ID) + ep = len(CORE.data[KEY_ZIGBEE][KEY_EP]) + 1 + CORE.data[KEY_ZIGBEE][KEY_EP] += [str(id_)] + obj = cg.RawExpression(f"{id_.type}({id_}, {ep}, {cluster})") + CORE.add_global(obj) + return ep diff --git a/esphome/components/zigbee/binary_sensor/__init__.py b/esphome/components/zigbee/binary_sensor/__init__.py new file mode 100644 index 0000000000..50627eaf93 --- /dev/null +++ b/esphome/components/zigbee/binary_sensor/__init__.py @@ -0,0 +1,115 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_STATE +from esphome.core import coroutine_with_priority + +from .. import ( + ZigbeeBaseSchema, + zigbee_assign, + zigbee_new_attr_list, + zigbee_new_cluster_list, + zigbee_new_variable, + zigbee_register_ep, + zigbee_set_string, +) +from ..const import ( + CONF_BASIC_ATTRIB_LIST_EXT, + CONF_BINARY_ATTRS, + CONF_BINARY_INPUT_ATTRIB_LIST, + CONF_BINARY_INPUT_CLUSTER_LIST, + CONF_BINARY_INPUT_EP, + CONF_GROUPS_ATTRIB_LIST, + CONF_IDENTIFY_ATTRIB_LIST, + CONF_SCENES_ATTRIB_LIST, + CONF_ZIGBEE_ID, + BinaryAttrs, + esphome_zb_ha_declare_binary_input_ep, + zigbee_ns, +) + +AUTO_LOAD = ["zigbee"] + +ZigbeeBinarySensor = zigbee_ns.class_( + "ZigbeeBinarySensor", binary_sensor.BinarySensor, cg.Component +) +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(ZigbeeBinarySensor) + .extend( + { + cv.GenerateID(CONF_BINARY_ATTRS): cv.declare_id(BinaryAttrs), + cv.GenerateID(CONF_BINARY_INPUT_ATTRIB_LIST): cv.declare_id( + cg.global_ns.namespace( + "ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST" + ) + ), + cv.GenerateID(CONF_BINARY_INPUT_CLUSTER_LIST): cv.declare_id( + cg.global_ns.namespace( + "ESPHOME_ZB_HA_DECLARE_BINARY_INPUT_CLUSTER_LIST" + ) + ), + cv.GenerateID(CONF_BINARY_INPUT_EP): cv.declare_id( + esphome_zb_ha_declare_binary_input_ep + ), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(ZigbeeBaseSchema) +) + + +@coroutine_with_priority(50.0) +async def to_code(config): + binary_attrs = zigbee_new_variable(config[CONF_BINARY_ATTRS]) + attr_list = zigbee_new_attr_list( + config[CONF_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]), + ) + + cluster = zigbee_new_cluster_list( + config[CONF_BINARY_INPUT_CLUSTER_LIST], + attr_list, + config[CONF_BASIC_ATTRIB_LIST_EXT], + config[CONF_IDENTIFY_ATTRIB_LIST], + config[CONF_GROUPS_ATTRIB_LIST], + config[CONF_SCENES_ATTRIB_LIST], + ) + + ep = zigbee_register_ep(config[CONF_BINARY_INPUT_EP], cluster) + + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(bool) + ) + cg.add(var.set_template(template_)) + + cg.add(var.set_ep(ep)) + cg.add(var.set_cluster_attributes(binary_attrs)) + hub = await cg.get_variable(config[CONF_ZIGBEE_ID]) + cg.add(var.set_parent(hub)) + + +@automation.register_action( + "binary_sensor.zigbee.publish", + binary_sensor.BinarySensorPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } + ), +) +async def binary_sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_STATE], args, bool) + cg.add(var.set_state(template_)) + return var diff --git a/esphome/components/zigbee/binary_sensor/zigbee_binary_sensor.cpp b/esphome/components/zigbee/binary_sensor/zigbee_binary_sensor.cpp new file mode 100644 index 0000000000..3b44eb1001 --- /dev/null +++ b/esphome/components/zigbee/binary_sensor/zigbee_binary_sensor.cpp @@ -0,0 +1,58 @@ +#include "zigbee_binary_sensor.h" +#ifdef USE_ZIGBEE +#include "esphome/core/log.h" +extern "C" { +#include +#include +#include +#include +#include +} +namespace esphome { +namespace zigbee { + +static const char *const TAG = "zigbee.binary_sensor"; + +void ZigbeeBinarySensor::setup() { + add_on_state_callback([this](bool state) { + if (state) { + cluster_attributes_->present_value = 1; + } else { + cluster_attributes_->present_value = 0; + } + ESP_LOGD(TAG, "set attribute ep: %d, present_value %d", ep_, cluster_attributes_->present_value); + ZB_ZCL_SET_ATTRIBUTE(ep_, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, &cluster_attributes_->present_value, ZB_FALSE); + this->parent_->flush(); + }); + + if (!this->publish_initial_state_) + return; + + if (this->f_ != nullptr) { + this->publish_initial_state(this->f_().value_or(false)); + } else { + this->publish_initial_state(false); + } +} + +void ZigbeeBinarySensor::loop() { + if (this->f_ == nullptr) + return; + + auto s = this->f_(); + if (s.has_value()) { + this->publish_state(*s); + } +} + +void ZigbeeBinarySensor::dump_config() { + LOG_BINARY_SENSOR("", "Zigbee Binary Sensor", this); + ESP_LOGCONFIG(TAG, " EP: %d", ep_); +} + +void ZigbeeBinarySensor::set_parent(Zigbee *parent) { this->parent_ = parent; } + +} // namespace zigbee +} // namespace esphome +#endif diff --git a/esphome/components/zigbee/binary_sensor/zigbee_binary_sensor.h b/esphome/components/zigbee/binary_sensor/zigbee_binary_sensor.h new file mode 100644 index 0000000000..a7e810c1ea --- /dev/null +++ b/esphome/components/zigbee/binary_sensor/zigbee_binary_sensor.h @@ -0,0 +1,96 @@ +#pragma once + +#include "esphome/components/zigbee/zigbee_component.h" +#ifdef USE_ZIGBEE +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +extern "C" { +#include +#include +} + +#define ESPHOME_ZB_HA_DECLARE_BINARY_INPUT_CLUSTER_LIST(cluster_list_name, binary_attr_list, basic_attr_list, \ + identify_attr_list, groups_attr_list, scenes_attr_list) \ + zb_zcl_cluster_desc_t cluster_list_name[] = { \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_IDENTIFY, ZB_ZCL_ARRAY_SIZE(identify_attr_list, zb_zcl_attr_t), \ + (identify_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID), \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_BASIC, ZB_ZCL_ARRAY_SIZE(basic_attr_list, zb_zcl_attr_t), \ + (basic_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID), \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_ARRAY_SIZE(binary_attr_list, zb_zcl_attr_t), \ + (binary_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID), \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_GROUPS, ZB_ZCL_ARRAY_SIZE(groups_attr_list, zb_zcl_attr_t), \ + (groups_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID), \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_SCENES, ZB_ZCL_ARRAY_SIZE(scenes_attr_list, zb_zcl_attr_t), \ + (scenes_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID)} + +#define ESPHOME_ZB_HA_DEVICE_VER_SIMPLE_SENSOR 0 + +#define ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_SIMPLE_DESC(ep_name, ep_id, in_clust_num, out_clust_num) \ + 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_CUSTOM_ATTR_DEVICE_ID, \ + ESPHOME_ZB_HA_DEVICE_VER_SIMPLE_SENSOR, \ + 0, \ + in_clust_num, \ + out_clust_num, \ + {ZB_ZCL_CLUSTER_ID_BASIC, ZB_ZCL_CLUSTER_ID_IDENTIFY, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, \ + ZB_ZCL_CLUSTER_ID_SCENES, ZB_ZCL_CLUSTER_ID_GROUPS}} + +#define ESPHOME_ZB_HA_BINARY_INPUT_REPORT_ATTR_COUNT ZB_ZCL_BINARY_INPUT_REPORT_ATTR_COUNT +#define ESPHOME_ZB_HA_BINARY_INPUT_IN_CLUSTER_NUM 5 // server roles in ESPHOME_ZB_HA_DECLARE_BINARY_INPUT_CLUSTER_LIST +#define ESPHOME_ZB_HA_BINARY_INPUT_OUT_CLUSTER_NUM 0 // client roles in ESPHOME_ZB_HA_DECLARE_BINARY_INPUT_CLUSTER_LIST + +#define ESPHOME_ZB_HA_DECLARE_BINARY_INPUT_EP(ep_name, ep_id, cluster_list) \ + ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_SIMPLE_DESC(ep_name, ep_id, ESPHOME_ZB_HA_BINARY_INPUT_IN_CLUSTER_NUM, \ + ESPHOME_ZB_HA_BINARY_INPUT_OUT_CLUSTER_NUM); \ + ZBOSS_DEVICE_DECLARE_REPORTING_CTX(reporting_info##ep_name, ESPHOME_ZB_HA_BINARY_INPUT_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, \ + (zb_af_simple_desc_1_1_t *) &simple_desc_##ep_name, \ + ESPHOME_ZB_HA_BINARY_INPUT_REPORT_ATTR_COUNT, reporting_info##ep_name, 0, NULL) + +// it cannot have ESPHOME prefix since it is used outside +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \ + description) \ + ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_BINARY_INPUT) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_OUT_OF_SERVICE_ID, (out_of_service)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, (present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_STATUS_FLAG_ID, (status_flag)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID, (description)) \ + ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST + +namespace esphome { +namespace zigbee { + +class ZigbeeBinarySensor : public Component, public binary_sensor::BinarySensor { + public: + void set_template(std::function()> &&f) { this->f_ = f; } + + void setup() override; + void loop() override; + void dump_config() override; + + void set_parent(Zigbee *parent); + void set_ep(zb_uint8_t ep) { this->ep_ = ep; } + void set_cluster_attributes(BinaryAttrs &cluster_attributes) { this->cluster_attributes_ = &cluster_attributes; } + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + std::function()> f_{nullptr}; + zb_uint8_t ep_{0}; + Zigbee *parent_{nullptr}; + BinaryAttrs *cluster_attributes_{nullptr}; +}; + +} // namespace zigbee +} // namespace esphome +#endif diff --git a/esphome/components/zigbee/const.py b/esphome/components/zigbee/const.py new file mode 100644 index 0000000000..43b361bbd0 --- /dev/null +++ b/esphome/components/zigbee/const.py @@ -0,0 +1,61 @@ +import esphome.codegen as cg + +zigbee_ns = cg.esphome_ns.namespace("zigbee") +Zigbee = zigbee_ns.class_("Zigbee", cg.Component) + +zb_char_t_ptr = cg.global_ns.namespace("zb_char_t *") + +CONF_ZIGBEE_ID = "zigbee_id" +CONF_SWITCH = "switch" +CONF_MAX_EP_NUMBER = 8 + +zb_zcl_basic_attrs_ext_t = cg.global_ns.namespace("zb_zcl_basic_attrs_ext_t") +zb_zcl_identify_attrs_t = cg.global_ns.namespace("zb_zcl_identify_attrs_t") +zb_zcl_groups_attrs_t = cg.global_ns.namespace("zb_zcl_groups_attrs_t") +zb_zcl_scenes_attrs_t = cg.global_ns.namespace("zb_zcl_scenes_attrs_t") + +CONF_BASIC_ATTRS_EXT = "basic_attrs_ext" +CONF_IDENTIFY_ATTRS = "identify_attrs" +CONF_GROUPS_ATTRS = "groups_attrs" +CONF_SCENES_ATTRS = "scenes_attrs" +CONF_BINARY_ATTRS = "binary_attrs" + +CONF_BASIC_ATTRIB_LIST_EXT = "basic_attrib_list_ext" +CONF_IDENTIFY_ATTRIB_LIST = "identify_attrib_list" +CONF_GROUPS_ATTRIB_LIST = "groups_attrib_list" +CONF_SCENES_ATTRIB_LIST = "scenes_attrib_list" + + +# it has to be class to make use_id work +ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT = cg.global_ns.class_( + "ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT" +) +ZB_ZCL_DECLARE_IDENTIFY_ATTRIB_LIST = cg.global_ns.class_( + "ZB_ZCL_DECLARE_IDENTIFY_ATTRIB_LIST" +) +ZB_ZCL_DECLARE_GROUPS_ATTRIB_LIST = cg.global_ns.class_( + "ZB_ZCL_DECLARE_GROUPS_ATTRIB_LIST" +) +ZB_ZCL_DECLARE_SCENES_ATTRIB_LIST = cg.global_ns.class_( + "ZB_ZCL_DECLARE_SCENES_ATTRIB_LIST" +) + +# input/output +BinaryAttrs = zigbee_ns.struct("BinaryAttrs") + +# input +CONF_BINARY_INPUT_ATTRIB_LIST = "binary_input_attrib_list" +CONF_BINARY_INPUT_CLUSTER_LIST = "binary_input_cluster_list" +CONF_BINARY_INPUT_EP = "binary_input_ep" +esphome_zb_ha_declare_binary_input_ep = cg.global_ns.namespace( + "ESPHOME_ZB_HA_DECLARE_BINARY_INPUT_EP" +) + +# output +CONF_BINARY_OUTPUT_ATTRIB_LIST = "binary_output_attrib_list" +CONF_BINARY_OUTPUT_CLUSTER_LIST = "binary_output_cluster_list" + +CONF_BINARY_OUTPUT_EP = "binary_output_ep" +esphome_zb_ha_declare_binary_output_ep = cg.global_ns.namespace( + "ESPHOME_ZB_HA_DECLARE_BINARY_OUTPUT_EP" +) diff --git a/esphome/components/zigbee/switch/__init__.py b/esphome/components/zigbee/switch/__init__.py new file mode 100644 index 0000000000..3a3cb9d91c --- /dev/null +++ b/esphome/components/zigbee/switch/__init__.py @@ -0,0 +1,110 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import output, switch +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_NAME, CONF_OUTPUT, CONF_STATE +from esphome.core import coroutine_with_priority + +from .. import ( + ZigbeeBaseSchema, + zigbee_assign, + zigbee_new_attr_list, + zigbee_new_cluster_list, + zigbee_new_variable, + zigbee_register_ep, + zigbee_set_string, +) +from ..const import ( + CONF_BASIC_ATTRIB_LIST_EXT, + CONF_BINARY_ATTRS, + CONF_BINARY_OUTPUT_ATTRIB_LIST, + CONF_BINARY_OUTPUT_CLUSTER_LIST, + CONF_BINARY_OUTPUT_EP, + CONF_GROUPS_ATTRIB_LIST, + CONF_IDENTIFY_ATTRIB_LIST, + CONF_SCENES_ATTRIB_LIST, + CONF_ZIGBEE_ID, + BinaryAttrs, + esphome_zb_ha_declare_binary_output_ep, + zigbee_ns, +) + +AUTO_LOAD = ["zigbee"] + +ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", switch.Switch, cg.Component) + +CONFIG_SCHEMA = ( + switch.switch_schema(ZigbeeSwitch) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.GenerateID(CONF_BINARY_ATTRS): cv.declare_id(BinaryAttrs), + cv.GenerateID(CONF_BINARY_OUTPUT_ATTRIB_LIST): cv.declare_id( + cg.global_ns.namespace( + "ESPHOME_ZB_ZCL_DECLARE_BINARY_OUTPUT_ATTRIB_LIST" + ) + ), + cv.GenerateID(CONF_BINARY_OUTPUT_CLUSTER_LIST): cv.declare_id( + cg.global_ns.namespace( + "ESPHOME_ZB_HA_DECLARE_BINARY_OUTPUT_CLUSTER_LIST" + ) + ), + cv.GenerateID(CONF_BINARY_OUTPUT_EP): cv.declare_id( + esphome_zb_ha_declare_binary_output_ep + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(ZigbeeBaseSchema) +) + + +@coroutine_with_priority(50.0) +async def to_code(config): + binary_attrs = zigbee_new_variable(config[CONF_BINARY_ATTRS]) + attr_list = zigbee_new_attr_list( + config[CONF_BINARY_OUTPUT_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]), + ) + + cluster = zigbee_new_cluster_list( + config[CONF_BINARY_OUTPUT_CLUSTER_LIST], + attr_list, + config[CONF_BASIC_ATTRIB_LIST_EXT], + config[CONF_IDENTIFY_ATTRIB_LIST], + config[CONF_GROUPS_ATTRIB_LIST], + config[CONF_SCENES_ATTRIB_LIST], + ) + + ep = zigbee_register_ep(config[CONF_BINARY_OUTPUT_EP], cluster) + + var = await switch.new_switch(config) + await cg.register_component(var, config) + + output_ = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(output_)) + cg.add(var.set_ep(ep)) + cg.add(var.set_cluster_attributes(binary_attrs)) + hub = await cg.get_variable(config[CONF_ZIGBEE_ID]) + cg.add(var.set_parent(hub)) + + +@automation.register_action( + "switch.zigbee.publish", + switch.SwitchPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(switch.Switch), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } + ), +) +async def switch_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_STATE], args, bool) + cg.add(var.set_state(template_)) + return var diff --git a/esphome/components/zigbee/switch/zcl_binary_output.cpp b/esphome/components/zigbee/switch/zcl_binary_output.cpp new file mode 100644 index 0000000000..b891adbed2 --- /dev/null +++ b/esphome/components/zigbee/switch/zcl_binary_output.cpp @@ -0,0 +1,47 @@ +#include "esphome/core/defines.h" +#ifdef USE_ZIGBEE +extern "C" { +#include "zboss_api.h" +#include "zcl/zb_zcl_common.h" +} +#include "zigbee_switch.h" + +static zb_ret_t check_value_binary_output_server(zb_uint16_t attr_id, zb_uint8_t endpoint, zb_uint8_t *value); + +void zb_zcl_binary_output_init_server(void) { + zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + check_value_binary_output_server, (zb_zcl_cluster_write_attr_hook_t) NULL, + (zb_zcl_cluster_handler_t) NULL); +} + +void zb_zcl_binary_output_init_client(void) { + zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, 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); +} + +#define ZB_ZCL_BINARY_OUTPUT_STATUS_FLAG_MAX_VALUE 0x0F + +static zb_ret_t check_value_binary_output_server(zb_uint16_t attr_id, zb_uint8_t endpoint, zb_uint8_t *value) { + zb_ret_t ret = RET_OK; + ZVUNUSED(endpoint); + + switch (attr_id) { + case ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID: + case ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID: + ret = ZB_ZCL_CHECK_BOOL_VALUE(*value) ? RET_OK : RET_ERROR; + break; + + case ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID: + if (*value > ZB_ZCL_BINARY_OUTPUT_STATUS_FLAG_MAX_VALUE) { + ret = RET_ERROR; + } + break; + + default: + break; + } + + return ret; +} +#endif diff --git a/esphome/components/zigbee/switch/zigbee_switch.cpp b/esphome/components/zigbee/switch/zigbee_switch.cpp new file mode 100644 index 0000000000..57d70cee32 --- /dev/null +++ b/esphome/components/zigbee/switch/zigbee_switch.cpp @@ -0,0 +1,93 @@ +#include "zigbee_switch.h" +#ifdef USE_ZIGBEE +#include "esphome/core/log.h" +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +namespace esphome { +namespace zigbee { + +static const char *const TAG = "zigbee_on_off.switch"; + +void ZigbeeSwitch::dump_config() { + LOG_SWITCH("", "Zigbee Switch", this); + ESP_LOGCONFIG(TAG, " EP: %d", ep_); +} + +void ZigbeeSwitch::setup() { + add_on_state_callback([this](bool state) { + if (state) { + cluster_attributes_->present_value = 1; + } else { + cluster_attributes_->present_value = 0; + } + ESP_LOGD(TAG, "set attribute ep: %d, present_value %d", ep_, cluster_attributes_->present_value); + ZB_ZCL_SET_ATTRIBUTE(ep_, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, &cluster_attributes_->present_value, ZB_FALSE); + this->parent_->flush(); + }); + + bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); + + if (initial_state) { + this->turn_on(); + } else { + this->turn_off(); + } +} + +void ZigbeeSwitch::write_state(bool state) { + if (state) { + this->output_->turn_on(); + } else { + this->output_->turn_off(); + } + this->publish_state(state); +} + +void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) { + zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t); + zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id; + zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id; + zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; + + p_device_cb_param->status = RET_OK; + + switch (device_cb_id) { + /* ZCL set attribute value */ + case ZB_ZCL_SET_ATTR_VALUE_CB_ID: + if (cluster_id == ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT) { + uint8_t value = p_device_cb_param->cb_param.set_attr_value_param.values.data8; + ESP_LOGI(TAG, "binary output attribute setting to %hd", value); + + if (attr_id == ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID) { + write_state((zb_bool_t) value); + } + } else { + /* other clusters attribute handled here */ + ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id); + } + break; + default: + p_device_cb_param->status = RET_ERROR; + break; + } + + ESP_LOGD(TAG, "%s status: %hd", __func__, p_device_cb_param->status); +} + +void ZigbeeSwitch::set_parent(Zigbee *parent) { + this->parent_ = parent; + this->parent_->add_callback(this->ep_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); }); +} + +} // namespace zigbee +} // namespace esphome +#endif diff --git a/esphome/components/zigbee/switch/zigbee_switch.h b/esphome/components/zigbee/switch/zigbee_switch.h new file mode 100644 index 0000000000..b7c084a796 --- /dev/null +++ b/esphome/components/zigbee/switch/zigbee_switch.h @@ -0,0 +1,135 @@ +#pragma once + +#include "esphome/components/zigbee/zigbee_component.h" +#ifdef USE_ZIGBEE +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" +#include "esphome/components/output/binary_output.h" +extern "C" { +#include +#include +} + +#define ESPHOME_ZB_HA_DEVICE_VER_SIMPLE_OUTPUT 0 // TODO what to set here? +#define ZB_ZCL_BINARY_OUTPUT_CLUSTER_REVISION_DEFAULT ((zb_uint16_t) 0x0001u) + +// NOLINTNEXTLINE(readability-identifier-naming) +enum zb_zcl_binary_output_attr_e { + ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID = 0x001C, + ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID = 0x0051, + ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID = 0x0055, + ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID = 0x006F, +}; + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_BINARY_OUTPUT_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_BINARY_OUTPUT_PRESENT_VALUE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_BOOL, \ + ZB_ZCL_ATTR_ACCESS_READ_WRITE | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_BINARY_OUTPUT_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_BINARY_OUTPUT_DESCRIPTION_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ESPHOME_ZB_ZCL_DECLARE_BINARY_OUTPUT_SIMPLE_DESC(ep_name, ep_id, in_clust_num, out_clust_num) \ + 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_CUSTOM_ATTR_DEVICE_ID, \ + ESPHOME_ZB_HA_DEVICE_VER_SIMPLE_OUTPUT, \ + 0, \ + in_clust_num, \ + out_clust_num, \ + {ZB_ZCL_CLUSTER_ID_BASIC, ZB_ZCL_CLUSTER_ID_IDENTIFY, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, \ + ZB_ZCL_CLUSTER_ID_SCENES, ZB_ZCL_CLUSTER_ID_GROUPS}} + +#define ESPHOME_ZB_ZCL_DECLARE_BINARY_OUTPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \ + description) \ + ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_BINARY_OUTPUT) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID, (out_of_service)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, (present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID, (status_flag)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID, (description)) \ + ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST + +#define ESPHOME_ZB_HA_BINARY_OUTPUT_REPORT_ATTR_COUNT 2 +#define ESPHOME_ZB_HA_BINARY_OUTPUT_IN_CLUSTER_NUM \ + 5 // server roles in ESPHOME_ZB_HA_DECLARE_BINARY_OUTPUT_CLUSTER_LIST +#define ESPHOME_ZB_HA_BINARY_OUTPUT_OUT_CLUSTER_NUM \ + 0 // client roles in ESPHOME_ZB_HA_DECLARE_BINARY_OUTPUT_CLUSTER_LIST + +#define ESPHOME_ZB_HA_DECLARE_BINARY_OUTPUT_EP(ep_name, ep_id, cluster_list) \ + ESPHOME_ZB_ZCL_DECLARE_BINARY_OUTPUT_SIMPLE_DESC(ep_name, ep_id, ESPHOME_ZB_HA_BINARY_OUTPUT_IN_CLUSTER_NUM, \ + ESPHOME_ZB_HA_BINARY_OUTPUT_OUT_CLUSTER_NUM); \ + ZBOSS_DEVICE_DECLARE_REPORTING_CTX(reporting_info##ep_name, ESPHOME_ZB_HA_BINARY_OUTPUT_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, \ + (zb_af_simple_desc_1_1_t *) &simple_desc_##ep_name, \ + ESPHOME_ZB_HA_BINARY_OUTPUT_REPORT_ATTR_COUNT, reporting_info##ep_name, 0, NULL) + +#define ESPHOME_ZB_HA_DECLARE_BINARY_OUTPUT_CLUSTER_LIST(cluster_list_name, binary_attr_list, basic_attr_list, \ + identify_attr_list, groups_attr_list, scenes_attr_list) \ + zb_zcl_cluster_desc_t cluster_list_name[] = { \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_IDENTIFY, ZB_ZCL_ARRAY_SIZE(identify_attr_list, zb_zcl_attr_t), \ + (identify_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID), \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_BASIC, ZB_ZCL_ARRAY_SIZE(basic_attr_list, zb_zcl_attr_t), \ + (basic_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID), \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_ARRAY_SIZE(binary_attr_list, zb_zcl_attr_t), \ + (binary_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID), \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_GROUPS, ZB_ZCL_ARRAY_SIZE(groups_attr_list, zb_zcl_attr_t), \ + (groups_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID), \ + ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_SCENES, ZB_ZCL_ARRAY_SIZE(scenes_attr_list, zb_zcl_attr_t), \ + (scenes_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_MANUF_CODE_INVALID)} + +void zb_zcl_binary_output_init_server(); +void zb_zcl_binary_output_init_client(); + +#define ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT_SERVER_ROLE_INIT zb_zcl_binary_output_init_server +#define ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT_CLIENT_ROLE_INIT zb_zcl_binary_output_init_client + +namespace esphome { +namespace zigbee { + +class ZigbeeSwitch : public switch_::Switch, public Component { + public: + void set_output(output::BinaryOutput *output) { this->output_ = output; } + void set_cluster_attributes(BinaryAttrs &cluster_attributes) { this->cluster_attributes_ = &cluster_attributes; } + void set_ep(zb_uint8_t ep) { this->ep_ = ep; } + + void setup() override; + float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; } + void dump_config() override; + + void set_parent(Zigbee *parent); + + protected: + void write_state(bool state) override; + void zcl_device_cb_(zb_bufid_t bufid); + + output::BinaryOutput *output_; + BinaryAttrs *cluster_attributes_ = nullptr; + zb_uint8_t ep_{0}; + Zigbee *parent_{nullptr}; +}; + +} // namespace zigbee +} // namespace esphome +#endif diff --git a/esphome/components/zigbee/zigbee_component.cpp b/esphome/components/zigbee/zigbee_component.cpp new file mode 100644 index 0000000000..8cbb8f18cb --- /dev/null +++ b/esphome/components/zigbee/zigbee_component.cpp @@ -0,0 +1,161 @@ +#include "zigbee_component.h" +#ifdef USE_ZIGBEE +#include "esphome/core/log.h" +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +namespace esphome { +namespace zigbee { + +static const char *const TAG = "zigbee"; + +Zigbee *global_zigbee = nullptr; + +#define IEEE_ADDR_BUF_SIZE 17 + +void Zigbee::zboss_signal_handler_esphome(zb_bufid_t bufid) { + zb_zdo_app_signal_hdr_t *sig_hndler = NULL; + zb_zdo_app_signal_type_t sig = zb_get_app_signal(bufid, &sig_hndler); + zb_ret_t status = ZB_GET_APP_SIGNAL_STATUS(bufid); + + switch (sig) { + case ZB_ZDO_SIGNAL_SKIP_STARTUP: + ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_SKIP_STARTUP, status: %d", status); + break; + case ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY: + ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY, status: %d", status); + break; + case ZB_ZDO_SIGNAL_LEAVE: + ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_LEAVE, status: %d", status); + break; + case ZB_BDB_SIGNAL_DEVICE_REBOOT: + ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_REBOOT, status: %d", status); + if (status == RET_OK) { + // this is from wrong thread + this->join_trigger_->trigger(); + } + break; + case ZB_BDB_SIGNAL_STEERING: + break; + case ZB_COMMON_SIGNAL_CAN_SLEEP: + ESP_LOGV(TAG, "ZB_COMMON_SIGNAL_CAN_SLEEP, status: %d", status); + break; + case ZB_BDB_SIGNAL_DEVICE_FIRST_START: + ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_FIRST_START, status: %d", status); + break; + case ZB_NLME_STATUS_INDICATION: + ESP_LOGD(TAG, "ZB_NLME_STATUS_INDICATION, status: %d", status); + break; + default: + ESP_LOGD(TAG, "zboss_signal_handler sig: %d, status: %d", sig, status); + break; + } + + auto err = zigbee_default_signal_handler(bufid); + if (err != RET_OK) { + ESP_LOGE(TAG, "zigbee_default_signal_handler ERROR %u [%s]", err, zb_error_to_string_get(err)); + } + + switch (sig) { + case ZB_BDB_SIGNAL_STEERING: + ESP_LOGD(TAG, "ZB_BDB_SIGNAL_STEERING, status: %d", status); + if (status == RET_OK) { + zb_ext_pan_id_t extended_pan_id; + char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0}; + int addr_len; + + zb_get_extended_pan_id(extended_pan_id); + addr_len = ieee_addr_to_str(ieee_addr_buf, sizeof(ieee_addr_buf), extended_pan_id); + + for (int i = 0; i < addr_len; ++i) { + if (ieee_addr_buf[i] != '0') { + // called from wrong thread + this->join_trigger_->trigger(); + break; + } + } + } + break; + } + + /* All callbacks should either reuse or free passed buffers. + * If bufid == 0, the buffer is invalid (not passed). + */ + if (bufid) { + zb_buf_free(bufid); + } +} + +void Zigbee::zcl_device_cb(zb_bufid_t bufid) { + zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t); + zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id; + zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id; + zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; + auto endpoint = p_device_cb_param->endpoint; + + ESP_LOGI(TAG, "zcl_device_cb %s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, + attr_id, endpoint); + + auto cb = global_zigbee->callbacks_.find(endpoint); + if (cb != global_zigbee->callbacks_.end()) { + cb->second(bufid); + return; + } + p_device_cb_param->status = RET_ERROR; +} + +void Zigbee::setup() { + global_zigbee = this; + auto err = settings_subsys_init(); + if (err) { + ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err); + return; + } + + /* Register callback for handling ZCL commands. */ + ZB_ZCL_REGISTER_DEVICE_CB(zcl_device_cb); + + /* Settings should be loaded after zcl_scenes_init */ + err = settings_load(); + if (err) { + ESP_LOGE(TAG, "Cannot load settings, err: %d", err); + return; + } + + /* Start Zigbee default thread */ + zigbee_enable(); +} + +static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) { + ESP_LOGD(TAG, "force zboss scheduler to wake and send attribute report"); + zb_buf_free(bufid); +} + +void Zigbee::flush() { need_flush_ = true; } + +void Zigbee::loop() { + if (need_flush_) { + need_flush_ = false; + zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0); + } +} + +void Zigbee::factory_reset() { + ESP_LOGD(TAG, "factory reset"); + ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0); +} + +} // namespace zigbee +} // namespace esphome + +extern "C" void zboss_signal_handler(zb_bufid_t bufid) { + esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(bufid); +} +#endif diff --git a/esphome/components/zigbee/zigbee_component.h b/esphome/components/zigbee/zigbee_component.h new file mode 100644 index 0000000000..d8f2029f06 --- /dev/null +++ b/esphome/components/zigbee/zigbee_component.h @@ -0,0 +1,65 @@ +#pragma once +#include +#include "esphome/core/defines.h" +#ifdef USE_ZIGBEE +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +extern "C" { +#include +#include +} + +#define ESPHOME_ZB_DECLARE_SIMPLE_DESC(ep_name, in_clusters_count, out_clusters_count) \ + typedef ZB_PACKED_PRE struct zb_af_simple_desc_##ep_name##_##out_clusters_count##_##out_clusters_count##_s { \ + zb_uint8_t endpoint; /* Endpoint */ \ + zb_uint16_t app_profile_id; /* Application profile identifier */ \ + zb_uint16_t app_device_id; /* Application device identifier */ \ + zb_bitfield_t app_device_version : 4; /* Application device version */ \ + zb_bitfield_t reserved : 4; /* Reserved */ \ + zb_uint8_t app_input_cluster_count; /* Application input cluster count */ \ + zb_uint8_t app_output_cluster_count; /* Application output cluster count */ \ + /* Application input and output cluster list */ \ + zb_uint16_t app_cluster_list[(in_clusters_count) + (out_clusters_count)]; \ + } ZB_PACKED_STRUCT zb_af_simple_desc_##ep_name##_##in_clusters_count##_##out_clusters_count##_t + +#define ESPHOME_CAT7(a, b, c, d, e, f, g) a##b##c##d##e##f##g +#define ESPHOME_ZB_AF_SIMPLE_DESC_TYPE(ep_name, in_num, out_num) \ + ESPHOME_CAT7(zb_af_simple_desc_, ep_name, _, in_num, _, out_num, _t) + +namespace esphome { +namespace zigbee { + +struct BinaryAttrs { + zb_bool_t out_of_service; + zb_bool_t present_value; + zb_uint8_t status_flags; + zb_uchar_t description[32]; // TODO it could be in progmem, max is ZB_ZCL_MAX_STRING_SIZE +}; + +class Zigbee : public Component { + public: + void setup() override; + void add_callback(zb_uint8_t endpoint, std::function cb) { callbacks_[endpoint] = cb; } + void zboss_signal_handler_esphome(zb_bufid_t bufid); + void factory_reset(); + Trigger<> *get_join_trigger() const { return this->join_trigger_; }; + void flush(); + void loop() override; + + protected: + static void zcl_device_cb(zb_bufid_t bufid); + std::map> callbacks_; + Trigger<> *join_trigger_{new Trigger<>()}; + bool need_flush_{false}; +}; + +extern Zigbee *global_zigbee; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +template class FactoryResetAction : public Action { + public: + void play(Ts... x) override { global_zigbee->factory_reset(); } +}; + +} // namespace zigbee +} // namespace esphome +#endif diff --git a/esphome/components/zigbee_ctx/__init__.py b/esphome/components/zigbee_ctx/__init__.py new file mode 100644 index 0000000000..6fbda70056 --- /dev/null +++ b/esphome/components/zigbee_ctx/__init__.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +from esphome.core import CORE, coroutine_with_priority + +KEY_ZIGBEE = "zigbee" +KEY_EP = "ep" + + +def zigbee_set_core_data(config): + CORE.data[KEY_ZIGBEE] = {} + CORE.data[KEY_ZIGBEE][KEY_EP] = [] + return config + + +@coroutine_with_priority(10.0) +async def to_code(config): + if len(CORE.data[KEY_ZIGBEE][KEY_EP]) > 0: + cg.add_global( + cg.RawExpression( + f"ZBOSS_DECLARE_DEVICE_CTX_EP_VA(zb_device_ctx, &{', &'.join(CORE.data[KEY_ZIGBEE][KEY_EP])})" + ) + ) + cg.add(cg.RawExpression("ZB_AF_REGISTER_DEVICE_CTX(&zb_device_ctx)")) diff --git a/tests/components/zigbee/common.yaml b/tests/components/zigbee/common.yaml new file mode 100644 index 0000000000..ac87999ee6 --- /dev/null +++ b/tests/components/zigbee/common.yaml @@ -0,0 +1,41 @@ +--- +switch: + - platform: zigbee + output: output_template + id: zigbee_switch_1 + - platform: zigbee + output: output_template + id: zigbee_switch_2 + +output: + - platform: template + id: output_template + type: binary + write_action: + - binary_sensor.zigbee.publish: + id: zigbee_binary_sensor_1 + state: ON + - binary_sensor.zigbee.publish: + id: zigbee_binary_sensor_1 + state: OFF + - platform: template + id: output_factory + type: binary + write_action: + - zigbee.factory_reset + +binary_sensor: + - platform: zigbee + id: zigbee_binary_sensor_1 + - platform: zigbee + id: zigbee_binary_sensor_2 + lambda: return true; + +zigbee: + on_join: + - switch.zigbee.publish: + id: zigbee_switch_1 + state: OFF + - switch.zigbee.publish: + id: zigbee_switch_1 + state: ON diff --git a/tests/components/zigbee/test.nrf52-adafruit.yaml b/tests/components/zigbee/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/zigbee/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/zigbee/test.nrf52-mcumgr.yaml b/tests/components/zigbee/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/zigbee/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml