From dc15aec7f27536c81c012417e1dc6f2c7de63577 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke Date: Mon, 17 Feb 2025 13:24:20 +0100 Subject: [PATCH] Add files via upload --- .../components/dynamic_on_time/__init__.py | 79 +++++++ .../dynamic_on_time/dynamic_on_time.cpp | 212 ++++++++++++++++++ .../dynamic_on_time/dynamic_on_time.h | 52 +++++ 3 files changed, 343 insertions(+) create mode 100644 esphome/components/dynamic_on_time/__init__.py create mode 100644 esphome/components/dynamic_on_time/dynamic_on_time.cpp create mode 100644 esphome/components/dynamic_on_time/dynamic_on_time.h diff --git a/esphome/components/dynamic_on_time/__init__.py b/esphome/components/dynamic_on_time/__init__.py new file mode 100644 index 0000000000..c55e86bec2 --- /dev/null +++ b/esphome/components/dynamic_on_time/__init__.py @@ -0,0 +1,79 @@ +''' +tbd +''' +from esphome.const import ( + CONF_ID, + CONF_ON_TIME, + CONF_THEN, + CONF_HOUR, + CONF_MINUTE, +) +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.components.number import Number +from esphome.components.switch import Switch +from esphome.components.time import RealTimeClock + +MULTI_CONF = True +DEPENDENCIES = ["time", "number", "switch"] +CODEOWNERS = ["@hostcc"] + +CONF_RTC = 'rtc' +CONF_MON = 'mon' +CONF_TUE = 'tue' +CONF_WED = 'wed' +CONF_THU = 'thu' +CONF_FRI = 'fri' +CONF_SAT = 'sat' +CONF_SUN = 'sun' +CONF_DISABLED = 'disabled' + +dynamic_on_time_ns = cg.esphome_ns.namespace("dynamic_on_time") +DynamicOnTimeComponent = dynamic_on_time_ns.class_( + "DynamicOnTime", cg.Component +) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(DynamicOnTimeComponent), + cv.Required(CONF_RTC): cv.use_id(RealTimeClock), + cv.Required(CONF_HOUR): cv.use_id(Number), + cv.Required(CONF_MINUTE): cv.use_id(Number), + cv.Required(CONF_MON): cv.use_id(Switch), + cv.Required(CONF_TUE): cv.use_id(Switch), + cv.Required(CONF_WED): cv.use_id(Switch), + cv.Required(CONF_THU): cv.use_id(Switch), + cv.Required(CONF_FRI): cv.use_id(Switch), + cv.Required(CONF_SAT): cv.use_id(Switch), + cv.Required(CONF_SUN): cv.use_id(Switch), + cv.Required(CONF_DISABLED): cv.use_id(Switch), + cv.Required(CONF_ON_TIME): automation.validate_automation({}), +}).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + ''' + tbd + ''' + actions = [] + for conf in config[CONF_ON_TIME]: + actions.extend(await automation.build_action_list( + conf[CONF_THEN], cg.TemplateArguments(), []) + ) + + var = cg.new_Pvariable( + config[CONF_ID], + await cg.get_variable(config[CONF_RTC]), + await cg.get_variable(config[CONF_HOUR]), + await cg.get_variable(config[CONF_MINUTE]), + await cg.get_variable(config[CONF_MON]), + await cg.get_variable(config[CONF_TUE]), + await cg.get_variable(config[CONF_WED]), + await cg.get_variable(config[CONF_THU]), + await cg.get_variable(config[CONF_FRI]), + await cg.get_variable(config[CONF_SAT]), + await cg.get_variable(config[CONF_SUN]), + await cg.get_variable(config[CONF_DISABLED]), + actions, + ) + await cg.register_component(var, config) diff --git a/esphome/components/dynamic_on_time/dynamic_on_time.cpp b/esphome/components/dynamic_on_time/dynamic_on_time.cpp new file mode 100644 index 0000000000..17fc82b67c --- /dev/null +++ b/esphome/components/dynamic_on_time/dynamic_on_time.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023 Ilia Sotnikov + +#include "dynamic_on_time.h" // NOLINT(build/include_subdir) +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace dynamic_on_time { + +static const char *tag = "dynamic_on_time"; + +DynamicOnTime::DynamicOnTime( + time::RealTimeClock *rtc, + number::Number *hour, + number::Number *minute, + switch_::Switch *mon, + switch_::Switch *tue, + switch_::Switch *wed, + switch_::Switch *thu, + switch_::Switch *fri, + switch_::Switch *sat, + switch_::Switch *sun, + switch_::Switch *disabled, + std::vector *> actions): + rtc_(rtc), + hour_(hour), minute_(minute), + mon_(mon), tue_(tue), wed_(wed), thu_(thu), fri_(fri), sat_(sat), + sun_(sun), disabled_(disabled), actions_(actions) {} + +std::vector DynamicOnTime::flags_to_days_of_week_( + bool mon, bool tue, bool wed, bool thu, bool fri, bool sat, bool sun +) { + // Numeric representation for days of week (starts from Sun internally) + std::vector days_of_week = { 1, 2, 3, 4, 5, 6, 7 }; + std::vector flags = { sun, mon, tue, wed, thu, fri, sat }; + + // Translate set of bool flags into vector of corresponding numeric + // representation. This uses 'erase-remove' approach ( + // https://en.cppreference.com/w/cpp/algorithm/remove, + // https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom) + days_of_week.erase( + std::remove_if( + std::begin(days_of_week), std::end(days_of_week), + [&](uint8_t& arg) { return !flags[&arg - days_of_week.data()]; }), + days_of_week.end()); + + return days_of_week; +} + +void DynamicOnTime::setup() { + // Update the configuration initially, ensuring all entities are created + // before a callback would be delivered to them + this->update_schedule_(); + + // Register the cron trigger component + App.register_component(this->trigger_); + + // The `Number` and `Switch` has no common base type with + // `add_on_state_callback`, and solutions to properly cast to derived + // class in single loop over vector of base class instances seemingly imply + // more code than just two loops + for (number::Number *comp : {this->hour_, this->minute_}) { + comp->add_on_state_callback([this](float value) { + this->update_schedule_(); + }); + } + + for (switch_::Switch *comp : { + this->mon_, this->tue_, this->wed_, this->thu_, this->fri_, this->sat_, + this->sun_, this->disabled_ + }) { + comp->add_on_state_callback([this](bool value) { + this->update_schedule_(); + }); + } +} + +void DynamicOnTime::update_schedule_() { + // CronTrigger doesn't allow its configuration to be reset programmatically, + // so its instance is either created initially, or reinitialized in place if + // allocated already + if (this->trigger_ != nullptr) { + // Use 'placement new' (https://en.cppreference.com/w/cpp/language/new) to + // reinitialize existing CronTrigger instance in place + this->trigger_->~CronTrigger(); + new (this->trigger_) time::CronTrigger(this->rtc_); + } else { + this->trigger_ = new time::CronTrigger(this->rtc_); + } + + // (Re)create the automation instance but only if scheduled actions aren't + // disabled + if (this->automation_ != nullptr) { + delete this->automation_; + this->automation_ = nullptr; + } + + if (!this->disabled_->state) { + this->automation_ = new Automation<>(this->trigger_); + // Add requested actions to it + this->automation_->add_actions(this->actions_); + } + + // All remaining logic is active regardless of scheduled actions are + // disabled, since callbacks from Switch/Number components being active still + // need to be processed otherwise inputs will be lost + // + // Set trigger to fire on zeroth second of configured time + this->trigger_->add_second(0); + // Enable all days of months for the schedule + for (uint8_t i = 1; i <= 31; i++) + this->trigger_->add_day_of_month(i); + // Same but for months + for (uint8_t i = 1; i <= 12; i++) + this->trigger_->add_month(i); + // Configure hour/minute of the schedule from corresponding components' state + this->trigger_->add_hour(static_cast(this->hour_->state)); + this->trigger_->add_minute(static_cast(this->minute_->state)); + // Similarly but for days of week translating set of components' state to + // vector of numeric representation as `CrontTrigger::add_days_of_week()` + // requires + this->days_of_week_ = this->flags_to_days_of_week_( + this->mon_->state, this->tue_->state, this->wed_->state, + this->thu_->state, this->fri_->state, this->sat_->state, + this->sun_->state); + this->trigger_->add_days_of_week(this->days_of_week_); + + // Initiate updating the cached value for the next schedule + this->next_schedule_.reset(); + + // Log the configuration + this->dump_config(); +} + +optional DynamicOnTime::get_next_schedule() { + if (this->disabled_->state || this->days_of_week_.empty()) + return {}; + + ESPTime now = this->rtc_->now(); + + if (now < this->next_schedule_.value_or(now)) + return this->next_schedule_; + + ESP_LOGVV(tag, "Non-cached calculation of next schedule"); + + // Calculate timestamp for the start of the week with time being hour/time of + // the schedule + time_t start_of_week = now.timestamp + - (now.second + now.hour * 3600 + now.minute * 60 + now.day_of_week * 86400) + + (3600 * static_cast(this->hour_->state) + + 60 * static_cast(this->minute_->state)); + + time_t next = 0, first = 0; + for (auto next_day : this->days_of_week_) { + // Calculate the timestamp for next day in schedule + next = start_of_week + 86400 * next_day; + // Capture timestamp for the first scheduled day + if (!first) + first = next; + // Exit if timestamp corresponds of later date in the schedule found + if (next > now.timestamp) + break; + } + // No later date has been found, use the earlier of scheduled ones plus one + // week + if (next < now.timestamp) + next = first + 7 * 86400; + + return this->next_schedule_ = ESPTime::from_epoch_local(next); +} + +void DynamicOnTime::dump_config() { + ESP_LOGCONFIG(tag, "Cron trigger details:"); + ESP_LOGCONFIG(tag, "Disabled: %s", ONOFF(this->disabled_->state)); + ESP_LOGCONFIG( + tag, "Hour (source: '%s'): %.0f", + this->hour_->get_name().c_str(), this->hour_->state); + ESP_LOGCONFIG( + tag, "Minute (source: '%s'): %.0f", + this->minute_->get_name().c_str(), this->minute_->state); + ESP_LOGCONFIG( + tag, "Mon (source: '%s'): %s", + this->mon_->get_name().c_str(), ONOFF(this->mon_->state)); + ESP_LOGCONFIG( + tag, "Tue (source: '%s'): %s", + this->tue_->get_name().c_str(), ONOFF(this->tue_->state)); + ESP_LOGCONFIG( + tag, "Wed (source: '%s'): %s", + this->wed_->get_name().c_str(), ONOFF(this->wed_->state)); + ESP_LOGCONFIG( + tag, "Thu (source: '%s'): %s", + this->thu_->get_name().c_str(), ONOFF(this->thu_->state)); + ESP_LOGCONFIG( + tag, "Fri (source: '%s'): %s", + this->fri_->get_name().c_str(), ONOFF(this->fri_->state)); + ESP_LOGCONFIG( + tag, "Sat (source: '%s'): %s", + this->sat_->get_name().c_str(), ONOFF(this->sat_->state)); + ESP_LOGCONFIG( + tag, "Sun (source: '%s'): %s", + this->sun_->get_name().c_str(), ONOFF(this->sun_->state)); + + auto schedule = this->get_next_schedule(); + if (schedule.has_value()) + ESP_LOGCONFIG( + tag, "Next schedule: %s", + schedule.value().strftime("%a %H:%M:%S %m/%d/%Y").c_str()); +} + +} // namespace dynamic_on_time +} // namespace esphome diff --git a/esphome/components/dynamic_on_time/dynamic_on_time.h b/esphome/components/dynamic_on_time/dynamic_on_time.h new file mode 100644 index 0000000000..be6b25f865 --- /dev/null +++ b/esphome/components/dynamic_on_time/dynamic_on_time.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023 Ilia Sotnikov + +#pragma once +#include +#include "esphome/components/time/real_time_clock.h" +#include "esphome/components/time/automation.h" +#include "esphome/components/number/number.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace dynamic_on_time { + +class DynamicOnTime : public Component { + public: + explicit DynamicOnTime( + time::RealTimeClock *, number::Number *, number::Number *, + switch_::Switch *, switch_::Switch *, switch_::Switch *, switch_::Switch *, + switch_::Switch *, switch_::Switch *, switch_::Switch *, switch_::Switch *, + std::vector *>); + + void setup() override; + void dump_config() override; + + optional get_next_schedule(); + + protected: + time::RealTimeClock *rtc_; + number::Number *hour_; + number::Number *minute_; + switch_::Switch *mon_; + switch_::Switch *tue_; + switch_::Switch *wed_; + switch_::Switch *thu_; + switch_::Switch *fri_; + switch_::Switch *sat_; + switch_::Switch *sun_; + switch_::Switch *disabled_; + std::vector *> actions_; + time::CronTrigger *trigger_{nullptr}; + Automation<> *automation_{nullptr}; + std::vector days_of_week_{}; + + std::vector flags_to_days_of_week_( + bool, bool, bool, bool, bool, bool, bool); + + void update_schedule_(); + optional next_schedule_{}; +}; + +} // namespace dynamic_on_time +} // namespace esphome