aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKaufman Home Automation <[email protected]>2022-02-06 16:44:35 -0700
committerGitHub <[email protected]>2022-02-06 16:44:35 -0700
commit4d30b8651378f1ae60a4ff04536bd2244b1c5750 (patch)
tree4622c45fb5c1df6c602ca94a666c0570a4d1e73b
parentfff5ec94a8d813d0e89c8a7dec0090b115c84046 (diff)
downloadPLF10-4d30b8651378f1ae60a4ff04536bd2244b1c5750.tar.gz
PLF10-4d30b8651378f1ae60a4ff04536bd2244b1c5750.zip
v1.84 upload
-rw-r--r--components/number/__init__.py182
-rw-r--r--components/number/automation.cpp47
-rw-r--r--components/number/automation.h76
-rw-r--r--components/number/number.cpp56
-rw-r--r--components/number/number.h118
-rw-r--r--components/select/__init__.py100
-rw-r--r--components/select/automation.h33
-rw-r--r--components/select/select.cpp43
-rw-r--r--components/select/select.h93
-rw-r--r--components/switch/__init__.py117
-rw-r--r--components/switch/automation.cpp10
-rw-r--r--components/switch/automation.h84
-rw-r--r--components/switch/switch.cpp55
-rw-r--r--components/switch/switch.h119
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),
+)
+
+
+ "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)
+
+
+ "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)
+
+
+ "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