mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 10:42:52 +00:00
[tuya] Add forced minute boundary time sync (#18603)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
pre-commit-ci-lite[bot]
J. Nick Koston
parent
2681126986
commit
1402cb7b64
@@ -4,6 +4,7 @@ from esphome.components import time, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "tuya"
|
||||
@@ -13,6 +14,7 @@ CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints"
|
||||
CONF_ON_DATAPOINT_UPDATE = "on_datapoint_update"
|
||||
CONF_DATAPOINT_TYPE = "datapoint_type"
|
||||
CONF_STATUS_PIN = "status_pin"
|
||||
CONF_FORCE_TIME_SYNC = "force_time_sync"
|
||||
|
||||
tuya_ns = cg.esphome_ns.namespace("tuya")
|
||||
TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True)
|
||||
@@ -43,11 +45,23 @@ DATAPOINT_TYPES = {
|
||||
|
||||
|
||||
CONF_TUYA_ID = "tuya_id"
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
def _validate_time_options(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_FORCE_TIME_SYNC] and CONF_TIME_ID not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_FORCE_TIME_SYNC} requires {CONF_TIME_ID}",
|
||||
path=[CONF_FORCE_TIME_SYNC],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Tuya),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(CONF_FORCE_TIME_SYNC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list(
|
||||
cv.uint8_t
|
||||
),
|
||||
@@ -63,7 +77,8 @@ CONFIG_SCHEMA = (
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
_validate_time_options,
|
||||
)
|
||||
|
||||
|
||||
@@ -71,9 +86,11 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
if CONF_TIME_ID in config:
|
||||
time_ = await cg.get_variable(config[CONF_TIME_ID])
|
||||
if (time_id := config.get(CONF_TIME_ID)) is not None:
|
||||
time_ = await cg.get_variable(time_id)
|
||||
cg.add(var.set_time_id(time_))
|
||||
if config[CONF_FORCE_TIME_SYNC]:
|
||||
cg.add(var.set_force_time_sync(True))
|
||||
if CONF_STATUS_PIN in config:
|
||||
status_pin_ = await cg.gpio_pin_expression(config[CONF_STATUS_PIN])
|
||||
cg.add(var.set_status_pin(status_pin_))
|
||||
|
||||
@@ -23,11 +23,13 @@ ESPHOME_LOG_TAG(TAG, "tuya");
|
||||
static constexpr uint32_t HEARTBEAT_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t WIFI_STATUS_INTERVAL_ID = 1;
|
||||
static constexpr uint32_t DATAPOINT_DUMP_TIMEOUT_ID = 2;
|
||||
static constexpr uint32_t FORCE_TIME_SYNC_INTERVAL_ID = 3;
|
||||
static const int COMMAND_DELAY = 10;
|
||||
static const int RECEIVE_TIMEOUT = 300;
|
||||
static const int MAX_RETRIES = 5;
|
||||
// Max bytes to log for datapoint values (larger values are truncated)
|
||||
static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16;
|
||||
static constexpr uint32_t FORCE_TIME_SYNC_CHECK_INTERVAL = 1000;
|
||||
|
||||
static bool network_is_connected() {
|
||||
#ifdef USE_NETWORK
|
||||
@@ -39,6 +41,13 @@ static bool network_is_connected() {
|
||||
|
||||
void Tuya::setup() {
|
||||
this->set_interval(HEARTBEAT_INTERVAL_ID, 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
|
||||
#ifdef USE_TIME
|
||||
if (this->force_time_sync_) {
|
||||
this->register_local_time_sync_callback_();
|
||||
this->set_interval(FORCE_TIME_SYNC_INTERVAL_ID, FORCE_TIME_SYNC_CHECK_INTERVAL,
|
||||
[this] { this->check_force_time_sync_(); });
|
||||
}
|
||||
#endif
|
||||
if (this->status_pin_ != nullptr) {
|
||||
this->status_pin_->digital_write(false);
|
||||
}
|
||||
@@ -99,6 +108,11 @@ void Tuya::dump_config() {
|
||||
}
|
||||
LOG_PIN(" Status Pin: ", this->status_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str());
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Force time sync: %s", YESNO(this->force_time_sync_));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Tuya::validate_message_() {
|
||||
@@ -306,12 +320,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
this->send_local_time_();
|
||||
|
||||
if (!this->time_sync_callback_registered_) {
|
||||
// tuya mcu supports time, so we let them know when our time changed
|
||||
this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||
this->time_sync_callback_registered_ = true;
|
||||
}
|
||||
this->register_local_time_sync_callback_();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -616,7 +625,23 @@ void Tuya::send_wifi_status_() {
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
void Tuya::send_local_time_() {
|
||||
void Tuya::register_local_time_sync_callback_() {
|
||||
if (this->time_sync_callback_registered_) {
|
||||
return;
|
||||
}
|
||||
this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||
this->time_sync_callback_registered_ = true;
|
||||
}
|
||||
|
||||
void Tuya::check_force_time_sync_() {
|
||||
ESPTime now = this->time_id_->now();
|
||||
if (!now.is_valid() || now.minute == this->last_force_time_sync_minute_)
|
||||
return;
|
||||
this->last_force_time_sync_minute_ = now.minute;
|
||||
this->send_local_time_(true);
|
||||
}
|
||||
|
||||
void Tuya::send_local_time_(bool periodic) {
|
||||
std::vector<uint8_t> payload;
|
||||
ESPTime now = this->time_id_->now();
|
||||
if (now.is_valid()) {
|
||||
@@ -631,7 +656,12 @@ void Tuya::send_local_time_() {
|
||||
if (day_of_week == 0) {
|
||||
day_of_week = 7;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending local time");
|
||||
// The forced send repeats every minute, so it stays out of the debug log
|
||||
if (periodic) {
|
||||
ESP_LOGV(TAG, "Sending local time");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Sending local time");
|
||||
}
|
||||
payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second, day_of_week};
|
||||
} else {
|
||||
// By spec we need to notify MCU that the time was not obtained if this is a response to a query
|
||||
|
||||
@@ -113,6 +113,7 @@ class Tuya final : public Component, public uart::UARTDevice {
|
||||
TuyaInitState get_init_state();
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
void set_force_time_sync(bool force_time_sync) { this->force_time_sync_ = force_time_sync; }
|
||||
#endif
|
||||
void add_ignore_mcu_update_on_datapoints(uint8_t ignore_mcu_update_on_datapoints) {
|
||||
this->ignore_mcu_update_on_datapoints_.push_back(ignore_mcu_update_on_datapoints);
|
||||
@@ -144,9 +145,13 @@ class Tuya final : public Component, public uart::UARTDevice {
|
||||
uint8_t get_wifi_rssi_();
|
||||
|
||||
#ifdef USE_TIME
|
||||
void send_local_time_();
|
||||
void register_local_time_sync_callback_();
|
||||
void check_force_time_sync_();
|
||||
void send_local_time_(bool periodic = false);
|
||||
void send_gmt_time_();
|
||||
time::RealTimeClock *time_id_{nullptr};
|
||||
bool force_time_sync_{false};
|
||||
uint8_t last_force_time_sync_minute_{0xFF}; // 0xFF: no minute seen yet
|
||||
bool time_sync_callback_registered_{false};
|
||||
bool gmt_time_sync_callback_registered_{false};
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Tests for Tuya time synchronization configuration validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.tuya import CONF_FORCE_TIME_SYNC, CONFIG_SCHEMA
|
||||
from esphome.const import CONF_TIME_ID
|
||||
|
||||
|
||||
def test_force_time_sync_accepts_time_id() -> None:
|
||||
config = CONFIG_SCHEMA(
|
||||
{
|
||||
CONF_FORCE_TIME_SYNC: True,
|
||||
CONF_TIME_ID: "tuya_time",
|
||||
}
|
||||
)
|
||||
|
||||
assert config[CONF_FORCE_TIME_SYNC] is True
|
||||
|
||||
|
||||
def test_force_time_sync_requires_time_id() -> None:
|
||||
with pytest.raises(cv.Invalid, match="force_time_sync requires time_id"):
|
||||
CONFIG_SCHEMA({CONF_FORCE_TIME_SYNC: True})
|
||||
@@ -0,0 +1,16 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
status_pin: GPIO12
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
common: !include common.yaml
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: tuya_time
|
||||
|
||||
tuya:
|
||||
time_id: tuya_time
|
||||
force_time_sync: true
|
||||
Reference in New Issue
Block a user