1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00

[nrf52,zigbee] Time synchronization (#12236)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
tomaszduda23
2026-01-28 16:51:17 +01:00
committed by GitHub
parent 051604f284
commit 3bd6ec4ec7
13 changed files with 238 additions and 21 deletions

View File

@@ -27,6 +27,9 @@ void RealTimeClock::dump_config() {
#ifdef USE_TIME_TIMEZONE #ifdef USE_TIME_TIMEZONE
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
#endif #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) { void RealTimeClock::synchronize_epoch_(uint32_t epoch) {

View File

@@ -120,7 +120,7 @@ async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
def consume_endpoint(config: ConfigType) -> ConfigType: def consume_endpoint(config: ConfigType) -> ConfigType:
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
return config return config
if " " in config[CONF_NAME]: if CONF_NAME in config and " " in config[CONF_NAME]:
_LOGGER.warning( _LOGGER.warning(
"Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'", "Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'",
config[CONF_NAME], config[CONF_NAME],

View File

@@ -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))

View File

@@ -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

View File

@@ -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 <zboss_api.h>
#include <zboss_api_addons.h>
}
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

View File

@@ -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_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_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value,
ZB_FALSE); ZB_FALSE);
this->parent_->flush(); this->parent_->force_report();
}); });
} }

View File

@@ -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_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID, ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID,
(zb_uint8_t *) &this->cluster_attributes_->present_value, ZB_FALSE); (zb_uint8_t *) &this->cluster_attributes_->present_value, ZB_FALSE);
this->parent_->flush(); this->parent_->force_report();
}); });
} }

View File

@@ -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_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_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value,
ZB_FALSE); 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 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; 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) { switch (device_cb_id) {
/* ZCL set attribute value */ /* ZCL set attribute value */
case ZB_ZCL_SET_ATTR_VALUE_CB_ID: case ZB_ZCL_SET_ATTR_VALUE_CB_ID:
@@ -58,10 +56,11 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) {
} else { } else {
/* other clusters attribute handled here */ /* other clusters attribute handled here */
ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id); ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id);
p_device_cb_param->status = RET_NOT_IMPLEMENTED;
} }
break; break;
default: default:
p_device_cb_param->status = RET_ERROR; p_device_cb_param->status = RET_NOT_IMPLEMENTED;
break; break;
} }

View File

@@ -112,10 +112,10 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
const auto &cb = global_zigbee->callbacks_[endpoint - 1]; const auto &cb = global_zigbee->callbacks_[endpoint - 1];
if (cb) { if (cb) {
cb(bufid); cb(bufid);
return;
} }
return;
} }
p_device_cb_param->status = RET_ERROR; p_device_cb_param->status = RET_NOT_IMPLEMENTED;
} }
void ZigbeeComponent::on_join_() { 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); zb_buf_free(bufid);
} }
void ZigbeeComponent::flush() { this->need_flush_ = true; } void ZigbeeComponent::force_report() { this->force_report_ = true; }
void ZigbeeComponent::loop() { void ZigbeeComponent::loop() {
if (this->need_flush_) { if (this->force_report_) {
this->need_flush_ = false; this->force_report_ = false;
zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0); zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0);
} }
} }

View File

@@ -72,7 +72,7 @@ class ZigbeeComponent : public Component {
void zboss_signal_handler_esphome(zb_bufid_t bufid); void zboss_signal_handler_esphome(zb_bufid_t bufid);
void factory_reset(); void factory_reset();
Trigger<> *get_join_trigger() { return &this->join_trigger_; }; Trigger<> *get_join_trigger() { return &this->join_trigger_; };
void flush(); void force_report();
void loop() override; void loop() override;
protected: protected:
@@ -84,7 +84,7 @@ class ZigbeeComponent : public Component {
std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{}; std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{};
CallbackManager<void()> join_cb_; CallbackManager<void()> join_cb_;
Trigger<> join_trigger_; Trigger<> join_trigger_;
bool need_flush_{false}; bool force_report_{false};
}; };
class ZigbeeEntity { class ZigbeeEntity {

View File

@@ -336,14 +336,14 @@ 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)
def _slot_index() -> int: def get_slot_index() -> int:
"""Find the next available endpoint slot""" """Find the next available endpoint slot."""
slot = next( slot = next(
(i for i, v in enumerate(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if v == ""), None (i for i, v in enumerate(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if v == ""), None
) )
if slot is None: if slot is None:
raise cv.Invalid( 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 return slot
@@ -358,7 +358,7 @@ async def _add_zigbee_ep(
app_device_id: str, app_device_id: str,
extra_field_values: dict[str, int] | None = None, extra_field_values: dict[str, int] | None = None,
) -> None: ) -> None:
slot_index = _slot_index() slot_index = get_slot_index()
prefix = f"zigbee_ep{slot_index + 1}" prefix = f"zigbee_ep{slot_index + 1}"
attrs_name = f"{prefix}_attrs" attrs_name = f"{prefix}_attrs"

View File

@@ -16,7 +16,6 @@
#include <type_traits> #include <type_traits>
#include <vector> #include <vector>
#include <concepts> #include <concepts>
#include <strings.h> #include <strings.h>
#include "esphome/core/optional.h" #include "esphome/core/optional.h"

View File

@@ -8,8 +8,6 @@ binary_sensor:
name: "Garage Door Open 3" name: "Garage Door Open 3"
- platform: template - platform: template
name: "Garage Door Open 4" name: "Garage Door Open 4"
- platform: template
name: "Garage Door Open 5"
- platform: template - platform: template
name: "Garage Door Internal" name: "Garage Door Internal"
internal: True internal: True
@@ -21,6 +19,10 @@ sensor:
- platform: template - platform: template
name: "Analog 2" name: "Analog 2"
lambda: return 11.0; lambda: return 11.0;
- platform: template
name: "Analog 3"
lambda: return 12.0;
internal: True
zigbee: zigbee:
wipe_on_boot: true wipe_on_boot: true
@@ -35,6 +37,9 @@ output:
write_action: write_action:
- zigbee.factory_reset - zigbee.factory_reset
time:
- platform: zigbee
switch: switch:
- platform: template - platform: template
name: "Template Switch" name: "Template Switch"