diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index f53a0a7cf7..8a78186178 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -27,6 +27,9 @@ void RealTimeClock::dump_config() { #ifdef USE_TIME_TIMEZONE ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); #endif + auto time = this->now(); + ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); } void RealTimeClock::synchronize_epoch_(uint32_t epoch) { diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 2281dd38a9..8179220507 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -120,7 +120,7 @@ async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None: def consume_endpoint(config: ConfigType) -> ConfigType: if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): return config - if " " in config[CONF_NAME]: + if CONF_NAME in config and " " in config[CONF_NAME]: _LOGGER.warning( "Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'", config[CONF_NAME], diff --git a/esphome/components/zigbee/time/__init__.py b/esphome/components/zigbee/time/__init__.py new file mode 100644 index 0000000000..82f94c8372 --- /dev/null +++ b/esphome/components/zigbee/time/__init__.py @@ -0,0 +1,86 @@ +import esphome.codegen as cg +from esphome.components import time as time_ +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE +from esphome.types import ConfigType + +from .. import consume_endpoint +from ..const_zephyr import CONF_ZIGBEE_ID, zigbee_ns +from ..zigbee_zephyr import ( + ZigbeeClusterDesc, + ZigbeeComponent, + get_slot_index, + zigbee_new_attr_list, + zigbee_new_cluster_list, + zigbee_new_variable, + zigbee_register_ep, +) + +DEPENDENCIES = ["zigbee"] + +ZigbeeTime = zigbee_ns.class_("ZigbeeTime", time_.RealTimeClock) + +CONFIG_SCHEMA = cv.All( + time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ZigbeeTime), + cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id( + ZigbeeComponent + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("1s")), + consume_endpoint, +) + + +async def to_code(config: ConfigType) -> None: + CORE.add_job(_add_time, config) + + +async def _add_time(config: ConfigType) -> None: + slot_index = get_slot_index() + + # Create unique names for this sensor's variables based on slot index + prefix = f"zigbee_ep{slot_index + 1}" + attrs_name = f"{prefix}_time_attrs" + attr_list_name = f"{prefix}_time_attrib_list" + cluster_list_name = f"{prefix}_cluster_list" + ep_name = f"{prefix}_ep" + + # Create the binary attributes structure + time_attrs = zigbee_new_variable(attrs_name, "zb_zcl_time_attrs_t") + attr_list = zigbee_new_attr_list( + attr_list_name, + "ZB_ZCL_DECLARE_TIME_ATTR_LIST", + str(time_attrs), + ) + + # Create cluster list and register endpoint + cluster_list_name, clusters = zigbee_new_cluster_list( + cluster_list_name, + [ + ZigbeeClusterDesc("ZB_ZCL_CLUSTER_ID_TIME", attr_list), + ZigbeeClusterDesc("ZB_ZCL_CLUSTER_ID_TIME"), + ], + ) + zigbee_register_ep( + ep_name, + cluster_list_name, + 0, + clusters, + slot_index, + "ZB_HA_CUSTOM_ATTR_DEVICE_ID", + ) + + # Create the ZigbeeTime component + var = cg.new_Pvariable(config[CONF_ID]) + await time_.register_time(var, config) + await cg.register_component(var, config) + + cg.add(var.set_endpoint(slot_index + 1)) + cg.add(var.set_cluster_attributes(time_attrs)) + hub = await cg.get_variable(config[CONF_ZIGBEE_ID]) + cg.add(var.set_parent(hub)) diff --git a/esphome/components/zigbee/time/zigbee_time_zephyr.cpp b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp new file mode 100644 index 0000000000..70ceb60abe --- /dev/null +++ b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp @@ -0,0 +1,87 @@ +#include "zigbee_time_zephyr.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME) +#include "esphome/core/log.h" + +namespace esphome::zigbee { + +static const char *const TAG = "zigbee.time"; + +// This time standard is the number of +// seconds since 0 hrs 0 mins 0 sec on 1st January 2000 UTC (Universal Coordinated Time). +constexpr time_t EPOCH_2000 = 946684800; + +ZigbeeTime *global_time = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +void ZigbeeTime::sync_time(zb_ret_t status, zb_uint32_t auth_level, zb_uint16_t short_addr, zb_uint8_t endpoint, + zb_uint32_t nw_time) { + if (status == RET_OK && auth_level >= ZB_ZCL_TIME_HAS_SYNCHRONIZED_BIT) { + global_time->set_epoch_time(nw_time + EPOCH_2000); + } else if (status != RET_TIMEOUT || !global_time->has_time_) { + ESP_LOGE(TAG, "Status: %d, auth_level: %u, short_addr: %d, endpoint: %d, nw_time: %u", status, auth_level, + short_addr, endpoint, nw_time); + } +} + +void ZigbeeTime::setup() { + global_time = this; + this->parent_->add_callback(this->endpoint_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); }); + synchronize_epoch_(EPOCH_2000); + this->parent_->add_join_callback([this]() { zb_zcl_time_server_synchronize(this->endpoint_, sync_time); }); +} + +void ZigbeeTime::dump_config() { + ESP_LOGCONFIG(TAG, + "Zigbee Time\n" + " Endpoint: %d", + this->endpoint_); + RealTimeClock::dump_config(); +} + +void ZigbeeTime::update() { + time_t time = timestamp_now(); + this->cluster_attributes_->time = time - EPOCH_2000; +} + +void ZigbeeTime::set_epoch_time(uint32_t epoch) { + this->defer([this, epoch]() { + this->synchronize_epoch_(epoch); + this->has_time_ = true; + }); +} + +void ZigbeeTime::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; + + switch (device_cb_id) { + /* ZCL set attribute value */ + case ZB_ZCL_SET_ATTR_VALUE_CB_ID: + if (cluster_id == ZB_ZCL_CLUSTER_ID_TIME) { + if (attr_id == ZB_ZCL_ATTR_TIME_TIME_ID) { + zb_uint32_t value = p_device_cb_param->cb_param.set_attr_value_param.values.data32; + ESP_LOGI(TAG, "Synchronize time to %u", value); + this->defer([this, value]() { synchronize_epoch_(value + EPOCH_2000); }); + } else if (attr_id == ZB_ZCL_ATTR_TIME_TIME_STATUS_ID) { + zb_uint8_t value = p_device_cb_param->cb_param.set_attr_value_param.values.data8; + ESP_LOGI(TAG, "Time status %hd", value); + this->defer([this, value]() { this->has_time_ = ZB_ZCL_TIME_TIME_STATUS_SYNCHRONIZED_BIT_IS_SET(value); }); + } + } else { + /* other clusters attribute handled here */ + ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id); + p_device_cb_param->status = RET_NOT_IMPLEMENTED; + } + break; + default: + p_device_cb_param->status = RET_NOT_IMPLEMENTED; + break; + } + + ESP_LOGD(TAG, "Zcl_device_cb_ status: %hd", p_device_cb_param->status); +} + +} // namespace esphome::zigbee + +#endif diff --git a/esphome/components/zigbee/time/zigbee_time_zephyr.h b/esphome/components/zigbee/time/zigbee_time_zephyr.h new file mode 100644 index 0000000000..3c2adc4b5f --- /dev/null +++ b/esphome/components/zigbee/time/zigbee_time_zephyr.h @@ -0,0 +1,38 @@ +#pragma once +#include "esphome/core/defines.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME) +#include "esphome/core/component.h" +#include "esphome/components/time/real_time_clock.h" +#include "esphome/components/zigbee/zigbee_zephyr.h" + +extern "C" { +#include +#include +} + +namespace esphome::zigbee { + +class ZigbeeTime : public time::RealTimeClock, public ZigbeeEntity { + public: + void setup() override; + void dump_config() override; + void update() override; + + void set_cluster_attributes(zb_zcl_time_attrs_t &cluster_attributes) { + this->cluster_attributes_ = &cluster_attributes; + } + + void set_epoch_time(uint32_t epoch); + + protected: + static void sync_time(zb_ret_t status, zb_uint32_t auth_level, zb_uint16_t short_addr, zb_uint8_t endpoint, + zb_uint32_t nw_time); + void zcl_device_cb_(zb_bufid_t bufid); + zb_zcl_time_attrs_t *cluster_attributes_{nullptr}; + + bool has_time_{false}; +}; + +} // namespace esphome::zigbee + +#endif diff --git a/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp index 8b7aff70a8..464cc04d62 100644 --- a/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp @@ -22,7 +22,7 @@ void ZigbeeBinarySensor::setup() { ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value, ZB_FALSE); - this->parent_->flush(); + this->parent_->force_report(); }); } diff --git a/esphome/components/zigbee/zigbee_sensor_zephyr.cpp b/esphome/components/zigbee/zigbee_sensor_zephyr.cpp index 74550d6487..25e1e083e0 100644 --- a/esphome/components/zigbee/zigbee_sensor_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_sensor_zephyr.cpp @@ -21,7 +21,7 @@ void ZigbeeSensor::setup() { ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID, (zb_uint8_t *) &this->cluster_attributes_->present_value, ZB_FALSE); - this->parent_->flush(); + this->parent_->force_report(); }); } diff --git a/esphome/components/zigbee/zigbee_switch_zephyr.cpp b/esphome/components/zigbee/zigbee_switch_zephyr.cpp index 5454f262f9..fef02e5a0c 100644 --- a/esphome/components/zigbee/zigbee_switch_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_switch_zephyr.cpp @@ -31,7 +31,7 @@ void ZigbeeSwitch::setup() { ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value, ZB_FALSE); - this->parent_->flush(); + this->parent_->force_report(); }); } @@ -41,8 +41,6 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) { 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: @@ -58,10 +56,11 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) { } else { /* other clusters attribute handled here */ ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id); + p_device_cb_param->status = RET_NOT_IMPLEMENTED; } break; default: - p_device_cb_param->status = RET_ERROR; + p_device_cb_param->status = RET_NOT_IMPLEMENTED; break; } diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index e43ab8f84d..eabf5c30d4 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -112,10 +112,10 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { const auto &cb = global_zigbee->callbacks_[endpoint - 1]; if (cb) { cb(bufid); + return; } - return; } - p_device_cb_param->status = RET_ERROR; + p_device_cb_param->status = RET_NOT_IMPLEMENTED; } void ZigbeeComponent::on_join_() { @@ -230,11 +230,11 @@ static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) { zb_buf_free(bufid); } -void ZigbeeComponent::flush() { this->need_flush_ = true; } +void ZigbeeComponent::force_report() { this->force_report_ = true; } void ZigbeeComponent::loop() { - if (this->need_flush_) { - this->need_flush_ = false; + if (this->force_report_) { + this->force_report_ = false; zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0); } } diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index d5f1257f9c..b75c192c1a 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -72,7 +72,7 @@ class ZigbeeComponent : public Component { void zboss_signal_handler_esphome(zb_bufid_t bufid); void factory_reset(); Trigger<> *get_join_trigger() { return &this->join_trigger_; }; - void flush(); + void force_report(); void loop() override; protected: @@ -84,7 +84,7 @@ class ZigbeeComponent : public Component { std::array, ZIGBEE_ENDPOINTS_COUNT> callbacks_{}; CallbackManager join_cb_; Trigger<> join_trigger_; - bool need_flush_{false}; + bool force_report_{false}; }; class ZigbeeEntity { diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 7f1f7dc57f..67a11d3685 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -336,14 +336,14 @@ async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None: CORE.add_job(_add_switch, entity, config) -def _slot_index() -> int: - """Find the next available endpoint slot""" +def get_slot_index() -> int: + """Find the next available endpoint slot.""" slot = next( (i for i, v in enumerate(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if v == ""), None ) if slot is None: raise cv.Invalid( - f"Not found empty slot, size ({len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])})" + f"No available Zigbee endpoint slots ({len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])} in use)" ) return slot @@ -358,7 +358,7 @@ async def _add_zigbee_ep( app_device_id: str, extra_field_values: dict[str, int] | None = None, ) -> None: - slot_index = _slot_index() + slot_index = get_slot_index() prefix = f"zigbee_ep{slot_index + 1}" attrs_name = f"{prefix}_attrs" diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6c5904ef25..9c7060cd1d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -16,7 +16,6 @@ #include #include #include - #include #include "esphome/core/optional.h" diff --git a/tests/components/zigbee/common.yaml b/tests/components/zigbee/common.yaml index 11100e1e0c..e4dee5f74a 100644 --- a/tests/components/zigbee/common.yaml +++ b/tests/components/zigbee/common.yaml @@ -8,8 +8,6 @@ binary_sensor: name: "Garage Door Open 3" - platform: template name: "Garage Door Open 4" - - platform: template - name: "Garage Door Open 5" - platform: template name: "Garage Door Internal" internal: True @@ -21,6 +19,10 @@ sensor: - platform: template name: "Analog 2" lambda: return 11.0; + - platform: template + name: "Analog 3" + lambda: return 12.0; + internal: True zigbee: wipe_on_boot: true @@ -35,6 +37,9 @@ output: write_action: - zigbee.factory_reset +time: + - platform: zigbee + switch: - platform: template name: "Template Switch"