From 98855e4123f236530509f7f40c3854d538375e3e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 20 Jul 2021 08:22:49 +1200 Subject: [PATCH 1/2] Number and Template Number updates (#2036) Co-authored-by: Otto winter --- esphome/components/api/api_connection.cpp | 8 +- esphome/components/mqtt/mqtt_number.cpp | 22 +++-- esphome/components/number/__init__.py | 27 ++++-- esphome/components/number/number.cpp | 69 ++++---------- esphome/components/number/number.h | 93 +++++++------------ .../components/template/number/__init__.py | 25 +++-- .../template/number/template_number.cpp | 33 ++++--- .../template/number/template_number.h | 19 ++-- esphome/components/web_server/web_server.cpp | 5 +- 9 files changed, 148 insertions(+), 153 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8c76583fc7..79ffcfa69e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -570,11 +570,11 @@ bool APIConnection::send_number_info(number::Number *number) { msg.object_id = number->get_object_id(); msg.name = number->get_name(); msg.unique_id = get_default_unique_id("number", number); - msg.icon = number->get_icon(); + msg.icon = number->traits.get_icon(); - msg.min_value = number->get_min_value(); - msg.max_value = number->get_max_value(); - msg.step = number->get_step(); + msg.min_value = number->traits.get_min_value(); + msg.max_value = number->traits.get_max_value(); + msg.step = number->traits.get_step(); return this->send_list_entities_number_response(msg); } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index bb67a225fd..0311526340 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -3,10 +3,6 @@ #ifdef USE_NUMBER -#ifdef USE_DEEP_SLEEP -#include "esphome/components/deep_sleep/deep_sleep_component.h" -#endif - namespace esphome { namespace mqtt { @@ -20,7 +16,7 @@ void MQTTNumberComponent::setup() { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { auto val = parse_float(state); if (!val.has_value()) { - ESP_LOGE(TAG, "Can't convert '%s' to number!", state.c_str()); + ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); return; } auto call = this->number_->make_call(); @@ -39,8 +35,15 @@ std::string MQTTNumberComponent::component_type() const { return "number"; } std::string MQTTNumberComponent::friendly_name() const { return this->number_->get_name(); } void MQTTNumberComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { - if (!this->number_->get_icon().empty()) - root["icon"] = this->number_->get_icon(); + const auto &traits = number_->traits; + // https://www.home-assistant.io/integrations/number.mqtt/ + if (!traits.get_icon().empty()) + root["icon"] = traits.get_icon(); + root["min_value"] = traits.get_min_value(); + root["max_value"] = traits.get_max_value(); + root["step"] = traits.get_step(); + + config.command_topic = true; } bool MQTTNumberComponent::send_initial_state() { if (this->number_->has_state()) { @@ -51,8 +54,9 @@ bool MQTTNumberComponent::send_initial_state() { } bool MQTTNumberComponent::is_internal() { return this->number_->is_internal(); } bool MQTTNumberComponent::publish_state(float value) { - int8_t accuracy = this->number_->get_accuracy_decimals(); - return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%f", value); + return this->publish(this->get_state_topic_(), buffer); } } // namespace mqtt diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index ed33931d8b..bf95cb1b31 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -1,3 +1,4 @@ +from typing import Optional import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -66,12 +67,18 @@ NUMBER_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( ) -async def setup_number_core_(var, config): +async def setup_number_core_( + var, config, *, min_value: float, max_value: float, step: Optional[float] +): cg.add(var.set_name(config[CONF_NAME])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) - cg.add(var.set_icon(config[CONF_ICON])) + cg.add(var.traits.set_icon(config[CONF_ICON])) + 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)) for conf in config.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -92,16 +99,24 @@ async def setup_number_core_(var, config): await mqtt.register_mqtt_component(mqtt_, config) -async def register_number(var, 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) + await setup_number_core_( + var, config, min_value=min_value, max_value=max_value, step=step + ) -async def new_number(config): +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) + await register_number( + var, config, min_value=min_value, max_value=max_value, step=step + ) return var diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index eaee5d4e69..dbc1c88a5d 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -8,67 +8,38 @@ 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()) { - auto value = *this->value_; - uint8_t accuracy = this->parent_->get_accuracy_decimals(); - float min_value = this->parent_->get_min_value(); - if (value < min_value) { - ESP_LOGW(TAG, " Value %s must not be less than minimum %s", value_accuracy_to_string(value, accuracy).c_str(), - value_accuracy_to_string(min_value, accuracy).c_str()); - this->value_.reset(); - return; - } - float max_value = this->parent_->get_max_value(); - if (value > max_value) { - ESP_LOGW(TAG, " Value %s must not be larger than maximum %s", value_accuracy_to_string(value, accuracy).c_str(), - value_accuracy_to_string(max_value, accuracy).c_str()); - this->value_.reset(); - return; - } - ESP_LOGD(TAG, " Value: %s", value_accuracy_to_string(*this->value_, accuracy).c_str()); - this->parent_->set(*this->value_); + if (!this->value_.has_value() || 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_); } -NumberCall &NumberCall::set_value(float value) { - this->value_ = value; - return *this; -} - -const optional &NumberCall::get_value() const { return this->value_; } - -NumberCall Number::make_call() { return NumberCall(this); } - void Number::publish_state(float state) { this->has_state_ = true; this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %.5f", this->get_name().c_str(), state); + ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); } -uint32_t Number::update_interval() { return 0; } -Number::Number(const std::string &name) : Nameable(name), state(NAN) {} -Number::Number() : Number("") {} - void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -void Number::set_icon(const std::string &icon) { this->icon_ = icon; } -std::string Number::get_icon() { return *this->icon_; } -int8_t Number::get_accuracy_decimals() { - // use printf %g to find number of digits based on step - char buf[32]; - sprintf(buf, "%.5g", this->step_); - std::string str{buf}; - size_t dot_pos = str.find('.'); - if (dot_pos == std::string::npos) - return 0; - - return str.length() - dot_pos - 1; -} -float Number::get_state() const { return this->state; } - -bool Number::has_state() const { return this->has_state_; } uint32_t Number::hash_base() { return 2282307003UL; } diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 4fe9692a6b..e32b53187b 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -9,8 +9,8 @@ namespace number { #define LOG_NUMBER(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, 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_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->traits.get_icon().c_str()); \ } \ } @@ -19,72 +19,56 @@ class Number; class NumberCall { public: explicit NumberCall(Number *parent) : parent_(parent) {} - NumberCall &set_value(float value); void perform(); - const optional &get_value() const; + NumberCall &set_value(float value) { + value_ = value; + return *this; + } + const optional &get_value() const { return value_; } protected: Number *const parent_; optional value_; }; +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_; } + void set_icon(std::string icon) { icon_ = std::move(icon); } + const std::string &get_icon() const { return icon_; } + + protected: + float min_value_ = NAN; + float max_value_ = NAN; + float step_ = NAN; + std::string icon_; +}; + /** Base-class for all numbers. * * A number can use publish_state to send out a new value. */ class Number : public Nameable { public: - explicit Number(); - explicit Number(const std::string &name); - - /** Manually set the icon of this number. By default the number's default defined by icon() is used. - * - * @param icon The icon, for example "mdi:flash". "" to disable. - */ - void set_icon(const std::string &icon); - /// Get the Home Assistant Icon. Uses the manual override if specified or the default value instead. - std::string get_icon(); - - /// Getter-syntax for .state. - float get_state() const; - - /// Get the accuracy in decimals. Based on the step value. - int8_t get_accuracy_decimals(); - - /** Publish the current state to the front-end. - */ - void publish_state(float state); - - NumberCall make_call(); - - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) - /// Add a callback that will be called every time the state changes. - void add_on_state_callback(std::function &&callback); - - /** This member variable stores the last state. - * - * On startup, when no state is available yet, this is NAN (not-a-number) and the validity - * can be checked using has_state(). - * - * This is exposed through a member variable for ease of use in esphome lambdas. - */ 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 &&callback); + + NumberTraits traits; + /// Return whether this number has gotten a full state yet. - bool has_state() const; - - /// Return with which interval the number is polled. Return 0 for non-polling mode. - virtual uint32_t update_interval(); - - void set_min_value(float min_value) { this->min_value_ = min_value; } - void set_max_value(float max_value) { this->max_value_ = max_value; } - void set_step(float step) { this->step_ = step; } - - float get_min_value() const { return this->min_value_; } - float get_max_value() const { return this->max_value_; } - float get_step() const { return this->step_; } + bool has_state() const { return has_state_; } protected: friend class NumberCall; @@ -95,17 +79,12 @@ class Number : public Nameable { * * @param value The value as validated by the NumberCall. */ - virtual void set(float value) = 0; + virtual void control(float value) = 0; uint32_t hash_base() override; CallbackManager state_callback_; - /// Override the icon advertised to Home Assistant, otherwise number's icon will be used. - optional icon_; bool has_state_{false}; - float step_{1.0}; - float min_value_{0}; - float max_value_{100}; }; } // namespace number diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index cf70a48c4d..557a01c6fa 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -4,6 +4,7 @@ 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, @@ -32,9 +33,10 @@ CONFIG_SCHEMA = cv.All( 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.Exclusive(CONF_LAMBDA, "lambda-optimistic"): cv.returning_lambda, + cv.Exclusive(CONF_OPTIMISTIC, "lambda-optimistic"): cv.boolean, cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_INITIAL_VALUE): cv.float_, } ).extend(cv.polling_component_schema("60s")), validate_min_max, @@ -44,20 +46,27 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - await number.register_number(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_)) + + elif CONF_OPTIMISTIC in config: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + if CONF_SET_ACTION in config: await automation.build_automation( var.get_set_trigger(), [(float, "x")], config[CONF_SET_ACTION] ) - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - - cg.add(var.set_min_value(config[CONF_MIN_VALUE])) - cg.add(var.set_max_value(config[CONF_MAX_VALUE])) - cg.add(var.set_step(config[CONF_STEP])) + if CONF_INITIAL_VALUE in config: + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 69c5d62684..500f9f2272 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -6,34 +6,45 @@ namespace template_ { static const char *const TAG = "template.number"; -TemplateNumber::TemplateNumber() : set_trigger_(new Trigger()) {} +void TemplateNumber::setup() { + if (this->f_.has_value() || !this->optimistic_) + return; + + this->pref_ = global_preferences.make_preference(this->get_object_id_hash()); + float value; + if (!this->pref_.load(&value)) { + if (!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()) { - this->publish_state(*val); - } + if (!val.has_value()) + return; + + this->publish_state(*val); } -void TemplateNumber::set(float value) { +void TemplateNumber::control(float value) { this->set_trigger_->trigger(value); - if (this->optimistic_) + if (this->optimistic_) { this->publish_state(value); + this->pref_.save(&value); + } } -float TemplateNumber::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateNumber::set_template(std::function()> &&f) { this->f_ = f; } void TemplateNumber::dump_config() { LOG_NUMBER("", "Template Number", this); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); LOG_UPDATE_INTERVAL(this); } -void TemplateNumber::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -Trigger *TemplateNumber::get_set_trigger() const { return this->set_trigger_; }; - } // namespace template_ } // namespace esphome diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 4c633e3b53..50cd256b7f 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -3,27 +3,32 @@ #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: - TemplateNumber(); - void set_template(std::function()> &&f); + void set_template(std::function()> &&f) { this->f_ = f; } + void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const; - void set_optimistic(bool optimistic); + Trigger *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; } protected: - void set(float value) override; + void control(float value) override; bool optimistic_{false}; - Trigger *set_trigger_; + float initial_value_{NAN}; + Trigger *set_trigger_ = new Trigger(); optional()>> f_; + + ESPPreferenceObject pref_; }; } // namespace template_ diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 57eef7a946..b775d44211 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -614,8 +614,9 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM std::string WebServer::number_json(number::Number *obj, float value) { return json::build_json([obj, value](JsonObject &root) { root["id"] = "number-" + obj->get_object_id(); - std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); - root["state"] = state; + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%f", value); + root["state"] = buffer; root["value"] = value; }); } From 0a82e6e792477c579ae60f19ab593ec8fcd47369 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 20 Jul 2021 10:28:23 +1200 Subject: [PATCH 2/2] Bump version to v1.20.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 65c6458dd6..7f4f08274b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "1.20.0b3" +__version__ = "1.20.0b4" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266"