mirror of
https://github.com/esphome/esphome.git
synced 2026-08-31 10:06:03 +00:00
[zigbee][time] Add zigbee time component on esp32 (#18656)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
J. Nick Koston
parent
a15fae6a1e
commit
37b78ad46a
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user