Add Number entities (from Home Assistant) (#1971)

Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
This commit is contained in:
Jesse Hills 2021-07-13 07:20:12 +12:00 committed by GitHub
parent 1f5c79bd17
commit dd37a4e04c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1206 additions and 0 deletions

View File

@ -73,6 +73,7 @@ esphome/components/midea_dongle/* @dudanov
esphome/components/mitsubishi/* @RubyBailey
esphome/components/network/* @esphome/core
esphome/components/nfc/* @jesserockz
esphome/components/number/* @esphome/core
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pid/* @OttoWinter

View File

@ -38,6 +38,7 @@ service APIConnection {
rpc switch_command (SwitchCommandRequest) returns (void) {}
rpc camera_image (CameraImageRequest) returns (void) {}
rpc climate_command (ClimateCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {}
}
@ -798,3 +799,41 @@ message ClimateCommandRequest {
bool has_custom_preset = 20;
string custom_preset = 21;
}
// ==================== NUMBER ====================
message ListEntitiesNumberResponse {
option (id) = 49;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
float min_value = 6;
float max_value = 7;
float step = 8;
}
message NumberStateResponse {
option (id) = 50;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER";
option (no_delay) = true;
fixed32 key = 1;
float state = 2;
// If the number does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
}
message NumberCommandRequest {
option (id) = 51;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_NUMBER";
option (no_delay) = true;
fixed32 key = 1;
float state = 2;
}

View File

@ -553,6 +553,42 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
}
#endif
#ifdef USE_NUMBER
bool APIConnection::send_number_state(number::Number *number, float state) {
if (!this->state_subscription_)
return false;
NumberStateResponse resp{};
resp.key = number->get_object_id_hash();
resp.state = state;
resp.missing_state = !number->has_state();
return this->send_number_state_response(resp);
}
bool APIConnection::send_number_info(number::Number *number) {
ListEntitiesNumberResponse msg;
msg.key = number->get_object_id_hash();
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.min_value = number->get_min_value();
msg.max_value = number->get_max_value();
msg.step = number->get_step();
return this->send_list_entities_number_response(msg);
}
void APIConnection::number_command(const NumberCommandRequest &msg) {
number::Number *number = App.get_number_by_key(msg.key);
if (number == nullptr)
return;
auto call = number->make_call();
call.set_value(msg.state);
call.perform();
}
#endif
#ifdef USE_ESP32_CAMERA
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
if (!this->state_subscription_)

View File

@ -62,6 +62,11 @@ class APIConnection : public APIServerConnection {
bool send_climate_state(climate::Climate *climate);
bool send_climate_info(climate::Climate *climate);
void climate_command(const ClimateCommandRequest &msg) override;
#endif
#ifdef USE_NUMBER
bool send_number_state(number::Number *number, float state);
bool send_number_info(number::Number *number);
void number_command(const NumberCommandRequest &msg) override;
#endif
bool send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {

View File

@ -3256,6 +3256,179 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
out.append("\n");
out.append("}");
}
bool ListEntitiesNumberResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->object_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
case 4: {
this->unique_id = value.as_string();
return true;
}
case 5: {
this->icon = value.as_string();
return true;
}
default:
return false;
}
}
bool ListEntitiesNumberResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
case 6: {
this->min_value = value.as_float();
return true;
}
case 7: {
this->max_value = value.as_float();
return true;
}
case 8: {
this->step = value.as_float();
return true;
}
default:
return false;
}
}
void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
buffer.encode_string(4, this->unique_id);
buffer.encode_string(5, this->icon);
buffer.encode_float(6, this->min_value);
buffer.encode_float(7, this->max_value);
buffer.encode_float(8, this->step);
}
void ListEntitiesNumberResponse::dump_to(std::string &out) const {
char buffer[64];
out.append("ListEntitiesNumberResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
out.append("\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" unique_id: ");
out.append("'").append(this->unique_id).append("'");
out.append("\n");
out.append(" icon: ");
out.append("'").append(this->icon).append("'");
out.append("\n");
out.append(" min_value: ");
sprintf(buffer, "%g", this->min_value);
out.append(buffer);
out.append("\n");
out.append(" max_value: ");
sprintf(buffer, "%g", this->max_value);
out.append(buffer);
out.append("\n");
out.append(" step: ");
sprintf(buffer, "%g", this->step);
out.append(buffer);
out.append("\n");
out.append("}");
}
bool NumberStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 3: {
this->missing_state = value.as_bool();
return true;
}
default:
return false;
}
}
bool NumberStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 2: {
this->state = value.as_float();
return true;
}
default:
return false;
}
}
void NumberStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_float(2, this->state);
buffer.encode_bool(3, this->missing_state);
}
void NumberStateResponse::dump_to(std::string &out) const {
char buffer[64];
out.append("NumberStateResponse {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" state: ");
sprintf(buffer, "%g", this->state);
out.append(buffer);
out.append("\n");
out.append(" missing_state: ");
out.append(YESNO(this->missing_state));
out.append("\n");
out.append("}");
}
bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 2: {
this->state = value.as_float();
return true;
}
default:
return false;
}
}
void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_float(2, this->state);
}
void NumberCommandRequest::dump_to(std::string &out) const {
char buffer[64];
out.append("NumberCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" state: ");
sprintf(buffer, "%g", this->state);
out.append(buffer);
out.append("\n");
out.append("}");
}
} // namespace api
} // namespace esphome

View File

@ -782,6 +782,45 @@ class ClimateCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesNumberResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
float min_value{0.0f};
float max_value{0.0f};
float step{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void dump_to(std::string &out) const override;
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class NumberStateResponse : public ProtoMessage {
public:
uint32_t key{0};
float state{0.0f};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void dump_to(std::string &out) const override;
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class NumberCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
float state{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void dump_to(std::string &out) const override;
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
} // namespace api
} // namespace esphome

View File

@ -184,6 +184,20 @@ bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResp
#endif
#ifdef USE_CLIMATE
#endif
#ifdef USE_NUMBER
bool APIServerConnectionBase::send_list_entities_number_response(const ListEntitiesNumberResponse &msg) {
ESP_LOGVV(TAG, "send_list_entities_number_response: %s", msg.dump().c_str());
return this->send_message_<ListEntitiesNumberResponse>(msg, 49);
}
#endif
#ifdef USE_NUMBER
bool APIServerConnectionBase::send_number_state_response(const NumberStateResponse &msg) {
ESP_LOGVV(TAG, "send_number_state_response: %s", msg.dump().c_str());
return this->send_message_<NumberStateResponse>(msg, 50);
}
#endif
#ifdef USE_NUMBER
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) {
case 1: {
@ -349,6 +363,15 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
msg.decode(msg_data, msg_size);
ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str());
this->on_climate_command_request(msg);
#endif
break;
}
case 51: {
#ifdef USE_NUMBER
NumberCommandRequest msg;
msg.decode(msg_data, msg_size);
ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str());
this->on_number_command_request(msg);
#endif
break;
}
@ -547,6 +570,19 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest
this->climate_command(msg);
}
#endif
#ifdef USE_NUMBER
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->number_command(msg);
}
#endif
} // namespace api
} // namespace esphome

View File

@ -111,6 +111,15 @@ class APIServerConnectionBase : public ProtoService {
#endif
#ifdef USE_CLIMATE
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif
#ifdef USE_NUMBER
bool send_list_entities_number_response(const ListEntitiesNumberResponse &msg);
#endif
#ifdef USE_NUMBER
bool send_number_state_response(const NumberStateResponse &msg);
#endif
#ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif
protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@ -147,6 +156,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
#ifdef USE_CLIMATE
virtual void climate_command(const ClimateCommandRequest &msg) = 0;
#endif
#ifdef USE_NUMBER
virtual void number_command(const NumberCommandRequest &msg) = 0;
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
@ -179,6 +191,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_CLIMATE
void on_climate_command_request(const ClimateCommandRequest &msg) override;
#endif
#ifdef USE_NUMBER
void on_number_command_request(const NumberCommandRequest &msg) override;
#endif
};
} // namespace api

View File

@ -197,6 +197,15 @@ void APIServer::on_climate_update(climate::Climate *obj) {
}
#endif
#ifdef USE_NUMBER
void APIServer::on_number_update(number::Number *obj, float state) {
if (obj->is_internal())
return;
for (auto *c : this->clients_)
c->send_number_state(obj, state);
}
#endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -60,6 +60,9 @@ class APIServer : public Component, public Controller {
#endif
#ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override;
#endif
#ifdef USE_NUMBER
void on_number_update(number::Number *obj, float state) override;
#endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }

View File

@ -51,5 +51,9 @@ bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); }
#endif
#ifdef USE_NUMBER
bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); }
#endif
} // namespace api
} // namespace esphome

View File

@ -39,6 +39,9 @@ class ListEntitiesIterator : public ComponentIterator {
#endif
#ifdef USE_CLIMATE
bool on_climate(climate::Climate *climate) override;
#endif
#ifdef USE_NUMBER
bool on_number(number::Number *number) override;
#endif
bool on_end() override;

View File

@ -37,6 +37,11 @@ bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor)
#ifdef USE_CLIMATE
bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); }
#endif
#ifdef USE_NUMBER
bool InitialStateIterator::on_number(number::Number *number) {
return this->client_->send_number_state(number, number->state);
}
#endif
InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
: ComponentIterator(server), client_(client) {}

View File

@ -36,6 +36,9 @@ class InitialStateIterator : public ComponentIterator {
#endif
#ifdef USE_CLIMATE
bool on_climate(climate::Climate *climate) override;
#endif
#ifdef USE_NUMBER
bool on_number(number::Number *number) override;
#endif
protected:
APIConnection *client_;

View File

@ -167,6 +167,21 @@ void ComponentIterator::advance() {
}
}
break;
#endif
#ifdef USE_NUMBER
case IteratorState::NUMBER:
if (this->at_ >= App.get_numbers().size()) {
advance_platform = true;
} else {
auto *number = App.get_numbers()[this->at_];
if (number->is_internal()) {
success = true;
break;
} else {
success = this->on_number(number);
}
}
break;
#endif
case IteratorState::MAX:
if (this->on_end()) {

View File

@ -47,6 +47,9 @@ class ComponentIterator {
#endif
#ifdef USE_CLIMATE
virtual bool on_climate(climate::Climate *climate) = 0;
#endif
#ifdef USE_NUMBER
virtual bool on_number(number::Number *number) = 0;
#endif
virtual bool on_end();
@ -81,6 +84,9 @@ class ComponentIterator {
#endif
#ifdef USE_CLIMATE
CLIMATE,
#endif
#ifdef USE_NUMBER
NUMBER,
#endif
MAX,
} state_{IteratorState::NONE};

View File

@ -91,6 +91,7 @@ MQTTJSONLightComponent = mqtt_ns.class_("MQTTJSONLightComponent", MQTTComponent)
MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent)
MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent)
MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
def validate_config(value):

View File

@ -0,0 +1,61 @@
#include "mqtt_number.h"
#include "esphome/core/log.h"
#ifdef USE_NUMBER
#ifdef USE_DEEP_SLEEP
#include "esphome/components/deep_sleep/deep_sleep_component.h"
#endif
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.number";
using namespace esphome::number;
MQTTNumberComponent::MQTTNumberComponent(Number *number) : MQTTComponent(), number_(number) {}
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());
return;
}
auto call = this->number_->make_call();
call.set_value(*val);
call.perform();
});
this->number_->add_on_state_callback([this](float state) { this->publish_state(state); });
}
void MQTTNumberComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
}
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();
}
bool MQTTNumberComponent::send_initial_state() {
if (this->number_->has_state()) {
return this->publish_state(this->number_->state);
} else {
return true;
}
}
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));
}
} // namespace mqtt
} // namespace esphome
#endif

View File

@ -0,0 +1,46 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTNumberComponent : public mqtt::MQTTComponent {
public:
/** Construct this MQTTNumberComponent instance with the provided friendly_name and number
*
* @param number The number.
*/
explicit MQTTNumberComponent(number::Number *number);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Override setup.
void setup() override;
void dump_config() override;
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
bool is_internal() override;
bool publish_state(float value);
protected:
/// Override for MQTTComponent, returns "number".
std::string component_type() const override;
std::string friendly_name() const override;
number::Number *number_;
};
} // namespace mqtt
} // namespace esphome
#endif

View 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

View 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

View 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

View 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

View 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

View File

@ -0,0 +1,63 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import number
from esphome.const import (
CONF_ID,
CONF_LAMBDA,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_OPTIMISTIC,
CONF_STEP,
)
from .. import template_ns
TemplateNumber = template_ns.class_(
"TemplateNumber", number.Number, cg.PollingComponent
)
CONF_SET_ACTION = "set_action"
def validate_min_max(config):
if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]:
raise cv.Invalid("max_value must be greater than min_value")
return config
CONFIG_SCHEMA = cv.All(
number.NUMBER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TemplateNumber),
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.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
}
).extend(cv.polling_component_schema("60s")),
validate_min_max,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await number.register_number(var, config)
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_))
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]))

View File

@ -0,0 +1,39 @@
#include "template_number.h"
#include "esphome/core/log.h"
namespace esphome {
namespace template_ {
static const char *const TAG = "template.number";
TemplateNumber::TemplateNumber() : set_trigger_(new Trigger<float>()) {}
void TemplateNumber::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (val.has_value()) {
this->publish_state(*val);
}
}
void TemplateNumber::set(float value) {
this->set_trigger_->trigger(value);
if (this->optimistic_)
this->publish_state(value);
}
float TemplateNumber::get_setup_priority() const { return setup_priority::HARDWARE; }
void TemplateNumber::set_template(std::function<optional<float>()> &&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<float> *TemplateNumber::get_set_trigger() const { return this->set_trigger_; };
} // namespace template_
} // namespace esphome

View File

@ -0,0 +1,30 @@
#pragma once
#include "esphome/components/number/number.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome {
namespace template_ {
class TemplateNumber : public number::Number, public PollingComponent {
public:
TemplateNumber();
void set_template(std::function<optional<float>()> &&f);
void update() override;
void dump_config() override;
float get_setup_priority() const override;
Trigger<float> *get_set_trigger() const;
void set_optimistic(bool optimistic);
protected:
void set(float value) override;
bool optimistic_{false};
Trigger<float> *set_trigger_;
optional<std::function<optional<float>()>> f_;
};
} // namespace template_
} // namespace esphome

View File

@ -119,6 +119,12 @@ void WebServer::setup() {
if (!obj->is_internal())
client->send(this->cover_json(obj).c_str(), "state");
#endif
#ifdef USE_NUMBER
for (auto *obj : App.get_numbers())
if (!obj->is_internal())
client->send(this->number_json(obj, obj->state).c_str(), "state");
#endif
});
#ifdef USE_LOGGER
@ -196,6 +202,11 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>");
#endif
#ifdef USE_NUMBER
for (auto *obj : App.get_numbers())
write_row(stream, obj, "number", "");
#endif
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"
"<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
@ -584,6 +595,32 @@ std::string WebServer::cover_json(cover::Cover *obj) {
}
#endif
#ifdef USE_NUMBER
void WebServer::on_number_update(number::Number *obj, float state) {
this->events_.send(this->number_json(obj, state).c_str(), "state");
}
void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_numbers()) {
if (obj->is_internal())
continue;
if (obj->get_object_id() != match.id)
continue;
std::string data = this->number_json(obj, obj->state);
request->send(200, "text/json", data.c_str());
return;
}
request->send(404);
}
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;
root["value"] = value;
});
}
#endif
bool WebServer::canHandle(AsyncWebServerRequest *request) {
if (request->url() == "/")
return true;
@ -636,6 +673,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
return true;
#endif
#ifdef USE_NUMBER
if (request->method() == HTTP_GET && match.domain == "number")
return true;
#endif
return false;
}
void WebServer::handleRequest(AsyncWebServerRequest *request) {
@ -711,6 +753,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
return;
}
#endif
#ifdef USE_NUMBER
if (match.domain == "number") {
this->handle_number_request(request, match);
return;
}
#endif
}
bool WebServer::isRequestHandlerTrivial() { return false; }

View File

@ -154,6 +154,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
std::string cover_json(cover::Cover *obj);
#endif
#ifdef USE_NUMBER
void on_number_update(number::Number *obj, float state) override;
/// Handle a number request under '/number/<id>'.
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match);
/// Dump the number state with its value as a JSON string.
std::string number_json(number::Number *obj, float value);
#endif
/// Override the web handler's canHandle method.
bool canHandle(AsyncWebServerRequest *request) override;
/// Override the web handler's handleRequest method.

View File

@ -553,6 +553,7 @@ CONF_STATE_CLASS = "state_class"
CONF_STATE_TOPIC = "state_topic"
CONF_STATIC_IP = "static_ip"
CONF_STATUS = "status"
CONF_STEP = "step"
CONF_STEP_MODE = "step_mode"
CONF_STEP_PIN = "step_pin"
CONF_STOP = "stop"

View File

@ -32,6 +32,9 @@
#ifdef USE_COVER
#include "esphome/components/cover/cover.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
namespace esphome {
@ -82,6 +85,10 @@ class Application {
void register_light(light::LightState *light) { this->lights_.push_back(light); }
#endif
#ifdef USE_NUMBER
void register_number(number::Number *number) { this->numbers_.push_back(number); }
#endif
/// Register the component in this Application instance.
template<class C> C *register_component(C *c) {
static_assert(std::is_base_of<Component, C>::value, "Only Component subclasses can be registered");
@ -208,6 +215,15 @@ class Application {
return nullptr;
}
#endif
#ifdef USE_NUMBER
const std::vector<number::Number *> &get_numbers() { return this->numbers_; }
number::Number *get_number_by_key(uint32_t key, bool include_internal = false) {
for (auto *obj : this->numbers_)
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
return nullptr;
}
#endif
Scheduler scheduler;
@ -245,6 +261,9 @@ class Application {
#ifdef USE_LIGHT
std::vector<light::LightState *> lights_{};
#endif
#ifdef USE_NUMBER
std::vector<number::Number *> numbers_{};
#endif
std::string name_;
std::string compilation_time_;

View File

@ -53,6 +53,12 @@ void Controller::setup_controller() {
obj->add_on_state_callback([this, obj]() { this->on_climate_update(obj); });
}
#endif
#ifdef USE_NUMBER
for (auto *obj : App.get_numbers()) {
if (!obj->is_internal())
obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); });
}
#endif
}
} // namespace esphome

View File

@ -25,6 +25,9 @@
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
namespace esphome {
@ -55,6 +58,9 @@ class Controller {
#ifdef USE_CLIMATE
virtual void on_climate_update(climate::Climate *obj){};
#endif
#ifdef USE_NUMBER
virtual void on_number_update(number::Number *obj, float state){};
#endif
};
} // namespace esphome

View File

@ -13,6 +13,7 @@
#define USE_COVER
#define USE_LIGHT
#define USE_CLIMATE
#define USE_NUMBER
#define USE_MQTT
#define USE_POWER_SUPPLY
#define USE_HOMEASSISTANT_TIME

View File

@ -559,6 +559,7 @@ def lint_inclusive_language(fname, match):
"esphome/components/display/display_buffer.h",
"esphome/components/i2c/i2c.h",
"esphome/components/mqtt/mqtt_component.h",
"esphome/components/number/number.h",
"esphome/components/output/binary_output.h",
"esphome/components/output/float_output.h",
"esphome/components/sensor/sensor.h",

View File

@ -38,3 +38,20 @@ esp32_improv:
authorizer: io0_button
authorized_duration: 1min
status_indicator: built_in_led
number:
- platform: template
name: My template number
id: template_number_id
optimistic: true
on_value:
- logger.log:
format: "Number changed to %f"
args: ["x"]
set_action:
- logger.log:
format: "Template Number set to %f"
args: ["x"]
max_value: 100
min_value: 0
step: 5