diff options
28 files changed, 1548 insertions, 0 deletions
diff --git a/components/template/__init__.py b/components/template/__init__.py new file mode 100644 index 0000000..6253af9 --- /dev/null +++ b/components/template/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +template_ns = cg.esphome_ns.namespace("template_") diff --git a/components/template/binary_sensor/__init__.py b/components/template/binary_sensor/__init__.py new file mode 100644 index 0000000..8f551e3 --- /dev/null +++ b/components/template/binary_sensor/__init__.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import binary_sensor +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE +from .. import template_ns + +TemplateBinarySensor = template_ns.class_( + "TemplateBinarySensor", binary_sensor.BinarySensor, cg.Component +) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateBinarySensor), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await binary_sensor.register_binary_sensor(var, config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(bool) + ) + cg.add(var.set_template(template_)) + + [email protected]_action( + "binary_sensor.template.publish", + binary_sensor.BinarySensorPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } + ), +) +async def binary_sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_STATE], args, bool) + cg.add(var.set_state(template_)) + return var diff --git a/components/template/binary_sensor/template_binary_sensor.cpp b/components/template/binary_sensor/template_binary_sensor.cpp new file mode 100644 index 0000000..66ff4be --- /dev/null +++ b/components/template/binary_sensor/template_binary_sensor.cpp @@ -0,0 +1,21 @@ +#include "template_binary_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.binary_sensor"; + +void TemplateBinarySensor::loop() { + if (!this->f_.has_value()) + return; + + auto s = (*this->f_)(); + if (s.has_value()) { + this->publish_state(*s); + } +} +void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } + +} // namespace template_ +} // namespace esphome diff --git a/components/template/binary_sensor/template_binary_sensor.h b/components/template/binary_sensor/template_binary_sensor.h new file mode 100644 index 0000000..a28929b --- /dev/null +++ b/components/template/binary_sensor/template_binary_sensor.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace template_ { + +class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { + public: + void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; } + + void loop() override; + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + optional<std::function<optional<bool>()>> f_{}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/components/template/button/__init__.py b/components/template/button/__init__.py new file mode 100644 index 0000000..aa192d1 --- /dev/null +++ b/components/template/button/__init__.py @@ -0,0 +1,13 @@ +import esphome.config_validation as cv +from esphome.components import button + + +CONFIG_SCHEMA = button.BUTTON_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(button.Button), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + await button.new_button(config) diff --git a/components/template/cover/__init__.py b/components/template/cover/__init__.py new file mode 100644 index 0000000..a628da7 --- /dev/null +++ b/components/template/cover/__init__.py @@ -0,0 +1,131 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import cover +from esphome.const import ( + CONF_ASSUMED_STATE, + CONF_CLOSE_ACTION, + CONF_CURRENT_OPERATION, + CONF_ID, + CONF_LAMBDA, + CONF_OPEN_ACTION, + CONF_OPTIMISTIC, + CONF_POSITION, + CONF_RESTORE_MODE, + CONF_STATE, + CONF_STOP_ACTION, + CONF_TILT, + CONF_TILT_ACTION, + CONF_TILT_LAMBDA, + CONF_POSITION_ACTION, +) +from .. import template_ns + +TemplateCover = template_ns.class_("TemplateCover", cover.Cover, cg.Component) + +TemplateCoverRestoreMode = template_ns.enum("TemplateCoverRestoreMode") +RESTORE_MODES = { + "NO_RESTORE": TemplateCoverRestoreMode.COVER_NO_RESTORE, + "RESTORE": TemplateCoverRestoreMode.COVER_RESTORE, + "RESTORE_AND_CALL": TemplateCoverRestoreMode.COVER_RESTORE_AND_CALL, +} + +CONF_HAS_POSITION = "has_position" + +CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateCover), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, + cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cover.register_cover(var, config) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(float) + ) + cg.add(var.set_state_lambda(template_)) + if CONF_OPEN_ACTION in config: + await automation.build_automation( + var.get_open_trigger(), [], config[CONF_OPEN_ACTION] + ) + if CONF_CLOSE_ACTION in config: + await automation.build_automation( + var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] + ) + if CONF_STOP_ACTION in config: + await automation.build_automation( + var.get_stop_trigger(), [], config[CONF_STOP_ACTION] + ) + if CONF_TILT_ACTION in config: + await automation.build_automation( + var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] + ) + cg.add(var.set_has_tilt(True)) + if CONF_TILT_LAMBDA in config: + tilt_template_ = await cg.process_lambda( + config[CONF_TILT_LAMBDA], [], return_type=cg.optional.template(float) + ) + cg.add(var.set_tilt_lambda(tilt_template_)) + if CONF_POSITION_ACTION in config: + await automation.build_automation( + var.get_position_trigger(), [(float, "pos")], config[CONF_POSITION_ACTION] + ) + cg.add(var.set_has_position(True)) + else: + cg.add(var.set_has_position(config[CONF_HAS_POSITION])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + cg.add(var.set_has_position(config[CONF_HAS_POSITION])) + + [email protected]_action( + "cover.template.publish", + cover.CoverPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(cover.Cover), + cv.Exclusive(CONF_STATE, "pos"): cv.templatable(cover.validate_cover_state), + cv.Exclusive(CONF_POSITION, "pos"): cv.templatable(cv.zero_to_one_float), + cv.Optional(CONF_CURRENT_OPERATION): cv.templatable( + cover.validate_cover_operation + ), + cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float), + } + ), +) +async def cover_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_STATE in config: + template_ = await cg.templatable(config[CONF_STATE], args, float) + cg.add(var.set_position(template_)) + if CONF_POSITION in config: + template_ = await cg.templatable(config[CONF_POSITION], args, float) + cg.add(var.set_position(template_)) + if CONF_TILT in config: + template_ = await cg.templatable(config[CONF_TILT], args, float) + cg.add(var.set_tilt(template_)) + if CONF_CURRENT_OPERATION in config: + template_ = await cg.templatable( + config[CONF_CURRENT_OPERATION], args, cover.CoverOperation + ) + cg.add(var.set_current_operation(template_)) + return var diff --git a/components/template/cover/template_cover.cpp b/components/template/cover/template_cover.cpp new file mode 100644 index 0000000..47c651e --- /dev/null +++ b/components/template/cover/template_cover.cpp @@ -0,0 +1,129 @@ +#include "template_cover.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +using namespace esphome::cover; + +static const char *const TAG = "template.cover"; + +TemplateCover::TemplateCover() + : open_trigger_(new Trigger<>()), + close_trigger_(new Trigger<>), + stop_trigger_(new Trigger<>()), + position_trigger_(new Trigger<float>()), + tilt_trigger_(new Trigger<float>()) {} +void TemplateCover::setup() { + ESP_LOGCONFIG(TAG, "Setting up template cover '%s'...", this->name_.c_str()); + switch (this->restore_mode_) { + case COVER_NO_RESTORE: + break; + case COVER_RESTORE: { + auto restore = this->restore_state_(); + if (restore.has_value()) + restore->apply(this); + break; + } + case COVER_RESTORE_AND_CALL: { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } + break; + } + } +} +void TemplateCover::loop() { + bool changed = false; + + if (this->state_f_.has_value()) { + auto s = (*this->state_f_)(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; + } + } + } + if (this->tilt_f_.has_value()) { + auto s = (*this->tilt_f_)(); + if (s.has_value()) { + auto tilt = clamp(*s, 0.0f, 1.0f); + if (tilt != this->tilt) { + this->tilt = tilt; + changed = true; + } + } + } + + if (changed) + this->publish_state(); +} +void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } +void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } +void TemplateCover::set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; } +float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } +Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } +Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } +Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } +void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } +void TemplateCover::control(const CoverCall &call) { + if (call.get_stop()) { + this->stop_prev_trigger_(); + this->stop_trigger_->trigger(); + this->prev_command_trigger_ = this->stop_trigger_; + this->publish_state(); + } + if (call.get_position().has_value()) { + auto pos = *call.get_position(); + this->stop_prev_trigger_(); + + if (pos == COVER_OPEN) { + this->open_trigger_->trigger(); + this->prev_command_trigger_ = this->open_trigger_; + } else if (pos == COVER_CLOSED) { + this->close_trigger_->trigger(); + this->prev_command_trigger_ = this->close_trigger_; + } else { + this->position_trigger_->trigger(pos); + } + + if (this->optimistic_) { + this->position = pos; + } + } + + if (call.get_tilt().has_value()) { + auto tilt = *call.get_tilt(); + this->tilt_trigger_->trigger(tilt); + + if (this->optimistic_) { + this->tilt = tilt; + } + } + + this->publish_state(); +} +CoverTraits TemplateCover::get_traits() { + auto traits = CoverTraits(); + traits.set_is_assumed_state(this->assumed_state_); + traits.set_supports_position(this->has_position_); + traits.set_supports_tilt(this->has_tilt_); + return traits; +} +Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; } +Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } +void TemplateCover::set_tilt_lambda(std::function<optional<float>()> &&tilt_f) { this->tilt_f_ = tilt_f; } +void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } +void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } +void TemplateCover::stop_prev_trigger_() { + if (this->prev_command_trigger_ != nullptr) { + this->prev_command_trigger_->stop_action(); + this->prev_command_trigger_ = nullptr; + } +} + +} // namespace template_ +} // namespace esphome diff --git a/components/template/cover/template_cover.h b/components/template/cover/template_cover.h new file mode 100644 index 0000000..3b9dcea --- /dev/null +++ b/components/template/cover/template_cover.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace template_ { + +enum TemplateCoverRestoreMode { + COVER_NO_RESTORE, + COVER_RESTORE, + COVER_RESTORE_AND_CALL, +}; + +class TemplateCover : public cover::Cover, public Component { + public: + TemplateCover(); + + void set_state_lambda(std::function<optional<float>()> &&f); + Trigger<> *get_open_trigger() const; + Trigger<> *get_close_trigger() const; + Trigger<> *get_stop_trigger() const; + Trigger<float> *get_position_trigger() const; + Trigger<float> *get_tilt_trigger() const; + void set_optimistic(bool optimistic); + void set_assumed_state(bool assumed_state); + void set_tilt_lambda(std::function<optional<float>()> &&tilt_f); + void set_has_position(bool has_position); + void set_has_tilt(bool has_tilt); + void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } + + void setup() override; + void loop() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: + void control(const cover::CoverCall &call) override; + cover::CoverTraits get_traits() override; + void stop_prev_trigger_(); + + TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; + optional<std::function<optional<float>()>> state_f_; + optional<std::function<optional<float>()>> tilt_f_; + bool assumed_state_{false}; + bool optimistic_{false}; + Trigger<> *open_trigger_; + Trigger<> *close_trigger_; + Trigger<> *stop_trigger_; + Trigger<> *prev_command_trigger_{nullptr}; + Trigger<float> *position_trigger_; + bool has_position_{false}; + Trigger<float> *tilt_trigger_; + bool has_tilt_{false}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/components/template/number/__init__.py b/components/template/number/__init__.py new file mode 100644 index 0000000..3dec706 --- /dev/null +++ b/components/template/number/__init__.py @@ -0,0 +1,93 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + CONF_ID, + CONF_INITIAL_VALUE, + CONF_LAMBDA, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, + CONF_STEP, +) +from .. import template_ns + +TemplateNumber = template_ns.class_( + "TemplateNumber", number.Number, cg.PollingComponent +) + +CONF_SET_ACTION = "set_action" + + +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid("max_value must be greater than min_value") + return config + + +def validate(config): + if CONF_LAMBDA in config: + if config[CONF_OPTIMISTIC]: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_VALUE in config: + raise cv.Invalid("initial_value cannot be used with lambda") + if CONF_RESTORE_VALUE in config: + raise cv.Invalid("restore_value cannot be used with lambda") + elif CONF_INITIAL_VALUE not in config: + config[CONF_INITIAL_VALUE] = config[CONF_MIN_VALUE] + + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the number being set." + ) + return config + + +CONFIG_SCHEMA = cv.All( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateNumber), + cv.Required(CONF_MAX_VALUE): cv.float_, + cv.Required(CONF_MIN_VALUE): cv.float_, + cv.Required(CONF_STEP): cv.positive_float, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_INITIAL_VALUE): cv.float_, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ).extend(cv.polling_component_schema("60s")), + validate_min_max, + validate, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await number.register_number( + var, + config, + min_value=config[CONF_MIN_VALUE], + max_value=config[CONF_MAX_VALUE], + step=config[CONF_STEP], + ) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(float) + ) + cg.add(var.set_template(template_)) + + else: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) + if CONF_RESTORE_VALUE in config: + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [(float, "x")], config[CONF_SET_ACTION] + ) diff --git a/components/template/number/template_number.cpp b/components/template/number/template_number.cpp new file mode 100644 index 0000000..90157b7 --- /dev/null +++ b/components/template/number/template_number.cpp @@ -0,0 +1,62 @@ +#include "template_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.number"; + +void TemplateNumber::setup() { + if (this->f_.has_value()) + return; + + float value; + if (!this->restore_value_) { + value = this->initial_value_; + } else { + + if ( this->has_forced_hash ) { + this->pref_ = global_preferences->make_preference<float>(this->forced_hash); + } else { + this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash()); + } + + + if (!this->pref_.load(&value)) { + if (!std::isnan(this->initial_value_)) + value = this->initial_value_; + else + value = this->traits.get_min_value(); + } + } + this->publish_state(value); +} + +void TemplateNumber::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->publish_state(*val); +} + +void TemplateNumber::control(float value) { + this->set_trigger_->trigger(value); + + if (this->optimistic_) + this->publish_state(value); + + if (this->restore_value_) + this->pref_.save(&value); +} +void TemplateNumber::dump_config() { + LOG_NUMBER("", "Template Number", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome diff --git a/components/template/number/template_number.h b/components/template/number/template_number.h new file mode 100644 index 0000000..9a82e44 --- /dev/null +++ b/components/template/number/template_number.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace template_ { + +class TemplateNumber : public number::Number, public PollingComponent { + public: + void set_template(std::function<optional<float>()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger<float> *get_set_trigger() const { return set_trigger_; } + void set_optimistic(bool optimistic) { optimistic_ = optimistic; } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(float value) override; + bool optimistic_{false}; + float initial_value_{NAN}; + bool restore_value_{false}; + Trigger<float> *set_trigger_ = new Trigger<float>(); + optional<std::function<optional<float>()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome diff --git a/components/template/output/__init__.py b/components/template/output/__init__.py new file mode 100644 index 0000000..b42a4be --- /dev/null +++ b/components/template/output/__init__.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import output +from esphome.const import CONF_ID, CONF_TYPE, CONF_BINARY +from .. import template_ns + +TemplateBinaryOutput = template_ns.class_("TemplateBinaryOutput", output.BinaryOutput) +TemplateFloatOutput = template_ns.class_("TemplateFloatOutput", output.FloatOutput) + +CONF_FLOAT = "float" +CONF_WRITE_ACTION = "write_action" + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_BINARY: output.BINARY_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateBinaryOutput), + cv.Required(CONF_WRITE_ACTION): automation.validate_automation( + single=True + ), + } + ), + CONF_FLOAT: output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateFloatOutput), + cv.Required(CONF_WRITE_ACTION): automation.validate_automation( + single=True + ), + } + ), + }, + lower=True, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if config[CONF_TYPE] == CONF_BINARY: + await automation.build_automation( + var.get_trigger(), [(bool, "state")], config[CONF_WRITE_ACTION] + ) + else: + await automation.build_automation( + var.get_trigger(), [(float, "state")], config[CONF_WRITE_ACTION] + ) + await output.register_output(var, config) diff --git a/components/template/output/template_output.h b/components/template/output/template_output.h new file mode 100644 index 0000000..90de801 --- /dev/null +++ b/components/template/output/template_output.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace template_ { + +class TemplateBinaryOutput : public output::BinaryOutput { + public: + Trigger<bool> *get_trigger() const { return trigger_; } + + protected: + void write_state(bool state) override { this->trigger_->trigger(state); } + + Trigger<bool> *trigger_ = new Trigger<bool>(); +}; + +class TemplateFloatOutput : public output::FloatOutput { + public: + Trigger<float> *get_trigger() const { return trigger_; } + + protected: + void write_state(float state) override { this->trigger_->trigger(state); } + + Trigger<float> *trigger_ = new Trigger<float>(); +}; + +} // namespace template_ +} // namespace esphome diff --git a/components/template/select/__init__.py b/components/template/select/__init__.py new file mode 100644 index 0000000..4eba771 --- /dev/null +++ b/components/template/select/__init__.py @@ -0,0 +1,84 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select +from esphome.const import ( + CONF_ID, + CONF_INITIAL_OPTION, + CONF_LAMBDA, + CONF_OPTIONS, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, +) +from .. import template_ns + +TemplateSelect = template_ns.class_( + "TemplateSelect", select.Select, cg.PollingComponent +) + +CONF_SET_ACTION = "set_action" + + +def validate(config): + if CONF_LAMBDA in config: + if config[CONF_OPTIMISTIC]: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_OPTION in config: + raise cv.Invalid("initial_value cannot be used with lambda") + if CONF_RESTORE_VALUE in config: + raise cv.Invalid("restore_value cannot be used with lambda") + elif CONF_INITIAL_OPTION in config: + if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: + raise cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + ) + else: + config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] + + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + ) + return config + + +CONFIG_SCHEMA = cv.All( + select.SELECT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateSelect), + cv.Required(CONF_OPTIONS): cv.All( + cv.ensure_list(cv.string_strict), cv.Length(min=1) + ), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_INITIAL_OPTION): cv.string_strict, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ).extend(cv.polling_component_schema("60s")), + validate, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await select.register_select(var, config, options=config[CONF_OPTIONS]) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) + ) + cg.add(var.set_template(template_)) + + else: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) + + if CONF_RESTORE_VALUE in config: + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION] + ) diff --git a/components/template/select/template_select.cpp b/components/template/select/template_select.cpp new file mode 100644 index 0000000..4efa306 --- /dev/null +++ b/components/template/select/template_select.cpp @@ -0,0 +1,80 @@ +#include "template_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.select"; + +void TemplateSelect::setup() { + if (this->f_.has_value()) + return; + + std::string value; + ESP_LOGD(TAG, "Setting up Template Select"); + if (!this->restore_value_) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial: %s", value.c_str()); + } else { + size_t index; + + if ( this->has_forced_hash ) { + this->pref_ = global_preferences->make_preference<size_t>(this->forced_hash); + } else { + this->pref_ = global_preferences->make_preference<size_t>(this->get_object_id_hash()); + } + + if (!this->pref_.load(&index)) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial (could not load): %s", value.c_str()); + } else { + value = this->traits.get_options().at(index); + ESP_LOGD(TAG, "State from restore: %s", value.c_str()); + } + } + + this->publish_state(value); +} + +void TemplateSelect::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + auto options = this->traits.get_options(); + if (std::find(options.begin(), options.end(), *val) == options.end()) { + ESP_LOGE(TAG, "lambda returned an invalid option %s", (*val).c_str()); + return; + } + + this->publish_state(*val); +} + +void TemplateSelect::control(const std::string &value) { + this->set_trigger_->trigger(value); + + if (this->optimistic_) + this->publish_state(value); + + if (this->restore_value_) { + auto options = this->traits.get_options(); + size_t index = std::find(options.begin(), options.end(), value) - options.begin(); + + this->pref_.save(&index); + } +} +void TemplateSelect::dump_config() { + LOG_SELECT("", "Template Select", this); + LOG_UPDATE_INTERVAL(this); + if (this->f_.has_value()) + return; + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + ESP_LOGCONFIG(TAG, " Initial Option: %s", this->initial_option_.c_str()); + ESP_LOGCONFIG(TAG, " Restore Value: %s", YESNO(this->restore_value_)); +} + +} // namespace template_ +} // namespace esphome diff --git a/components/template/select/template_select.h b/components/template/select/template_select.h new file mode 100644 index 0000000..2f00765 --- /dev/null +++ b/components/template/select/template_select.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace template_ { + +class TemplateSelect : public select::Select, public PollingComponent { + public: + void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_initial_option(const std::string &initial_option) { this->initial_option_ = initial_option; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(const std::string &value) override; + bool optimistic_ = false; + std::string initial_option_; + bool restore_value_ = false; + Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); + optional<std::function<optional<std::string>()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome diff --git a/components/template/sensor/__init__.py b/components/template/sensor/__init__.py new file mode 100644 index 0000000..75fb505 --- /dev/null +++ b/components/template/sensor/__init__.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_STATE, + STATE_CLASS_NONE, +) +from .. import template_ns + +TemplateSensor = template_ns.class_( + "TemplateSensor", sensor.Sensor, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(TemplateSensor), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(float) + ) + cg.add(var.set_template(template_)) + + [email protected]_action( + "sensor.template.publish", + sensor.SensorPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_STATE): cv.templatable(cv.float_), + } + ), +) +async def sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_STATE], args, float) + cg.add(var.set_state(template_)) + return var diff --git a/components/template/sensor/template_sensor.cpp b/components/template/sensor/template_sensor.cpp new file mode 100644 index 0000000..b28eb3f --- /dev/null +++ b/components/template/sensor/template_sensor.cpp @@ -0,0 +1,28 @@ +#include "template_sensor.h" +#include "esphome/core/log.h" +#include <cmath> + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.sensor"; + +void TemplateSensor::update() { + if (this->f_.has_value()) { + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } + } else if (!std::isnan(this->get_raw_state())) { + this->publish_state(this->get_raw_state()); + } +} +float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void TemplateSensor::set_template(std::function<optional<float>()> &&f) { this->f_ = f; } +void TemplateSensor::dump_config() { + LOG_SENSOR("", "Template Sensor", this); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome diff --git a/components/template/sensor/template_sensor.h b/components/template/sensor/template_sensor.h new file mode 100644 index 0000000..2630cb0 --- /dev/null +++ b/components/template/sensor/template_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace template_ { + +class TemplateSensor : public sensor::Sensor, public PollingComponent { + public: + void set_template(std::function<optional<float>()> &&f); + + void update() override; + + void dump_config() override; + + float get_setup_priority() const override; + + protected: + optional<std::function<optional<float>()>> f_; +}; + +} // namespace template_ +} // namespace esphome diff --git a/components/template/switch/__init__.py b/components/template/switch/__init__.py new file mode 100644 index 0000000..6095a7c --- /dev/null +++ b/components/template/switch/__init__.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import switch +from esphome.const import ( + CONF_ASSUMED_STATE, + CONF_ID, + CONF_LAMBDA, + CONF_OPTIMISTIC, + CONF_RESTORE_STATE, + CONF_STATE, + CONF_TURN_OFF_ACTION, + CONF_TURN_ON_ACTION, +) +from .. import template_ns + +TemplateSwitch = template_ns.class_("TemplateSwitch", switch.Switch, cg.Component) + + +def validate(config): + if ( + not config[CONF_OPTIMISTIC] + and CONF_TURN_ON_ACTION not in config + and CONF_TURN_OFF_ACTION not in config + ): + raise cv.Invalid( + "Either optimistic mode must be enabled, or turn_on_action or turn_off_action must be set, " + "to handle the switch being set." + ) + return config + + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateSwitch), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, + } + ).extend(cv.COMPONENT_SCHEMA), + validate, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await switch.register_switch(var, config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(bool) + ) + cg.add(var.set_state_lambda(template_)) + if CONF_TURN_OFF_ACTION in config: + await automation.build_automation( + var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION] + ) + if CONF_TURN_ON_ACTION in config: + await automation.build_automation( + var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION] + ) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) + cg.add(var.set_restore_state(config[CONF_RESTORE_STATE])) + + [email protected]_action( + "switch.template.publish", + switch.SwitchPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(switch.Switch), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } + ), +) +async def switch_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_STATE], args, bool) + cg.add(var.set_state(template_)) + return var diff --git a/components/template/switch/template_switch.cpp b/components/template/switch/template_switch.cpp new file mode 100644 index 0000000..b3e545d --- /dev/null +++ b/components/template/switch/template_switch.cpp @@ -0,0 +1,66 @@ +#include "template_switch.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.switch"; + +TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} + +void TemplateSwitch::loop() { + if (!this->f_.has_value()) + return; + auto s = (*this->f_)(); + if (!s.has_value()) + return; + + this->publish_state(*s); +} +void TemplateSwitch::write_state(bool state) { + if (this->prev_trigger_ != nullptr) { + this->prev_trigger_->stop_action(); + } + + if (state) { + this->prev_trigger_ = this->turn_on_trigger_; + this->turn_on_trigger_->trigger(); + } else { + this->prev_trigger_ = this->turn_off_trigger_; + this->turn_off_trigger_->trigger(); + } + + if (this->optimistic_) + this->publish_state(state); +} +void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } +bool TemplateSwitch::assumed_state() { return this->assumed_state_; } +void TemplateSwitch::set_state_lambda(std::function<optional<bool>()> &&f) { this->f_ = f; } +float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } +Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } +Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } +void TemplateSwitch::setup() { + if (!this->restore_state_) + return; + + auto restored = this->get_initial_state(); + if (!restored.has_value()) + return; + + ESP_LOGD(TAG, " Restored state %s", ONOFF(*restored)); + if (*restored) { + this->turn_on(); + } else { + this->turn_off(); + } +} +void TemplateSwitch::dump_config() { + LOG_SWITCH("", "Template Switch", this); + ESP_LOGCONFIG(TAG, " Restore State: %s", YESNO(this->restore_state_)); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} +void TemplateSwitch::set_restore_state(bool restore_state) { this->restore_state_ = restore_state; } +void TemplateSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } + +} // namespace template_ +} // namespace esphome diff --git a/components/template/switch/template_switch.h b/components/template/switch/template_switch.h new file mode 100644 index 0000000..ef9b567 --- /dev/null +++ b/components/template/switch/template_switch.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace template_ { + +class TemplateSwitch : public switch_::Switch, public Component { + public: + TemplateSwitch(); + + void setup() override; + void dump_config() override; + + void set_state_lambda(std::function<optional<bool>()> &&f); + void set_restore_state(bool restore_state); + Trigger<> *get_turn_on_trigger() const; + Trigger<> *get_turn_off_trigger() const; + void set_optimistic(bool optimistic); + void set_assumed_state(bool assumed_state); + void loop() override; + + float get_setup_priority() const override; + + protected: + bool assumed_state() override; + + void write_state(bool state) override; + + optional<std::function<optional<bool>()>> f_; + bool optimistic_{false}; + bool assumed_state_{false}; + Trigger<> *turn_on_trigger_; + Trigger<> *turn_off_trigger_; + Trigger<> *prev_trigger_{nullptr}; + bool restore_state_{false}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/components/template/text_sensor/__init__.py b/components/template/text_sensor/__init__.py new file mode 100644 index 0000000..2e098a7 --- /dev/null +++ b/components/template/text_sensor/__init__.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import text_sensor +from esphome.components.text_sensor import TextSensorPublishAction +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE +from .. import template_ns + +TemplateTextSensor = template_ns.class_( + "TemplateTextSensor", text_sensor.TextSensor, cg.PollingComponent +) + +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateTextSensor), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) + ) + cg.add(var.set_template(template_)) + + [email protected]_action( + "text_sensor.template.publish", + TextSensorPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(text_sensor.TextSensor), + cv.Required(CONF_STATE): cv.templatable(cv.string_strict), + } + ), +) +async def text_sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_STATE], args, cg.std_string) + cg.add(var.set_state(template_)) + return var diff --git a/components/template/text_sensor/template_text_sensor.cpp b/components/template/text_sensor/template_text_sensor.cpp new file mode 100644 index 0000000..83bebb5 --- /dev/null +++ b/components/template/text_sensor/template_text_sensor.cpp @@ -0,0 +1,24 @@ +#include "template_text_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.text_sensor"; + +void TemplateTextSensor::update() { + if (this->f_.has_value()) { + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } + } else if (this->has_state()) { + this->publish_state(this->state); + } +} +float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void TemplateTextSensor::set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; } +void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } + +} // namespace template_ +} // namespace esphome diff --git a/components/template/text_sensor/template_text_sensor.h b/components/template/text_sensor/template_text_sensor.h new file mode 100644 index 0000000..07a2bd9 --- /dev/null +++ b/components/template/text_sensor/template_text_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace template_ { + +class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { + public: + void set_template(std::function<optional<std::string>()> &&f); + + void update() override; + + float get_setup_priority() const override; + + void dump_config() override; + + protected: + optional<std::function<optional<std::string>()>> f_{}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/components/total_daily_energy/sensor.py b/components/total_daily_energy/sensor.py new file mode 100644 index 0000000..9ee1476 --- /dev/null +++ b/components/total_daily_energy/sensor.py @@ -0,0 +1,100 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, time +from esphome.const import ( + CONF_ICON, + CONF_ID, + CONF_RESTORE, + CONF_TIME_ID, + DEVICE_CLASS_ENERGY, + CONF_METHOD, + STATE_CLASS_TOTAL_INCREASING, + CONF_UNIT_OF_MEASUREMENT, + CONF_ACCURACY_DECIMALS, +) +from esphome.core.entity_helpers import inherit_property_from + +DEPENDENCIES = ["time"] + +CONF_POWER_ID = "power_id" +CONF_MIN_SAVE_INTERVAL = "min_save_interval" +total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy") +TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod") +TOTAL_DAILY_ENERGY_METHODS = { + "trapezoid": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID, + "left": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_LEFT, + "right": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_RIGHT, +} +TotalDailyEnergy = total_daily_energy_ns.class_( + "TotalDailyEnergy", sensor.Sensor, cg.Component +) + + +def inherit_unit_of_measurement(uom, config): + return uom + "h" + + +def inherit_accuracy_decimals(decimals, config): + return decimals + 2 + + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(TotalDailyEnergy), + cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), + cv.Optional(CONF_RESTORE, default=True): cv.boolean, + cv.Optional( + CONF_MIN_SAVE_INTERVAL, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_METHOD, default="right"): cv.enum( + TOTAL_DAILY_ENERGY_METHODS, lower=True + ), + cv.Optional("forced_hash"): cv.int_, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(TotalDailyEnergy), + cv.Optional(CONF_ICON): cv.icon, + cv.Optional(CONF_UNIT_OF_MEASUREMENT): sensor.validate_unit_of_measurement, + cv.Optional(CONF_ACCURACY_DECIMALS): sensor.validate_accuracy_decimals, + cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), + }, + extra=cv.ALLOW_EXTRA, + ), + inherit_property_from(CONF_ICON, CONF_POWER_ID), + inherit_property_from( + CONF_UNIT_OF_MEASUREMENT, CONF_POWER_ID, transform=inherit_unit_of_measurement + ), + inherit_property_from( + CONF_ACCURACY_DECIMALS, CONF_POWER_ID, transform=inherit_accuracy_decimals + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + sens = await cg.get_variable(config[CONF_POWER_ID]) + cg.add(var.set_parent(sens)) + time_ = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time(time_)) + cg.add(var.set_restore(config[CONF_RESTORE])) + cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) + cg.add(var.set_method(config[CONF_METHOD])) + + if "forced_hash" in config: + cg.add(var.set_forced_hash(config["forced_hash"])) diff --git a/components/total_daily_energy/total_daily_energy.cpp b/components/total_daily_energy/total_daily_energy.cpp new file mode 100644 index 0000000..c88d8c2 --- /dev/null +++ b/components/total_daily_energy/total_daily_energy.cpp @@ -0,0 +1,86 @@ +#include "total_daily_energy.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace total_daily_energy { + +static const char *const TAG = "total_daily_energy"; + +void TotalDailyEnergy::setup() { + float initial_value = 0; + + if (this->restore_) { + + + if ( this->has_forced_hash ) { + this->pref_ = global_preferences->make_preference<float>(this->forced_hash); + } else { + this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash()); + } + + this->pref_.load(&initial_value); + } + this->publish_state_and_save(initial_value); + + this->last_update_ = millis(); + this->last_save_ = this->last_update_; + + this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); +} + +void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); } + +void TotalDailyEnergy::loop() { + auto t = this->time_->now(); + if (!t.is_valid()) + return; + + if (this->last_day_of_year_ == 0) { + this->last_day_of_year_ = t.day_of_year; + return; + } + + if (t.day_of_year != this->last_day_of_year_) { + this->last_day_of_year_ = t.day_of_year; + this->total_energy_ = 0; + this->publish_state_and_save(0); + } +} + +void TotalDailyEnergy::publish_state_and_save(float state) { + this->total_energy_ = state; + this->publish_state(state); + const uint32_t now = millis(); + if (now - this->last_save_ < this->min_save_interval_) { + return; + } + this->last_save_ = now; + this->pref_.save(&state); +} + +void TotalDailyEnergy::process_new_state_(float state) { + if (std::isnan(state)) + return; + const uint32_t now = millis(); + const float old_state = this->last_power_state_; + const float new_state = state; + float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f; + float delta_energy = 0.0f; + switch (this->method_) { + case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID: + delta_energy = delta_hours * (old_state + new_state) / 2.0; + break; + case TOTAL_DAILY_ENERGY_METHOD_LEFT: + delta_energy = delta_hours * old_state; + break; + case TOTAL_DAILY_ENERGY_METHOD_RIGHT: + delta_energy = delta_hours * new_state; + break; + } + this->last_power_state_ = new_state; + this->last_update_ = now; + this->publish_state_and_save(this->total_energy_ + delta_energy); +} + +} // namespace total_daily_energy +} // namespace esphome diff --git a/components/total_daily_energy/total_daily_energy.h b/components/total_daily_energy/total_daily_energy.h new file mode 100644 index 0000000..f091bf9 --- /dev/null +++ b/components/total_daily_energy/total_daily_energy.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace total_daily_energy { + +enum TotalDailyEnergyMethod { + TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0, + TOTAL_DAILY_ENERGY_METHOD_LEFT, + TOTAL_DAILY_ENERGY_METHOD_RIGHT, +}; + +class TotalDailyEnergy : public sensor::Sensor, public Component { + public: + void set_restore(bool restore) { restore_ = restore; } + void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; } + void set_time(time::RealTimeClock *time) { time_ = time; } + void set_parent(Sensor *parent) { parent_ = parent; } + void set_method(TotalDailyEnergyMethod method) { method_ = method; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void publish_state_and_save(float state); + + + bool has_forced_hash = false; + uint32_t forced_hash = 0; + void set_forced_hash(uint32_t hash_value) { + forced_hash = hash_value; + has_forced_hash = true; + } + + protected: + void process_new_state_(float state); + + ESPPreferenceObject pref_; + time::RealTimeClock *time_; + Sensor *parent_; + TotalDailyEnergyMethod method_; + uint16_t last_day_of_year_{}; + uint32_t last_update_{0}; + uint32_t last_save_{0}; + uint32_t min_save_interval_{0}; + bool restore_; + float total_energy_{0.0f}; + float last_power_state_{0.0f}; +}; + +} // namespace total_daily_energy +} // namespace esphome |