mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Add files via upload
This commit is contained in:
		
							
								
								
									
										79
									
								
								esphome/components/dynamic_on_time/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								esphome/components/dynamic_on_time/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										212
									
								
								esphome/components/dynamic_on_time/dynamic_on_time.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								esphome/components/dynamic_on_time/dynamic_on_time.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<esphome::Action<> *> 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<uint8_t> 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<uint8_t> days_of_week = { 1, 2, 3, 4, 5, 6, 7 }; | ||||
|   std::vector<bool> 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<uint8_t>(this->hour_->state)); | ||||
|   this->trigger_->add_minute(static_cast<uint8_t>(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<ESPTime> 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<int>(this->hour_->state) | ||||
|     + 60 * static_cast<int>(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 | ||||
							
								
								
									
										52
									
								
								esphome/components/dynamic_on_time/dynamic_on_time.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/dynamic_on_time/dynamic_on_time.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
| // Copyright (c) 2023 Ilia Sotnikov | ||||
|  | ||||
| #pragma once | ||||
| #include <vector> | ||||
| #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<esphome::Action<> *>); | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   optional<ESPTime> 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<esphome::Action<> *> actions_; | ||||
|   time::CronTrigger *trigger_{nullptr}; | ||||
|   Automation<> *automation_{nullptr}; | ||||
|   std::vector<uint8_t> days_of_week_{}; | ||||
|  | ||||
|   std::vector<uint8_t> flags_to_days_of_week_( | ||||
|     bool, bool, bool, bool, bool, bool, bool); | ||||
|  | ||||
|   void update_schedule_(); | ||||
|   optional<ESPTime> next_schedule_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace dynamic_on_time | ||||
| }  // namespace esphome | ||||
		Reference in New Issue
	
	Block a user