mirror of
https://github.com/esphome/esphome.git
synced 2025-09-27 07:32:22 +01:00
Merge branches 'dev' and 'dev' of https://github.com/p1ngb4ck/esphome into dev
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