mirror of
https://github.com/esphome/esphome.git
synced 2025-09-17 02:32:20 +01:00
Add Number entities (from Home Assistant) (#1971)
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
This commit is contained in:
154
esphome/components/number/__init__.py
Normal file
154
esphome/components/number/__init__.py
Normal file
@@ -0,0 +1,154 @@
|
||||
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_ICON,
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_ON_VALUE,
|
||||
CONF_ON_VALUE_RANGE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_NAME,
|
||||
CONF_MQTT_ID,
|
||||
CONF_VALUE,
|
||||
ICON_EMPTY,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
number_ns = cg.esphome_ns.namespace("number")
|
||||
Number = number_ns.class_("Number", cg.Nameable)
|
||||
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
|
||||
)
|
||||
|
||||
icon = cv.icon
|
||||
|
||||
|
||||
NUMBER_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent),
|
||||
cv.GenerateID(): cv.declare_id(Number),
|
||||
cv.Optional(CONF_ICON, default=ICON_EMPTY): icon,
|
||||
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),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def setup_number_core_(var, config):
|
||||
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]))
|
||||
|
||||
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_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):
|
||||
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)
|
||||
|
||||
|
||||
async def new_number(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await register_number(var, config)
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
@automation.register_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)
|
||||
|
||||
|
||||
@automation.register_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
|
47
esphome/components/number/automation.cpp
Normal file
47
esphome/components/number/automation.cpp
Normal file
@@ -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 (isnan(state))
|
||||
return;
|
||||
|
||||
float local_min = this->min_.value(state);
|
||||
float local_max = this->max_.value(state);
|
||||
|
||||
bool in_range;
|
||||
if (isnan(local_min) && isnan(local_max)) {
|
||||
in_range = this->previous_in_range_;
|
||||
} else if (isnan(local_min)) {
|
||||
in_range = state <= local_max;
|
||||
} else if (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
|
76
esphome/components/number/automation.h
Normal file
76
esphome/components/number/automation.h
Normal file
@@ -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 (isnan(this->min_)) {
|
||||
return state <= this->max_;
|
||||
} else if (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
|
76
esphome/components/number/number.cpp
Normal file
76
esphome/components/number/number.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#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()) {
|
||||
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_);
|
||||
}
|
||||
}
|
||||
|
||||
NumberCall &NumberCall::set_value(float value) {
|
||||
this->value_ = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
const optional<float> &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);
|
||||
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<void(float)> &&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; }
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
112
esphome/components/number/number.h
Normal file
112
esphome/components/number/number.h
Normal file
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.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, type, (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_icon().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
|
||||
} \
|
||||
}
|
||||
|
||||
class Number;
|
||||
|
||||
class NumberCall {
|
||||
public:
|
||||
explicit NumberCall(Number *parent) : parent_(parent) {}
|
||||
NumberCall &set_value(float value);
|
||||
void perform();
|
||||
|
||||
const optional<float> &get_value() const;
|
||||
|
||||
protected:
|
||||
Number *const parent_;
|
||||
optional<float> value_;
|
||||
};
|
||||
|
||||
/** 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<void(float)> &&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;
|
||||
|
||||
/// 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_; }
|
||||
|
||||
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 set(float value) = 0;
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(float)> state_callback_;
|
||||
/// Override the icon advertised to Home Assistant, otherwise number's icon will be used.
|
||||
optional<std::string> icon_;
|
||||
bool has_state_{false};
|
||||
float step_{1.0};
|
||||
float min_value_{0};
|
||||
float max_value_{100};
|
||||
};
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
Reference in New Issue
Block a user