diff --git a/esphome/components/template/time/__init__.py b/esphome/components/template/time/__init__.py new file mode 100644 index 0000000000..614daabd41 --- /dev/null +++ b/esphome/components/template/time/__init__.py @@ -0,0 +1,25 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.components import time +from esphome.const import CONF_ID +from .. import template_ns + +CODEOWNERS = ["@RFDarter"] + +TemplateRealTimeClock = template_ns.class_("TemplateRealTimeClock", time.RealTimeClock) +WriteAction = template_ns.class_("SystemTimeSetAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateRealTimeClock), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await time.register_time(var, config) diff --git a/esphome/components/template/time/template_time.cpp b/esphome/components/template/time/template_time.cpp new file mode 100644 index 0000000000..bd9154cb78 --- /dev/null +++ b/esphome/components/template/time/template_time.cpp @@ -0,0 +1,21 @@ +#include "template_time.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.time"; + +void TemplateRealTimeClock::setup() {} + +void TemplateRealTimeClock::update() {} + +void TemplateRealTimeClock::dump_config() { + ESP_LOGCONFIG(TAG, "template.time"); + + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +} + +float TemplateRealTimeClock::get_setup_priority() const { return setup_priority::DATA; } +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/time/template_time.h b/esphome/components/template/time/template_time.h new file mode 100644 index 0000000000..8d61772601 --- /dev/null +++ b/esphome/components/template/time/template_time.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace template_ { + +class TemplateRealTimeClock : public time::RealTimeClock { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 6a3368ca73..597ae1da0c 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -11,6 +11,8 @@ import esphome.config_validation as cv from esphome.const import ( CONF_AT, CONF_CRON, + CONF_DATETIME, + CONF_DAY, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF_HOUR, @@ -18,6 +20,7 @@ from esphome.const import ( CONF_ID, CONF_MINUTE, CONF_MINUTES, + CONF_MONTH, CONF_MONTHS, CONF_ON_TIME, CONF_ON_TIME_SYNC, @@ -25,6 +28,7 @@ from esphome.const import ( CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, + CONF_YEAR, ) from esphome.core import coroutine_with_priority @@ -33,11 +37,14 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@OttoWinter"] IS_PLATFORM_COMPONENT = True +CONF_UTC = "utc" + time_ns = cg.esphome_ns.namespace("time") RealTimeClock = time_ns.class_("RealTimeClock", cg.PollingComponent) CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Component) SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component) TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition) +SystemTimeSetAction = time_ns.class_("SystemTimeSetAction", automation.Action) def _load_tzdata(iana_key: str) -> Optional[bytes]: @@ -344,3 +351,39 @@ async def to_code(config): async def time_has_time_to_code(config, condition_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren) + + +@automation.register_action( + "system_time.set", + SystemTimeSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(RealTimeClock), + cv.Required(CONF_DATETIME): cv.Any( + cv.returning_lambda, cv.date_time(date=True, time=True) + ), + cv.Optional(CONF_UTC, default=False): cv.boolean, + }, + ), +) +async def system_time_set_to_code(config, action_id, template_arg, args): + action_var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(action_var, config[CONF_ID]) + + datetime_config = config[CONF_DATETIME] + cg.add(action_var.set_utc(config[CONF_UTC])) + if cg.is_template(datetime_config): + template_ = await cg.templatable(datetime_config, args, cg.ESPTime) + cg.add(action_var.set_time(template_)) + else: + datetime_struct = cg.StructInitializer( + cg.ESPTime, + ("second", datetime_config[CONF_SECOND]), + ("minute", datetime_config[CONF_MINUTE]), + ("hour", datetime_config[CONF_HOUR]), + ("day_of_month", datetime_config[CONF_DAY]), + ("month", datetime_config[CONF_MONTH]), + ("year", datetime_config[CONF_YEAR]), + ) + cg.add(action_var.set_time(datetime_struct)) + return action_var diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 2b9a95c6bd..7c7462f954 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -58,5 +58,52 @@ void RealTimeClock::apply_timezone_() { tzset(); } +void RealTimeClock::set_time(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second, + bool utc) { + ESPTime time{ + .second = second, + .minute = minute, + .hour = hour, + .day_of_week = 1, // not used + .day_of_month = day, + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = month, + .year = year, + .is_dst = false, // not used + .timestamp = 0 // overwritten by recalc_timestamp_utc(false) + }; + + if (utc) { + time.recalc_timestamp_utc(); + } else { + time.recalc_timestamp_local(); + } + + if (!time.is_valid()) { + ESP_LOGE(TAG, "Invalid time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(time.timestamp); +}; + +void RealTimeClock::set_time(ESPTime datetime, bool utc) { + return this->set_time(datetime.year, datetime.month, datetime.day_of_month, datetime.hour, datetime.minute, + datetime.second, utc); +}; + +void RealTimeClock::set_time(const std::string &datetime, bool utc) { + ESPTime val{}; + if (!ESPTime::strptime(datetime, val)) { + ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object"); + return; + } + this->set_time(val, utc); +} + +void RealTimeClock::set_time(time_t epoch_seconds, bool utc) { + ESPTime val = ESPTime::from_epoch_local(epoch_seconds); + this->set_time(val, utc); +} + } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index a17168ae6f..dd60d053c0 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -20,6 +20,12 @@ class RealTimeClock : public PollingComponent { public: explicit RealTimeClock(); + void set_time(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second, + bool utc = false); + void set_time(ESPTime datetime, bool utc = false); + void set_time(const std::string &datetime, bool utc = false); + void set_time(time_t epoch_seconds, bool utc = false); + /// Set the time zone. void set_timezone(const std::string &tz) { this->timezone_ = tz; } @@ -60,5 +66,17 @@ template class TimeHasTimeCondition : public Condition { RealTimeClock *parent_; }; +template class SystemTimeSetAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(ESPTime, time) + TEMPLATABLE_VALUE(bool, utc) + + void play(Ts... x) override { + if (this->time_.has_value() && this->utc_.has_value()) { + this->parent_->set_time(this->time_.value(x...), this->utc_.value(x...)); + } + } +}; + } // namespace time } // namespace esphome