[zigbee][time] Add zigbee time component on esp32 (#18656)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
luar123
2026-08-24 17:24:57 -05:00
committed by GitHub
co-authored by J. Nick Koston
parent a15fae6a1e
commit 37b78ad46a
9 changed files with 249 additions and 20 deletions
+3
View File
@@ -9,6 +9,7 @@ SCALE = "scale"
CONF_ATTRIBUTE_ID = "attribute_id"
KEY_ZIGBEE_EP = "zigbee_ep"
KEY_ZIGBEE_EP_NO_NUM = "zigbee_ep_no_num"
KEY_ZIGBEE_FIRST_EP_CL = "zigbee_first_ep_cl"
DEVICE_ID = {
"RANGE_EXTENDER": cg.RawExpression("EZB_ZHA_RANGE_EXTENDER_DEVICE_ID"),
@@ -18,11 +19,13 @@ DEVICE_ID = {
cluster_id = cg.esphome_ns.enum("ezb_zcl_cluster_id_e")
CLUSTER_ID = {
"BASIC": cluster_id.EZB_ZCL_CLUSTER_ID_BASIC,
"TIME": cluster_id.EZB_ZCL_CLUSTER_ID_TIME,
"BINARY_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_BINARY_INPUT,
"ANALOG_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
}
CLUSTER_ROLE = {
"SERVER": cg.RawExpression("EZB_ZCL_CLUSTER_SERVER"),
"CLIENT": cg.RawExpression("EZB_ZCL_CLUSTER_CLIENT"),
}
attr_type = cg.esphome_ns.enum("ezb_zcl_attr_type_e")
ATTR_TYPE = {
+38 -10
View File
@@ -1,13 +1,15 @@
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.const import CONF_ID, CONF_UPDATE_INTERVAL
from esphome.core import CORE
from esphome.types import ConfigType
from .. import consume_endpoint
from ..const import zigbee_ns
from ..const_esp32 import ROLE
from ..const_zephyr import CONF_ZIGBEE_ID
from ..zigbee_ep_esp32 import add_clusters_to_first_ep, get_first_ep_num
from ..zigbee_zephyr import (
ZigbeeClusterDesc,
ZigbeeComponent,
@@ -22,26 +24,52 @@ DEPENDENCIES = ["zigbee"]
ZigbeeTime = zigbee_ns.class_("ZigbeeTime", time_.RealTimeClock)
def _validate_zigbee_time(config: ConfigType) -> ConfigType:
if CORE.is_nrf52:
return consume_endpoint(config)
if CORE.is_esp32:
cl = [
{
CONF_ID: "TIME",
ROLE: "CLIENT",
},
{
CONF_ID: "TIME",
ROLE: "SERVER",
},
]
add_clusters_to_first_ep(cl)
return config
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
),
cv.GenerateID(CONF_ZIGBEE_ID): cv.use_id(ZigbeeComponent),
cv.SplitDefault(
CONF_UPDATE_INTERVAL,
nrf52="1s",
esp32="15min",
): cv.update_interval, # override default from TIME_SCHEMA. Remove once nrf52 implementation is aligned.
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(cv.polling_component_schema("1s")),
consume_endpoint,
).extend(cv.COMPONENT_SCHEMA),
_validate_zigbee_time,
)
async def to_code(config: ConfigType) -> None:
CORE.add_job(_add_time, config)
if CORE.using_zephyr:
CORE.add_job(_add_time_zephyr, config)
if CORE.is_esp32:
zb = await cg.get_variable(config[CONF_ZIGBEE_ID])
var = cg.new_Pvariable(config[CONF_ID], zb, get_first_ep_num())
await cg.register_component(var, config)
await time_.register_time(var, config)
async def _add_time(config: ConfigType) -> None:
async def _add_time_zephyr(config: ConfigType) -> None:
slot_index = get_slot_index()
# Create unique names for this sensor's variables based on slot index
@@ -0,0 +1,118 @@
#include "zigbee_time_esp32.h"
#if defined(USE_ZIGBEE) && defined(USE_ESP32) && defined(USE_TIME)
#include "esphome/core/log.h"
#include "esphome/core/application.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;
static ZigbeeTime *global_time = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void ZigbeeTime::setup() {
global_time = this;
if (this->parent_->is_started()) {
this->register_zb_time_();
} else {
this->parent_->add_on_start_callback([this]() { this->register_zb_time_(); });
}
}
void ZigbeeTime::register_zb_time_() {
ezb_zcl_time_interface_t time_interface = {
.get_utc_time = esphome::zigbee::ZigbeeTime::get_utc_time,
.set_utc_time = esphome::zigbee::ZigbeeTime::set_utc_time,
};
ezb_err_t ret;
if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
this->set_timeout("zb_time_register", 100, [this]() { this->register_zb_time_(); });
return;
}
ret = ezb_zcl_time_server_interface_register(this->endpoint_, time_interface);
esp_zigbee_lock_release();
if (ret != EZB_ERR_NONE) {
ESP_LOGW(TAG, "Setup failed: %d", ret);
this->mark_failed();
return;
}
this->registered_ = true;
this->parent_->add_on_join_callback([this](bool x) { this->update(); });
if (this->parent_->is_joined()) {
this->update();
}
}
void ZigbeeTime::status_cb(ezb_err_t status) {
if (status == EZB_ERR_NONE) {
ESP_LOGV(TAG, "Time synchronization successful");
} else if (status == EZB_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Time synchronization timed out");
} else {
ESP_LOGW(TAG, "Time synchronization failed with error: %d", status);
}
}
void ZigbeeTime::update() {
if (this->parent_->is_joined() && this->registered_) {
if (esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
ESP_LOGV(TAG, "Updating time sync from Zigbee network...");
ezb_zcl_time_server_synchronize_time(this->endpoint_, 10, esphome::zigbee::ZigbeeTime::status_cb,
EZB_ZCL_TIME_SERVER_RANK_MASTER);
esp_zigbee_lock_release();
this->retry_count_ = 0;
} else {
if (this->retry_count_ == 0) {
ESP_LOGW(TAG, "Could not acquire Zigbee lock to synchronize time, will retry maximum 3 times");
}
if (this->retry_count_ < 3) {
this->set_timeout("zb_time_sync", 100, [this]() { this->update(); });
this->retry_count_++;
} else {
ESP_LOGW(TAG, "Could not acquire Zigbee lock to synchronize time");
this->retry_count_ = 0;
}
}
} else {
ESP_LOGD(TAG, "Not connected to Zigbee network, cannot synchronize time");
}
}
uint32_t ZigbeeTime::get_utc_time() {
const time_t now = global_time->timestamp_now();
if (now < EPOCH_2000) {
return 0xFFFFFFFF; // ZCL invalid UTCTime
}
return (uint32_t) (now - EPOCH_2000);
}
void ZigbeeTime::set_utc_time(uint32_t utc) {
// prevent overflow
if (utc <= (std::numeric_limits<uint32_t>::max() - EPOCH_2000)) {
global_time->set_epoch_time(utc + EPOCH_2000);
}
}
void ZigbeeTime::set_epoch_time(uint32_t utc) {
// called from zigbee task, defer to main loop
this->defer([this, utc]() {
ESP_LOGV(TAG, "Setting device time to UTC: %u", static_cast<unsigned>(utc));
this->synchronize_epoch_(utc);
});
App.wake_loop_threadsafe();
}
void ZigbeeTime::dump_config() {
ESP_LOGCONFIG(TAG,
"Zigbee Time\n"
" Endpoint: %u",
this->endpoint_);
RealTimeClock::dump_config();
}
} // namespace esphome::zigbee
#endif
@@ -0,0 +1,34 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ZIGBEE) && defined(USE_ESP32) && defined(USE_TIME)
#include "esphome/core/component.h"
#include "esphome/components/time/real_time_clock.h"
#include "../zigbee_esp32.h"
namespace esphome::zigbee {
class ZigbeeComponent;
class ZigbeeTime final : public time::RealTimeClock {
public:
ZigbeeTime(ZigbeeComponent *parent, uint8_t ep) : parent_(parent), endpoint_(ep) {}
void setup() override;
void update() override;
void dump_config() override;
void set_epoch_time(uint32_t utc);
protected:
void register_zb_time_();
static void set_utc_time(uint32_t utc);
static uint32_t get_utc_time();
static void status_cb(ezb_err_t status);
ZigbeeComponent *parent_;
uint8_t endpoint_;
uint8_t retry_count_{0};
bool registered_{false};
};
} // namespace esphome::zigbee
#endif
+46 -7
View File
@@ -18,6 +18,7 @@ from .const_esp32 import (
DEVICE_TYPE,
KEY_ZIGBEE_EP,
KEY_ZIGBEE_EP_NO_NUM,
KEY_ZIGBEE_FIRST_EP_CL,
ROLE,
)
@@ -95,11 +96,11 @@ def _get_next_ep_num(eps: list[int]) -> int:
def _compare_clusters(
existing_ep: dict[str, Any],
ep: dict[str, Any],
existing_cl_list: list[dict[str, Any]],
cl_list: list[dict[str, Any]],
) -> tuple[str | int, str] | None:
existing_clusters = [(cl[CONF_ID], cl[ROLE]) for cl in existing_ep[CONF_CLUSTERS]]
for cl in [(cl[CONF_ID], cl[ROLE]) for cl in ep[CONF_CLUSTERS]]:
existing_clusters = [(cl[CONF_ID], cl[ROLE]) for cl in existing_cl_list]
for cl in [(cl[CONF_ID], cl[ROLE]) for cl in cl_list]:
if cl in existing_clusters:
return cl
return None
@@ -110,7 +111,7 @@ def _merge_endpoints(
ep: dict[str, Any],
use_type: bool | None,
) -> bool:
if _compare_clusters(existing_ep, ep):
if _compare_clusters(existing_ep.get(CONF_CLUSTERS, []), ep.get(CONF_CLUSTERS, [])):
return False
if (
ep.get(DEVICE_TYPE)
@@ -200,6 +201,17 @@ def create_ep(router: bool) -> None:
# clear list so that it is not processed again
del zb_data[KEY_ZIGBEE_EP_NO_NUM]
# Add clusters to first ep
cl_list: list[dict] = zb_data.setdefault(KEY_ZIGBEE_FIRST_EP_CL, [])
if cl_list:
first_ep = ep_dict[get_first_ep_num()]
first_ep.setdefault(CONF_CLUSTERS, [])
if cl := _compare_clusters(first_ep[CONF_CLUSTERS], cl_list):
raise cv.Invalid(
f"Endpoint {get_first_ep_num()} has more than one cluster with cluster id {cl[0]} and role {cl[1]}."
)
first_ep[CONF_CLUSTERS] += cl_list
del zb_data[KEY_ZIGBEE_FIRST_EP_CL]
# Add default device type to endpoints that have none
for ep in ep_dict.values():
@@ -207,6 +219,15 @@ def create_ep(router: bool) -> None:
ep[DEVICE_TYPE] = "CUSTOM_ATTR"
def get_first_ep_num() -> int | None:
"""Return the number of the first endpoint."""
zb_data = CORE.data.setdefault(KEY_ZIGBEE, {})
ep_dict: dict[int, dict] = zb_data.setdefault(KEY_ZIGBEE_EP, {})
if ep_dict:
return min(ep_dict.keys())
return None
def add_ep(ep: dict[str, Any], ep_num: int | None, use_type: bool | None) -> None:
"""Add a Zigbee endpoint configuration to CORE.data.
@@ -230,8 +251,8 @@ def add_ep(ep: dict[str, Any], ep_num: int | None, use_type: bool | None) -> Non
# check if the existing endpoint has same clusters
existing_ep = ep_dict[ep_num]
if cl := _compare_clusters(
existing_ep,
ep,
existing_ep.get(CONF_CLUSTERS, []),
ep.get(CONF_CLUSTERS, []),
):
raise cv.Invalid(
f"Endpoint {ep_num} has more than one cluster with cluster id {cl[0]} and role {cl[1]}."
@@ -245,3 +266,21 @@ def add_ep(ep: dict[str, Any], ep_num: int | None, use_type: bool | None) -> Non
if use_type or ep.get(DEVICE_TYPE):
ep[CONF_USE_DEVICE_TYPE] = {ep.get(DEVICE_TYPE): use_type}
ep_dict[ep_num] = ep
def add_clusters_to_first_ep(cl: list[dict[str, Any]]) -> None:
"""Add a list of Zigbee clusters to CORE.data.
Args:
cl: list of cluster dictonaries.
"""
zb_data = CORE.data.setdefault(KEY_ZIGBEE, {})
cl_list: list[dict] = zb_data.setdefault(KEY_ZIGBEE_FIRST_EP_CL, [])
if cluster := _compare_clusters(
cl_list,
cl,
):
raise cv.Invalid(
f"Only one cluster with cluster id {cluster[0]} and role {cluster[1]} can be added to first endpoint."
)
cl_list += cl
@@ -30,6 +30,8 @@ ezb_zcl_cluster_desc_t esphome_zb_default_cluster_dscr_create(uint16_t cluster_i
return ezb_zcl_basic_create_cluster_desc(NULL, role_mask);
case EZB_ZCL_CLUSTER_ID_IDENTIFY:
return ezb_zcl_identify_create_cluster_desc(NULL, role_mask);
case EZB_ZCL_CLUSTER_ID_TIME:
return ezb_zcl_time_create_cluster_desc(NULL, role_mask);
case EZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
return ezb_zcl_analog_input_create_cluster_desc(NULL, role_mask);
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
@@ -49,6 +51,8 @@ ezb_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, ezb_zcl_cluster_desc_
return ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
case EZB_ZCL_CLUSTER_ID_IDENTIFY:
return ezb_zcl_identify_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
case EZB_ZCL_CLUSTER_ID_TIME:
return ezb_zcl_time_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
case EZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
return ezb_zcl_analog_input_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
+3
View File
@@ -41,3 +41,6 @@ number:
min_value: 2
max_value: 100
step: 1
time:
- platform: zigbee
@@ -10,6 +10,3 @@ zigbee:
on_start:
then:
- logger.log: "Started zigbee stack"
time:
- platform: zigbee
@@ -5,3 +5,6 @@ zigbee:
on_join:
then:
- logger.log: "Joined network"
time:
- platform: zigbee