diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 89788f1e98..f809fff529 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -14,6 +14,8 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_MQTT_ID, CONF_VALUE, + CONF_OPERATION, + CONF_CYCLE, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -35,6 +37,7 @@ ValueRangeTrigger = number_ns.class_( # Actions NumberSetAction = number_ns.class_("NumberSetAction", automation.Action) +NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action) # Conditions NumberInRangeCondition = number_ns.class_( @@ -49,6 +52,15 @@ NUMBER_MODES = { "SLIDER": NumberMode.NUMBER_MODE_SLIDER, } +NumberOperation = number_ns.enum("NumberOperation") + +NUMBER_OPERATION_OPTIONS = { + "INCREMENT": NumberOperation.NUMBER_OP_INCREMENT, + "DECREMENT": NumberOperation.NUMBER_OP_DECREMENT, + "TO_MIN": NumberOperation.NUMBER_OP_TO_MIN, + "TO_MAX": NumberOperation.NUMBER_OP_TO_MAX, +} + icon = cv.icon NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( @@ -159,12 +171,18 @@ async def to_code(config): cg.add_global(number_ns.using) +OPERATION_BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Number), + } +) + + @automation.register_action( "number.set", NumberSetAction, - cv.Schema( + OPERATION_BASE_SCHEMA.extend( { - cv.Required(CONF_ID): cv.use_id(Number), cv.Required(CONF_VALUE): cv.templatable(cv.float_), } ), @@ -175,3 +193,85 @@ async def number_set_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_VALUE], args, float) cg.add(var.set_value(template_)) return var + + +@automation.register_action( + "number.increment", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="INCREMENT"): cv.one_of( + "INCREMENT", upper=True + ), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "number.decrement", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="DECREMENT"): cv.one_of( + "DECREMENT", upper=True + ), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "number.to_min", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="TO_MIN"): cv.one_of( + "TO_MIN", upper=True + ), + } + ) + ), +) +@automation.register_action( + "number.to_max", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="TO_MAX"): cv.one_of( + "TO_MAX", upper=True + ), + } + ) + ), +) +@automation.register_action( + "number.operation", + NumberOperationAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPERATION): cv.templatable( + cv.enum(NUMBER_OPERATION_OPTIONS, upper=True) + ), + cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), + } + ), +) +async def number_to_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_OPERATION in config: + to_ = await cg.templatable(config[CONF_OPERATION], args, NumberOperation) + cg.add(var.set_operation(to_)) + if CONF_CYCLE in config: + cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) + cg.add(var.set_cycle(cycle_)) + if CONF_MODE in config: + cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[config[CONF_MODE]])) + if CONF_CYCLE in config: + cg.add(var.set_cycle(config[CONF_CYCLE])) + return var diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 98554a346a..33f0f9727e 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -29,6 +29,25 @@ template class NumberSetAction : public Action { Number *number_; }; +template class NumberOperationAction : public Action { + public: + explicit NumberOperationAction(Number *number) : number_(number) {} + TEMPLATABLE_VALUE(NumberOperation, operation) + TEMPLATABLE_VALUE(bool, cycle) + + void play(Ts... x) override { + auto call = this->number_->make_call(); + call.with_operation(this->operation_.value(x...)); + if (this->cycle_.has_value()) { + call.with_cycle(this->cycle_.value(x...)); + } + call.perform(); + } + + protected: + Number *number_; +}; + class ValueRangeTrigger : public Trigger, public Component { public: explicit ValueRangeTrigger(Number *parent) : parent_(parent) {} diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 99a2c04a22..03a7cc6ce3 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -6,30 +6,6 @@ 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; @@ -41,15 +17,6 @@ void Number::add_on_state_callback(std::function &&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 diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 40fdfceec1..8f9bf8c2e1 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "number_call.h" +#include "number_traits.h" namespace esphome { namespace number { @@ -20,54 +22,6 @@ namespace number { class Number; -class NumberCall { - public: - explicit NumberCall(Number *parent) : parent_(parent) {} - void perform(); - - NumberCall &set_value(float value) { - value_ = value; - return *this; - } - const optional &get_value() const { return value_; } - - protected: - Number *const parent_; - optional 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 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. diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp new file mode 100644 index 0000000000..4219f85328 --- /dev/null +++ b/esphome/components/number/number_call.cpp @@ -0,0 +1,118 @@ +#include "number_call.h" +#include "number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } + +NumberCall &NumberCall::number_increment(bool cycle) { + return this->with_operation(NUMBER_OP_INCREMENT).with_cycle(cycle); +} + +NumberCall &NumberCall::number_decrement(bool cycle) { + return this->with_operation(NUMBER_OP_DECREMENT).with_cycle(cycle); +} + +NumberCall &NumberCall::number_to_min() { return this->with_operation(NUMBER_OP_TO_MIN); } + +NumberCall &NumberCall::number_to_max() { return this->with_operation(NUMBER_OP_TO_MAX); } + +NumberCall &NumberCall::with_operation(NumberOperation operation) { + this->operation_ = operation; + return *this; +} + +NumberCall &NumberCall::with_value(float value) { + this->value_ = value; + return *this; +} + +NumberCall &NumberCall::with_cycle(bool cycle) { + this->cycle_ = cycle; + return *this; +} + +void NumberCall::perform() { + auto *parent = this->parent_; + const auto *name = parent->get_name().c_str(); + const auto &traits = parent->traits; + + if (this->operation_ == NUMBER_OP_NONE) { + ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); + return; + } + + float target_value = NAN; + float min_value = traits.get_min_value(); + float max_value = traits.get_max_value(); + + if (this->operation_ == NUMBER_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting number value", name); + if (!this->value_.has_value() || std::isnan(*this->value_)) { + ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); + return; + } + target_value = this->value_.value(); + } else if (this->operation_ == NUMBER_OP_TO_MIN) { + if (std::isnan(min_value)) { + ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); + } else { + target_value = min_value; + } + } else if (this->operation_ == NUMBER_OP_TO_MAX) { + if (std::isnan(max_value)) { + ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); + } else { + target_value = max_value; + } + } else if (this->operation_ == NUMBER_OP_INCREMENT) { + ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); + if (!parent->has_state()) { + ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); + return; + } + auto step = traits.get_step(); + target_value = parent->state + (std::isnan(step) ? 1 : step); + if (target_value > max_value) { + if (this->cycle_ && !std::isnan(min_value)) { + target_value = min_value; + } else { + target_value = max_value; + } + } + } else if (this->operation_ == NUMBER_OP_DECREMENT) { + ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); + if (!parent->has_state()) { + ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); + return; + } + auto step = traits.get_step(); + target_value = parent->state - (std::isnan(step) ? 1 : step); + if (target_value < min_value) { + if (this->cycle_ && !std::isnan(max_value)) { + target_value = max_value; + } else { + target_value = min_value; + } + } + } + + if (target_value < min_value) { + ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); + return; + } + if (target_value > max_value) { + ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); + return; + } + + ESP_LOGD(TAG, " New number value: %f", target_value); + this->parent_->control(target_value); +} + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h new file mode 100644 index 0000000000..9a3dad560f --- /dev/null +++ b/esphome/components/number/number_call.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "number_traits.h" + +namespace esphome { +namespace number { + +class Number; + +enum NumberOperation { + NUMBER_OP_NONE, + NUMBER_OP_SET, + NUMBER_OP_INCREMENT, + NUMBER_OP_DECREMENT, + NUMBER_OP_TO_MIN, + NUMBER_OP_TO_MAX, +}; + +class NumberCall { + public: + explicit NumberCall(Number *parent) : parent_(parent) {} + void perform(); + + NumberCall &set_value(float value); + const optional &get_value() const { return value_; } + + NumberCall &number_increment(bool cycle); + NumberCall &number_decrement(bool cycle); + NumberCall &number_to_min(); + NumberCall &number_to_max(); + + NumberCall &with_operation(NumberOperation operation); + NumberCall &with_value(float value); + NumberCall &with_cycle(bool cycle); + + protected: + Number *const parent_; + NumberOperation operation_{NUMBER_OP_NONE}; + optional value_; + bool cycle_; +}; + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp new file mode 100644 index 0000000000..dcd05daa2a --- /dev/null +++ b/esphome/components/number/number_traits.cpp @@ -0,0 +1,20 @@ +#include "esphome/core/log.h" +#include "number_traits.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { + this->unit_of_measurement_ = unit_of_measurement; +} + +std::string NumberTraits::get_unit_of_measurement() { + if (this->unit_of_measurement_.has_value()) + return *this->unit_of_measurement_; + return ""; +} + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h new file mode 100644 index 0000000000..47756ff66f --- /dev/null +++ b/esphome/components/number/number_traits.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace number { + +enum NumberMode : uint8_t { + NUMBER_MODE_AUTO = 0, + NUMBER_MODE_BOX = 1, + NUMBER_MODE_SLIDER = 2, +}; + +class NumberTraits { + public: + // Set/get the number value boundaries. + 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_; } + + // Set/get the step size for incrementing or decrementing the number value. + void set_step(float step) { step_ = step; } + float get_step() const { return step_; } + + /// Manually set the unit of measurement. + void set_unit_of_measurement(const std::string &unit_of_measurement); + /// Get the unit of measurement, using the manual override if set. + std::string get_unit_of_measurement(); + + // Set/get the frontend mode. + void set_mode(NumberMode mode) { this->mode_ = mode; } + NumberMode get_mode() const { return this->mode_; } + + protected: + float min_value_ = NAN; + float max_value_ = NAN; + float step_ = NAN; + optional unit_of_measurement_; ///< Unit of measurement override + NumberMode mode_{NUMBER_MODE_AUTO}; +}; + +} // namespace number +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index e38f39eab6..ffda860377 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -120,6 +120,11 @@ number: name: My template number id: template_number_id optimistic: true + max_value: 100 + min_value: 0 + step: 5 + unit_of_measurement: "%" + mode: slider on_value: - logger.log: format: "Number changed to %f" @@ -128,11 +133,31 @@ number: - logger.log: format: "Template Number set to %f" args: ["x"] - max_value: 100 - min_value: 0 - step: 5 - unit_of_measurement: "%" - mode: slider + - number.set: + id: template_number_id + value: 50 + - number.to_min: template_number_id + - number.to_min: + id: template_number_id + - number.to_max: template_number_id + - number.to_max: + id: template_number_id + - number.increment: template_number_id + - number.increment: + id: template_number_id + cycle: false + - number.decrement: template_number_id + - number.decrement: + id: template_number_id + cycle: false + - number.operation: + id: template_number_id + operation: Increment + cycle: false + - number.operation: + id: template_number_id + operation: !lambda "return NUMBER_OP_INCREMENT;" + cycle: !lambda "return false;" - id: modbus_numbertest platform: modbus_controller