Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions components/zigbee/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,17 @@ def require_vfs_select():
CONF_DEVICE_TYPE,
CONF_ENDPOINTS,
CONF_IDENT_TIME,
CONF_CHANNELS,
CONF_STACK_SIZE,
CONF_MANUFACTURER,
CONF_NUM,
CONF_ON_JOIN,
CONF_ON_REPORT,
CONF_REPORT,
CONF_ROLE,
CONF_ROUTER,
CONF_ON_IDENTIFY_EFFECT,
CONF_ON_CUSTOM_COMMAND,
CONF_SCALE,
BinarySensor,
Sensor,
Expand All @@ -78,6 +82,8 @@ def require_vfs_select():
ZigBeeAttribute,
ZigBeeComponent,
ZigBeeJoinTrigger,
ZigbeeIdentifyEffectTrigger,
ZigbeeCustomCommandTrigger,
ZigBeeOnReportTrigger,
ZigBeeOnValueTrigger,
)
Expand Down Expand Up @@ -249,6 +255,8 @@ def _require_vfs_select(config):
cv.Optional(CONF_VERSION, default=0): cv.int_,
cv.Optional(CONF_AREA, default=0): cv.int_, # make enum
cv.Optional(CONF_ROUTER, default=False): cv.boolean,
cv.Optional(CONF_CHANNELS): cv.string,
cv.Optional(CONF_STACK_SIZE, default=4096): cv.int_,
cv.Optional(CONF_DEBUG, default=False): cv.boolean,
cv.Optional(CONF_COMPONENTS): cv.Any(
cv.one_of("all", "none", lower=True),
Expand Down Expand Up @@ -340,6 +348,20 @@ def _require_vfs_select(config):
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ZigBeeJoinTrigger),
}
),
cv.Optional(CONF_ON_IDENTIFY_EFFECT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ZigbeeIdentifyEffectTrigger
),
}
),
cv.Optional(CONF_ON_CUSTOM_COMMAND): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ZigbeeCustomCommandTrigger
),
}
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.require_framework_version(esp_idf=cv.Version(5, 1, 2)),
Expand Down Expand Up @@ -506,6 +528,20 @@ async def to_code(config):
)
if CONF_IDENT_TIME in config:
cg.add(var.set_ident_time(config[CONF_IDENT_TIME]))
if CONF_CHANNELS in config:
# Simple mask parsing if it's a list or single channel
# For simplicity in this common contribution, we assume a single channel string for now
# or a mask. Matching user's previous usage.
channels = config[CONF_CHANNELS]
if channels.isdigit():
mask = 1 << int(channels)
else:
# Handle list-like strings or hex if needed?
# Sticking to what was tested earlier: "15" -> 1 << 15
mask = 1 << int(channels)
cg.add(var.set_channels(mask))
if CONF_STACK_SIZE in config:
cg.add(var.set_stack_size(config[CONF_STACK_SIZE]))
for ep in ep_list:
cg.add(
var.create_default_cluster(ep[CONF_NUM], DEVICE_ID[ep[CONF_DEVICE_TYPE]])
Expand Down Expand Up @@ -535,6 +571,23 @@ async def to_code(config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

for conf in config.get(CONF_ON_IDENTIFY_EFFECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.uint8, "effect_id"), (cg.uint8, "effect_variant")], conf)

for conf in config.get(CONF_ON_CUSTOM_COMMAND, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
[
(cg.uint16, "cluster_id"),
(cg.uint8, "command_id"),
(cg.uint16, "size"),
(cg.RawExpression("void *"), "value"),
],
conf,
)


ZIGBEE_ACTION_SCHEMA = cv.Schema(
{
Expand Down
17 changes: 17 additions & 0 deletions components/zigbee/automation.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ class ZigBeeJoinTrigger : public Trigger<> {
}
};

class ZigbeeIdentifyEffectTrigger : public Trigger<uint8_t, uint8_t> {
public:
explicit ZigbeeIdentifyEffectTrigger(ZigBeeComponent *parent) {
parent->on_identify_effect_callback_.add(
[this](uint8_t effect_id, uint8_t effect_variant) { this->trigger(effect_id, effect_variant); });
}
};

class ZigbeeCustomCommandTrigger : public Trigger<uint16_t, uint8_t, uint16_t, void *> {
public:
explicit ZigbeeCustomCommandTrigger(ZigBeeComponent *parent) {
parent->on_custom_command_callback_.add([this](uint16_t cluster_id, uint8_t command_id, uint16_t size, void *value) {
this->trigger(cluster_id, command_id, size, value);
});
}
};

template<typename... Ts> class ResetZigbeeAction : public Action<Ts...>, public Parented<ZigBeeComponent> {
public:
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
Expand Down
4 changes: 4 additions & 0 deletions components/zigbee/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
CONF_ROUTER = "router"
CONF_AS_GENERIC = "as_generic"
CONF_ON_REPORT = "on_report"
CONF_ON_IDENTIFY_EFFECT = "on_identify_effect"
CONF_ON_CUSTOM_COMMAND = "on_custom_command"
CONF_CHANNELS = "channels"
CONF_STACK_SIZE = "stack_size"

# dummies for upstream compatibility
binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor")
Expand Down
7 changes: 7 additions & 0 deletions components/zigbee/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
ZigBeeOnReportTrigger = zigbee_ns.class_(
"ZigBeeOnReportTrigger", automation.Trigger.template(int), cg.Component
)
ZigbeeIdentifyEffectTrigger = zigbee_ns.class_(
"ZigbeeIdentifyEffectTrigger", automation.Trigger.template(cg.uint8, cg.uint8)
)
ZigbeeCustomCommandTrigger = zigbee_ns.class_(
"ZigbeeCustomCommandTrigger",
automation.Trigger.template(cg.uint16, cg.uint8, cg.uint16, cg.RawExpression("void *")),
)
ResetZigbeeAction = zigbee_ns.class_(
"ResetZigbeeAction", automation.Action, cg.Parented.template(ZigBeeComponent)
)
Expand Down
35 changes: 33 additions & 2 deletions components/zigbee/zigbee.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) {
extended_pan_id[7], extended_pan_id[6], extended_pan_id[5], extended_pan_id[4], extended_pan_id[3],
extended_pan_id[2], extended_pan_id[1], extended_pan_id[0], esp_zb_get_pan_id(),
esp_zb_get_current_channel());
zigbeeC->channel_ = esp_zb_get_current_channel();
zigbeeC->pref_.save(&zigbeeC->channel_);
zigbeeC->on_join_callback_.call();
zigbeeC->connected_ = true;
} else {
Expand Down Expand Up @@ -268,6 +270,19 @@ static esp_err_t zb_action_handler(esp_zb_core_action_callback_id_t callback_id,
case ESP_ZB_CORE_REPORT_ATTR_CB_ID:
ret = zb_report_attribute_handler((esp_zb_zcl_report_attr_message_t *) message);
break;
case ESP_ZB_CORE_IDENTIFY_EFFECT_CB_ID: {
esp_zb_zcl_identify_effect_message_t *msg = (esp_zb_zcl_identify_effect_message_t *) message;
ESP_LOGD(TAG, "Receive Identify Effect: effect_id(0x%x), variant(0x%x)", msg->effect_id, msg->effect_variant);
zigbeeC->handle_identify_effect(msg->info.dst_endpoint, msg->effect_id, msg->effect_variant);
break;
}
case ESP_ZB_CORE_CMD_CUSTOM_CLUSTER_REQ_CB_ID: {
esp_zb_zcl_custom_cluster_command_message_t *msg = (esp_zb_zcl_custom_cluster_command_message_t *) message;
ESP_LOGD(TAG, "Receive Custom Cluster command: cluster(0x%x), command(0x%x)", msg->info.cluster,
msg->info.command.id);
zigbeeC->handle_custom_command(msg);
break;
}
case ESP_ZB_CORE_CMD_DEFAULT_RESP_CB_ID:
ESP_LOGD(TAG, "Receive Zigbee default response callback");
break;
Expand Down Expand Up @@ -297,6 +312,15 @@ void ZigBeeComponent::handle_report_attribute(uint8_t dst_endpoint, uint16_t clu
attr->second->on_report(attribute, src_address, src_endpoint);
}

void ZigBeeComponent::handle_identify_effect(uint8_t endpoint, uint8_t effect_id, uint8_t effect_variant) {
this->on_identify_effect_callback_.call(effect_id, effect_variant);
}

void ZigBeeComponent::handle_custom_command(esp_zb_zcl_custom_cluster_command_message_t *message) {
this->on_custom_command_callback_.call(message->info.cluster, message->info.command.id, message->data.size,
message->data.value);
}

void ZigBeeComponent::create_default_cluster(uint8_t endpoint_id, esp_zb_ha_standard_devices_t device_id) {
this->cluster_list_[endpoint_id] = esphome_zb_default_clusters_create(device_id);
this->endpoint_list_[endpoint_id] = device_id;
Expand Down Expand Up @@ -414,6 +438,13 @@ void ZigBeeComponent::setup() {
.radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(),
.host_config = ESP_ZB_DEFAULT_HOST_CONFIG(),
};
this->pref_ = global_preferences->make_preference<uint8_t>(2024012501UL);
if (this->pref_.load(&this->channel_)) {
if (this->channel_mask_ == ESP_ZB_PRIMARY_CHANNEL_MASK) {
this->channel_mask_ = (1 << this->channel_);
ESP_LOGD(TAG, "Loaded channel %d from preferences", this->channel_);
}
}
#ifdef CONFIG_WIFI_COEX
if (esp_coex_wifi_i154_enable() != ESP_OK) {
this->mark_failed();
Expand Down Expand Up @@ -474,7 +505,7 @@ void ZigBeeComponent::setup() {

esp_zb_core_action_handler_register(zb_action_handler);

if (esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK) != ESP_OK) {
if (esp_zb_set_primary_network_channel_set(this->channel_mask_) != ESP_OK) {
ESP_LOGE(TAG, "Could not setup Zigbee");
this->mark_failed();
return;
Expand All @@ -488,7 +519,7 @@ void ZigBeeComponent::setup() {
reporting_info.attr_id, reporting_info.cluster_id, reporting_info.ep);
}
}
xTaskCreate(esp_zb_task_, "Zigbee_main", 4096, NULL, 24, NULL);
xTaskCreate(esp_zb_task_, "Zigbee_main", this->stack_size_, NULL, 24, NULL);
}

void ZigBeeComponent::dump_config() {
Expand Down
11 changes: 11 additions & 0 deletions components/zigbee/zigbee.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include "zigbee_helpers.h"

#ifdef USE_ZIGBEE_TIME
Expand Down Expand Up @@ -65,6 +66,8 @@ class ZigBeeComponent : public Component {
void dump_config() override;
esp_err_t create_endpoint(uint8_t endpoint_id, esp_zb_ha_standard_devices_t device_id);
void set_ident_time(uint8_t ident_time);
void set_channels(uint32_t mask) { this->channel_mask_ = mask; }
void set_stack_size(uint32_t stack_size) { this->stack_size_ = stack_size; }
void set_basic_cluster(std::string model, std::string manufacturer, std::string date, uint8_t power,
uint8_t app_version, uint8_t stack_version, uint8_t hw_version, std::string area,
uint8_t physical_env);
Expand All @@ -79,6 +82,8 @@ class ZigBeeComponent : public Component {
void handle_attribute(esp_zb_device_cb_common_info_t info, esp_zb_zcl_attribute_t attribute);
void handle_report_attribute(uint8_t dst_endpoint, uint16_t cluster, esp_zb_zcl_attribute_t attribute,
esp_zb_zcl_addr_t src_address, uint8_t src_endpoint);
void handle_identify_effect(uint8_t endpoint, uint8_t effect_id, uint8_t effect_variant);
void handle_custom_command(esp_zb_zcl_custom_cluster_command_message_t *message);
void searchBindings();
static void bindingTableCb(const esp_zb_zdo_binding_table_info_t *table_info, void *user_ctx);

Expand All @@ -101,6 +106,8 @@ class ZigBeeComponent : public Component {
bool started_ = false;

CallbackManager<void()> on_join_callback_{};
CallbackManager<void(uint8_t, uint8_t)> on_identify_effect_callback_{};
CallbackManager<void(uint16_t, uint8_t, uint16_t, void *)> on_custom_command_callback_{};
std::deque<std::tuple<ZigBeeAttribute *, esp_zb_zcl_reporting_info_t>> reporting_list;
struct {
std::string model;
Expand Down Expand Up @@ -131,6 +138,10 @@ class ZigBeeComponent : public Component {
std::map<std::tuple<uint8_t, uint16_t, uint8_t, uint16_t>, ZigBeeAttribute *> attributes_;
esp_zb_ep_list_t *esp_zb_ep_list_ = esp_zb_ep_list_create();
uint8_t ident_time_;
uint32_t channel_mask_ = ESP_ZB_PRIMARY_CHANNEL_MASK;
uint32_t stack_size_{4096};
ESPPreferenceObject pref_;
uint8_t channel_ = 0;
};

extern "C" void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct);
Expand Down