diff options
author | Kaufman Home Automation <[email protected]> | 2022-02-06 16:44:35 -0700 |
---|---|---|
committer | GitHub <[email protected]> | 2022-02-06 16:44:35 -0700 |
commit | 4d30b8651378f1ae60a4ff04536bd2244b1c5750 (patch) | |
tree | 4622c45fb5c1df6c602ca94a666c0570a4d1e73b | |
parent | fff5ec94a8d813d0e89c8a7dec0090b115c84046 (diff) | |
download | PLF10-4d30b8651378f1ae60a4ff04536bd2244b1c5750.tar.gz PLF10-4d30b8651378f1ae60a4ff04536bd2244b1c5750.zip |
v1.84 upload
-rw-r--r-- | components/number/__init__.py | 182 | ||||
-rw-r--r-- | components/number/automation.cpp | 47 | ||||
-rw-r--r-- | components/number/automation.h | 76 | ||||
-rw-r--r-- | components/number/number.cpp | 56 | ||||
-rw-r--r-- | components/number/number.h | 118 | ||||
-rw-r--r-- | components/select/__init__.py | 100 | ||||
-rw-r--r-- | components/select/automation.h | 33 | ||||
-rw-r--r-- | components/select/select.cpp | 43 | ||||
-rw-r--r-- | components/select/select.h | 93 | ||||
-rw-r--r-- | components/switch/__init__.py | 117 | ||||
-rw-r--r-- | components/switch/automation.cpp | 10 | ||||
-rw-r--r-- | components/switch/automation.h | 84 | ||||
-rw-r--r-- | components/switch/switch.cpp | 55 | ||||
-rw-r--r-- | components/switch/switch.h | 119 |
14 files changed, 1133 insertions, 0 deletions
diff --git a/components/number/__init__.py b/components/number/__init__.py new file mode 100644 index 0000000..d93f01a --- /dev/null +++ b/components/number/__init__.py @@ -0,0 +1,182 @@ +from typing import Optional +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_ABOVE, + CONF_BELOW, + CONF_ID, + CONF_MODE, + CONF_ON_VALUE, + CONF_ON_VALUE_RANGE, + CONF_TRIGGER_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_MQTT_ID, + CONF_VALUE, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +number_ns = cg.esphome_ns.namespace("number") +Number = number_ns.class_("Number", cg.EntityBase) +NumberPtr = Number.operator("ptr") + +# Triggers +NumberStateTrigger = number_ns.class_( + "NumberStateTrigger", automation.Trigger.template(cg.float_) +) +ValueRangeTrigger = number_ns.class_( + "ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component +) + +# Actions +NumberSetAction = number_ns.class_("NumberSetAction", automation.Action) + +# Conditions +NumberInRangeCondition = number_ns.class_( + "NumberInRangeCondition", automation.Condition +) + +NumberMode = number_ns.enum("NumberMode") + +NUMBER_MODES = { + "AUTO": NumberMode.NUMBER_MODE_AUTO, + "BOX": NumberMode.NUMBER_MODE_BOX, + "SLIDER": NumberMode.NUMBER_MODE_SLIDER, +} + +icon = cv.icon + +NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), + cv.GenerateID(): cv.declare_id(Number), + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NumberStateTrigger), + } + ), + cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), + cv.Optional(CONF_ABOVE): cv.float_, + cv.Optional(CONF_BELOW): cv.float_, + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), + ), + cv.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string_strict, + cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True), + cv.Optional("forced_hash"): cv.int_, + + } +) + + +async def setup_number_core_( + var, config, *, min_value: float, max_value: float, step: Optional[float] +): + await setup_entity(var, config) + + cg.add(var.traits.set_min_value(min_value)) + cg.add(var.traits.set_max_value(max_value)) + if step is not None: + cg.add(var.traits.set_step(step)) + + cg.add(var.traits.set_mode(config[CONF_MODE])) + + if "forced_hash" in config: + cg.add(var.set_forced_hash(config["forced_hash"])) + + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if CONF_ABOVE in conf: + template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if CONF_BELOW in conf: + template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + if CONF_UNIT_OF_MEASUREMENT in config: + cg.add(var.traits.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_number( + var, config, *, min_value: float, max_value: float, step: Optional[float] = None +): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_number(var)) + await setup_number_core_( + var, config, min_value=min_value, max_value=max_value, step=step + ) + + +async def new_number( + config, *, min_value: float, max_value: float, step: Optional[float] = None +): + var = cg.new_Pvariable(config[CONF_ID]) + await register_number( + var, config, min_value=min_value, max_value=max_value, step=step + ) + return var + + +NUMBER_IN_RANGE_CONDITION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(Number), + cv.Optional(CONF_ABOVE): cv.float_, + cv.Optional(CONF_BELOW): cv.float_, + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), +) + + [email protected]_condition( + "number.in_range", NumberInRangeCondition, NUMBER_IN_RANGE_CONDITION_SCHEMA +) +async def number_in_range_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(condition_id, template_arg, paren) + + if CONF_ABOVE in config: + cg.add(var.set_min(config[CONF_ABOVE])) + if CONF_BELOW in config: + cg.add(var.set_max(config[CONF_BELOW])) + + return var + + +@coroutine_with_priority(40.0) +async def to_code(config): + cg.add_define("USE_NUMBER") + cg.add_global(number_ns.using) + + [email protected]_action( + "number.set", + NumberSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Number), + cv.Required(CONF_VALUE): cv.templatable(cv.float_), + } + ), +) +async def number_set_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_VALUE], args, float) + cg.add(var.set_value(template_)) + return var diff --git a/components/number/automation.cpp b/components/number/automation.cpp new file mode 100644 index 0000000..c75d272 --- /dev/null +++ b/components/number/automation.cpp @@ -0,0 +1,47 @@ +#include "automation.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number.automation"; + +void ValueRangeTrigger::setup() { + this->rtc_ = global_preferences->make_preference<bool>(this->parent_->get_object_id_hash()); + bool initial_state; + if (this->rtc_.load(&initial_state)) { + this->previous_in_range_ = initial_state; + } + + this->parent_->add_on_state_callback([this](float state) { this->on_state_(state); }); +} +float ValueRangeTrigger::get_setup_priority() const { return setup_priority::HARDWARE; } + +void ValueRangeTrigger::on_state_(float state) { + if (std::isnan(state)) + return; + + float local_min = this->min_.value(state); + float local_max = this->max_.value(state); + + bool in_range; + if (std::isnan(local_min) && std::isnan(local_max)) { + in_range = this->previous_in_range_; + } else if (std::isnan(local_min)) { + in_range = state <= local_max; + } else if (std::isnan(local_max)) { + in_range = state >= local_min; + } else { + in_range = local_min <= state && state <= local_max; + } + + if (in_range != this->previous_in_range_ && in_range) { + this->trigger(state); + } + + this->previous_in_range_ = in_range; + this->rtc_.save(&in_range); +} + +} // namespace number +} // namespace esphome diff --git a/components/number/automation.h b/components/number/automation.h new file mode 100644 index 0000000..98554a3 --- /dev/null +++ b/components/number/automation.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace number { + +class NumberStateTrigger : public Trigger<float> { + public: + explicit NumberStateTrigger(Number *parent) { + parent->add_on_state_callback([this](float value) { this->trigger(value); }); + } +}; + +template<typename... Ts> class NumberSetAction : public Action<Ts...> { + public: + NumberSetAction(Number *number) : number_(number) {} + TEMPLATABLE_VALUE(float, value) + + void play(Ts... x) override { + auto call = this->number_->make_call(); + call.set_value(this->value_.value(x...)); + call.perform(); + } + + protected: + Number *number_; +}; + +class ValueRangeTrigger : public Trigger<float>, public Component { + public: + explicit ValueRangeTrigger(Number *parent) : parent_(parent) {} + + template<typename V> void set_min(V min) { this->min_ = min; } + template<typename V> void set_max(V max) { this->max_ = max; } + + void setup() override; + float get_setup_priority() const override; + + protected: + void on_state_(float state); + + Number *parent_; + ESPPreferenceObject rtc_; + bool previous_in_range_{false}; + TemplatableValue<float, float> min_{NAN}; + TemplatableValue<float, float> max_{NAN}; +}; + +template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...> { + public: + NumberInRangeCondition(Number *parent) : parent_(parent) {} + + void set_min(float min) { this->min_ = min; } + void set_max(float max) { this->max_ = max; } + bool check(Ts... x) override { + const float state = this->parent_->state; + if (std::isnan(this->min_)) { + return state <= this->max_; + } else if (std::isnan(this->max_)) { + return state >= this->min_; + } else { + return this->min_ <= state && state <= this->max_; + } + } + + protected: + Number *parent_; + float min_{NAN}; + float max_{NAN}; +}; + +} // namespace number +} // namespace esphome diff --git a/components/number/number.cpp b/components/number/number.cpp new file mode 100644 index 0000000..99a2c04 --- /dev/null +++ b/components/number/number.cpp @@ -0,0 +1,56 @@ +#include "number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +void NumberCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + if (!this->value_.has_value() || std::isnan(*this->value_)) { + ESP_LOGW(TAG, "No value set for NumberCall"); + return; + } + + const auto &traits = this->parent_->traits; + auto value = *this->value_; + + float min_value = traits.get_min_value(); + if (value < min_value) { + ESP_LOGW(TAG, " Value %f must not be less than minimum %f", value, min_value); + return; + } + float max_value = traits.get_max_value(); + if (value > max_value) { + ESP_LOGW(TAG, " Value %f must not be greater than maximum %f", value, max_value); + return; + } + ESP_LOGD(TAG, " Value: %f", *this->value_); + this->parent_->control(*this->value_); +} + +void Number::publish_state(float state) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); + this->state_callback_.call(state); +} + +void Number::add_on_state_callback(std::function<void(float)> &&callback) { + this->state_callback_.add(std::move(callback)); +} + +std::string NumberTraits::get_unit_of_measurement() { + if (this->unit_of_measurement_.has_value()) + return *this->unit_of_measurement_; + return ""; +} +void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { + this->unit_of_measurement_ = unit_of_measurement; +} + +uint32_t Number::hash_base() { return 2282307003UL; } + +} // namespace number +} // namespace esphome diff --git a/components/number/number.h b/components/number/number.h new file mode 100644 index 0000000..2cdcac8 --- /dev/null +++ b/components/number/number.h @@ -0,0 +1,118 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace number { + +#define LOG_NUMBER(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + if (!(obj)->traits.get_unit_of_measurement().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->traits.get_unit_of_measurement().c_str()); \ + } \ + } + +class Number; + +class NumberCall { + public: + explicit NumberCall(Number *parent) : parent_(parent) {} + void perform(); + + NumberCall &set_value(float value) { + value_ = value; + return *this; + } + const optional<float> &get_value() const { return value_; } + + protected: + Number *const parent_; + optional<float> value_; +}; + +enum NumberMode : uint8_t { + NUMBER_MODE_AUTO = 0, + NUMBER_MODE_BOX = 1, + NUMBER_MODE_SLIDER = 2, +}; + +class NumberTraits { + public: + void set_min_value(float min_value) { min_value_ = min_value; } + float get_min_value() const { return min_value_; } + void set_max_value(float max_value) { max_value_ = max_value; } + float get_max_value() const { return max_value_; } + void set_step(float step) { step_ = step; } + float get_step() const { return step_; } + + /// Get the unit of measurement, using the manual override if set. + std::string get_unit_of_measurement(); + /// Manually set the unit of measurement. + void set_unit_of_measurement(const std::string &unit_of_measurement); + + // Get/set the frontend mode. + NumberMode get_mode() const { return this->mode_; } + void set_mode(NumberMode mode) { this->mode_ = mode; } + + protected: + float min_value_ = NAN; + float max_value_ = NAN; + float step_ = NAN; + optional<std::string> unit_of_measurement_; ///< Unit of measurement override + NumberMode mode_{NUMBER_MODE_AUTO}; +}; + +/** Base-class for all numbers. + * + * A number can use publish_state to send out a new value. + */ +class Number : public EntityBase { + public: + float state; + + void publish_state(float state); + + NumberCall make_call() { return NumberCall(this); } + void set(float value) { make_call().set_value(value).perform(); } + + void add_on_state_callback(std::function<void(float)> &&callback); + + NumberTraits traits; + + /// Return whether this number has gotten a full state yet. + bool has_state() const { return has_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: + friend class NumberCall; + + /** Set the value of the number, this is a virtual method that each number integration must implement. + * + * This method is called by the NumberCall. + * + * @param value The value as validated by the NumberCall. + */ + virtual void control(float value) = 0; + + uint32_t hash_base() override; + + CallbackManager<void(float)> state_callback_; + bool has_state_{false}; +}; + +} // namespace number +} // namespace esphome diff --git a/components/select/__init__.py b/components/select/__init__.py new file mode 100644 index 0000000..7f9e07f --- /dev/null +++ b/components/select/__init__.py @@ -0,0 +1,100 @@ +from typing import List +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_ID, + CONF_ON_VALUE, + CONF_OPTION, + CONF_TRIGGER_ID, + CONF_MQTT_ID, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +select_ns = cg.esphome_ns.namespace("select") +Select = select_ns.class_("Select", cg.EntityBase) +SelectPtr = Select.operator("ptr") + +# Triggers +SelectStateTrigger = select_ns.class_( + "SelectStateTrigger", automation.Trigger.template(cg.float_) +) + +# Actions +SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) + +icon = cv.icon + +SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), + cv.GenerateID(): cv.declare_id(Select), + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SelectStateTrigger), + } + ), + cv.Optional("forced_hash"): cv.int_, + } +) + + +async def setup_select_core_(var, config, *, options: List[str]): + await setup_entity(var, config) + + cg.add(var.traits.set_options(options)) + + if "forced_hash" in config: + cg.add(var.set_forced_hash(config["forced_hash"])) + + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_select(var, config, *, options: List[str]): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_select(var)) + await setup_select_core_(var, config, options=options) + + +async def new_select(config, *, options: List[str]): + var = cg.new_Pvariable(config[CONF_ID]) + await register_select(var, config, options=options) + + return var + + +@coroutine_with_priority(40.0) +async def to_code(config): + cg.add_define("USE_SELECT") + cg.add_global(select_ns.using) + + [email protected]_action( + "select.set", + SelectSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Select), + cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), + } + ), +) +async def select_set_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_OPTION], args, cg.std_string) + cg.add(var.set_option(template_)) + + return var diff --git a/components/select/automation.h b/components/select/automation.h new file mode 100644 index 0000000..1e0bfed --- /dev/null +++ b/components/select/automation.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "select.h" + +namespace esphome { +namespace select { + +class SelectStateTrigger : public Trigger<std::string> { + public: + explicit SelectStateTrigger(Select *parent) { + parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); }); + } +}; + +template<typename... Ts> class SelectSetAction : public Action<Ts...> { + public: + SelectSetAction(Select *select) : select_(select) {} + TEMPLATABLE_VALUE(std::string, option) + + void play(Ts... x) override { + auto call = this->select_->make_call(); + call.set_option(this->option_.value(x...)); + call.perform(); + } + + protected: + Select *select_; +}; + +} // namespace select +} // namespace esphome diff --git a/components/select/select.cpp b/components/select/select.cpp new file mode 100644 index 0000000..14f4d92 --- /dev/null +++ b/components/select/select.cpp @@ -0,0 +1,43 @@ +#include "select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace select { + +static const char *const TAG = "select"; + +void SelectCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + if (!this->option_.has_value()) { + ESP_LOGW(TAG, "No value set for SelectCall"); + return; + } + + const auto &traits = this->parent_->traits; + auto value = *this->option_; + auto options = traits.get_options(); + + if (std::find(options.begin(), options.end(), value) == options.end()) { + ESP_LOGW(TAG, " Option %s is not a valid option.", value.c_str()); + return; + } + + ESP_LOGD(TAG, " Option: %s", (*this->option_).c_str()); + this->parent_->control(*this->option_); +} + +void Select::publish_state(const std::string &state) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); + this->state_callback_.call(state); +} + +void Select::add_on_state_callback(std::function<void(std::string)> &&callback) { + this->state_callback_.add(std::move(callback)); +} + +uint32_t Select::hash_base() { return 2812997003UL; } + +} // namespace select +} // namespace esphome diff --git a/components/select/select.h b/components/select/select.h new file mode 100644 index 0000000..17e8d24 --- /dev/null +++ b/components/select/select.h @@ -0,0 +1,93 @@ +#pragma once + +#include <set> +#include <utility> +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace select { + +#define LOG_SELECT(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +class Select; + +class SelectCall { + public: + explicit SelectCall(Select *parent) : parent_(parent) {} + void perform(); + + SelectCall &set_option(const std::string &option) { + option_ = option; + return *this; + } + const optional<std::string> &get_option() const { return option_; } + + protected: + Select *const parent_; + optional<std::string> option_; +}; + +class SelectTraits { + public: + void set_options(std::vector<std::string> options) { this->options_ = std::move(options); } + const std::vector<std::string> get_options() const { return this->options_; } + + protected: + std::vector<std::string> options_; +}; + +/** Base-class for all selects. + * + * A select can use publish_state to send out a new value. + */ +class Select : public EntityBase { + public: + std::string state; + + void publish_state(const std::string &state); + + SelectCall make_call() { return SelectCall(this); } + void set(const std::string &value) { make_call().set_option(value).perform(); } + + void add_on_state_callback(std::function<void(std::string)> &&callback); + + SelectTraits traits; + + /// Return whether this select has gotten a full state yet. + bool has_state() const { return has_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: + friend class SelectCall; + + /** Set the value of the select, this is a virtual method that each select integration must implement. + * + * This method is called by the SelectCall. + * + * @param value The value as validated by the SelectCall. + */ + virtual void control(const std::string &value) = 0; + + uint32_t hash_base() override; + + CallbackManager<void(std::string)> state_callback_; + bool has_state_{false}; +}; + +} // namespace select +} // namespace esphome diff --git a/components/switch/__init__.py b/components/switch/__init__.py new file mode 100644 index 0000000..9d07b33 --- /dev/null +++ b/components/switch/__init__.py @@ -0,0 +1,117 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import Condition, maybe_simple_id +from esphome.components import mqtt +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_ON_TURN_OFF, + CONF_ON_TURN_ON, + CONF_TRIGGER_ID, + CONF_MQTT_ID, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +switch_ns = cg.esphome_ns.namespace("switch_") +Switch = switch_ns.class_("Switch", cg.EntityBase) +SwitchPtr = Switch.operator("ptr") + +ToggleAction = switch_ns.class_("ToggleAction", automation.Action) +TurnOffAction = switch_ns.class_("TurnOffAction", automation.Action) +TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) +SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action) + +SwitchCondition = switch_ns.class_("SwitchCondition", Condition) +SwitchTurnOnTrigger = switch_ns.class_( + "SwitchTurnOnTrigger", automation.Trigger.template() +) +SwitchTurnOffTrigger = switch_ns.class_( + "SwitchTurnOffTrigger", automation.Trigger.template() +) + +icon = cv.icon + + +SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), + cv.Optional(CONF_INVERTED): cv.boolean, + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), + } + ), + cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), + } + ), + cv.Optional("forced_hash"): cv.int_, + } +) + + +async def setup_switch_core_(var, config): + await setup_entity(var, config) + + if "forced_hash" in config: + cg.add(var.set_forced_hash(config["forced_hash"])) + + + if CONF_INVERTED in config: + cg.add(var.set_inverted(config[CONF_INVERTED])) + for conf in config.get(CONF_ON_TURN_ON, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TURN_OFF, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_switch(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_switch(var)) + await setup_switch_core_(var, config) + + +SWITCH_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Switch), + } +) + + [email protected]_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA) [email protected]_action("switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA) [email protected]_action("switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA) +async def switch_toggle_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + [email protected]_condition("switch.is_on", SwitchCondition, SWITCH_ACTION_SCHEMA) +async def switch_is_on_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, True) + + [email protected]_condition("switch.is_off", SwitchCondition, SWITCH_ACTION_SCHEMA) +async def switch_is_off_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, False) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(switch_ns.using) + cg.add_define("USE_SWITCH") diff --git a/components/switch/automation.cpp b/components/switch/automation.cpp new file mode 100644 index 0000000..5989ae9 --- /dev/null +++ b/components/switch/automation.cpp @@ -0,0 +1,10 @@ +#include "automation.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace switch_ { + +static const char *const TAG = "switch.automation"; + +} // namespace switch_ +} // namespace esphome diff --git a/components/switch/automation.h b/components/switch/automation.h new file mode 100644 index 0000000..579daf4 --- /dev/null +++ b/components/switch/automation.h @@ -0,0 +1,84 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace switch_ { + +template<typename... Ts> class TurnOnAction : public Action<Ts...> { + public: + explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {} + + void play(Ts... x) override { this->switch_->turn_on(); } + + protected: + Switch *switch_; +}; + +template<typename... Ts> class TurnOffAction : public Action<Ts...> { + public: + explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {} + + void play(Ts... x) override { this->switch_->turn_off(); } + + protected: + Switch *switch_; +}; + +template<typename... Ts> class ToggleAction : public Action<Ts...> { + public: + explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {} + + void play(Ts... x) override { this->switch_->toggle(); } + + protected: + Switch *switch_; +}; + +template<typename... Ts> class SwitchCondition : public Condition<Ts...> { + public: + SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {} + bool check(Ts... x) override { return this->parent_->state == this->state_; } + + protected: + Switch *parent_; + bool state_; +}; + +class SwitchTurnOnTrigger : public Trigger<> { + public: + SwitchTurnOnTrigger(Switch *a_switch) { + a_switch->add_on_state_callback([this](bool state) { + if (state) { + this->trigger(); + } + }); + } +}; + +class SwitchTurnOffTrigger : public Trigger<> { + public: + SwitchTurnOffTrigger(Switch *a_switch) { + a_switch->add_on_state_callback([this](bool state) { + if (!state) { + this->trigger(); + } + }); + } +}; + +template<typename... Ts> class SwitchPublishAction : public Action<Ts...> { + public: + SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } + + protected: + Switch *switch_; +}; + +} // namespace switch_ +} // namespace esphome diff --git a/components/switch/switch.cpp b/components/switch/switch.cpp new file mode 100644 index 0000000..e0da5c9 --- /dev/null +++ b/components/switch/switch.cpp @@ -0,0 +1,55 @@ +#include "switch.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace switch_ { + +static const char *const TAG = "switch"; + +Switch::Switch(const std::string &name) : EntityBase(name), state(false) {} +Switch::Switch() : Switch("") {} + +void Switch::turn_on() { + ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); + this->write_state(!this->inverted_); +} +void Switch::turn_off() { + ESP_LOGD(TAG, "'%s' Turning OFF.", this->get_name().c_str()); + this->write_state(this->inverted_); +} +void Switch::toggle() { + ESP_LOGD(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON"); + this->write_state(this->inverted_ == this->state); +} +optional<bool> Switch::get_initial_state() { + + if ( this->has_forced_hash ) { + this->rtc_ = global_preferences->make_preference<bool>(this->forced_hash); + } else { + this->rtc_ = global_preferences->make_preference<bool>(this->get_object_id_hash()); + } + bool initial_state; + if (!this->rtc_.load(&initial_state)) + return {}; + return initial_state; +} +void Switch::publish_state(bool state) { + if (!this->publish_dedup_.next(state)) + return; + this->state = state != this->inverted_; + + this->rtc_.save(&this->state); + ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); + this->state_callback_.call(this->state); +} +bool Switch::assumed_state() { return false; } + +void Switch::add_on_state_callback(std::function<void(bool)> &&callback) { + this->state_callback_.add(std::move(callback)); +} +void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; } +uint32_t Switch::hash_base() { return 3129890955UL; } +bool Switch::is_inverted() const { return this->inverted_; } + +} // namespace switch_ +} // namespace esphome diff --git a/components/switch/switch.h b/components/switch/switch.h new file mode 100644 index 0000000..8a172ec --- /dev/null +++ b/components/switch/switch.h @@ -0,0 +1,119 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/preferences.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace switch_ { + +#define LOG_SWITCH(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + if ((obj)->assumed_state()) { \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ + } \ + if ((obj)->is_inverted()) { \ + ESP_LOGCONFIG(TAG, "%s Inverted: YES", prefix); \ + } \ + } + +/** Base class for all switches. + * + * A switch is basically just a combination of a binary sensor (for reporting switch values) + * and a write_state method that writes a state to the hardware. + */ +class Switch : public EntityBase { + public: + explicit Switch(); + explicit Switch(const std::string &name); + + /** Publish a state to the front-end from the back-end. + * + * The input value is inverted if applicable. Then the internal value member is set and + * finally the callbacks are called. + * + * @param state The new state. + */ + void publish_state(bool state); + + /// The current reported state of the binary sensor. + bool state; + + /** Turn this switch on. This is called by the front-end. + * + * For implementing switches, please override write_state. + */ + void turn_on(); + /** Turn this switch off. This is called by the front-end. + * + * For implementing switches, please override write_state. + */ + void turn_off(); + /** Toggle this switch. This is called by the front-end. + * + * For implementing switches, please override write_state. + */ + void toggle(); + + /** Set whether the state should be treated as inverted. + * + * To the developer and user an inverted switch will act just like a non-inverted one. + * In particular, the only thing that's changed by this is the value passed to + * write_state and the state in publish_state. The .state member variable and + * turn_on/turn_off/toggle remain unaffected. + * + * @param inverted Whether to invert this switch. + */ + void set_inverted(bool inverted); + + /** Set callback for state changes. + * + * @param callback The void(bool) callback. + */ + void add_on_state_callback(std::function<void(bool)> &&callback); + + optional<bool> get_initial_state(); + + /** Return whether this switch uses an assumed state - i.e. if both the ON/OFF actions should be displayed in Home + * Assistant because the real state is unknown. + * + * Defaults to false. + */ + virtual bool assumed_state(); + + bool is_inverted() const; + + 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: + /** Write the given state to hardware. You should implement this + * abstract method if you want to create your own switch. + * + * In the implementation of this method, you should also call + * publish_state to acknowledge that the state was written to the hardware. + * + * @param state The state to write. Inversion is already applied if user specified it. + */ + virtual void write_state(bool state) = 0; + + uint32_t hash_base() override; + + CallbackManager<void(bool)> state_callback_{}; + bool inverted_{false}; + Deduplicator<bool> publish_dedup_; + ESPPreferenceObject rtc_; +}; + +} // namespace switch_ +} // namespace esphome |