Add select entities and implement template select (#2067)

Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
Jesse Hills 2021-08-02 20:00:51 +12:00 committed by GitHub
parent 69c7cf783e
commit 76991cdcc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1053 additions and 0 deletions

View file

@ -100,6 +100,7 @@ esphome/components/script/* @esphome/core
esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath
esphome/components/selec_meter/* @sourabhjaiswal
esphome/components/select/* @esphome/core
esphome/components/sensor/* @esphome/core
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sht4x/* @sjtrny

View file

@ -39,6 +39,7 @@ service APIConnection {
rpc camera_image (CameraImageRequest) returns (void) {}
rpc climate_command (ClimateCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {}
rpc select_command (SelectCommandRequest) returns (void) {}
}
@ -867,3 +868,39 @@ message NumberCommandRequest {
fixed32 key = 1;
float state = 2;
}
// ==================== SELECT ====================
message ListEntitiesSelectResponse {
option (id) = 52;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
repeated string options = 6;
}
message SelectStateResponse {
option (id) = 53;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT";
option (no_delay) = true;
fixed32 key = 1;
string state = 2;
// If the select does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
}
message SelectCommandRequest {
option (id) = 54;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SELECT";
option (no_delay) = true;
fixed32 key = 1;
string state = 2;
}

View file

@ -609,6 +609,41 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
}
#endif
#ifdef USE_SELECT
bool APIConnection::send_select_state(select::Select *select, std::string state) {
if (!this->state_subscription_)
return false;
SelectStateResponse resp{};
resp.key = select->get_object_id_hash();
resp.state = std::move(state);
resp.missing_state = !select->has_state();
return this->send_select_state_response(resp);
}
bool APIConnection::send_select_info(select::Select *select) {
ListEntitiesSelectResponse msg;
msg.key = select->get_object_id_hash();
msg.object_id = select->get_object_id();
msg.name = select->get_name();
msg.unique_id = get_default_unique_id("select", select);
msg.icon = select->traits.get_icon();
for (const auto &option : select->traits.get_options())
msg.options.push_back(option);
return this->send_list_entities_select_response(msg);
}
void APIConnection::select_command(const SelectCommandRequest &msg) {
select::Select *select = App.get_select_by_key(msg.key);
if (select == nullptr)
return;
auto call = select->make_call();
call.set_option(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

@ -67,6 +67,11 @@ class APIConnection : public APIServerConnection {
bool send_number_state(number::Number *number, float state);
bool send_number_info(number::Number *number);
void number_command(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
bool send_select_state(select::Select *select, std::string state);
bool send_select_info(select::Select *select);
void select_command(const SelectCommandRequest &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

@ -3574,6 +3574,172 @@ void NumberCommandRequest::dump_to(std::string &out) const {
out.append("\n");
out.append("}");
}
bool ListEntitiesSelectResponse::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;
}
case 6: {
this->options.push_back(value.as_string());
return true;
}
default:
return false;
}
}
bool ListEntitiesSelectResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesSelectResponse::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);
for (auto &it : this->options) {
buffer.encode_string(6, it, true);
}
}
void ListEntitiesSelectResponse::dump_to(std::string &out) const {
char buffer[64];
out.append("ListEntitiesSelectResponse {\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");
for (const auto &it : this->options) {
out.append(" options: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append("}");
}
bool SelectStateResponse::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 SelectStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->state = value.as_string();
return true;
}
default:
return false;
}
}
bool SelectStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void SelectStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_string(2, this->state);
buffer.encode_bool(3, this->missing_state);
}
void SelectStateResponse::dump_to(std::string &out) const {
char buffer[64];
out.append("SelectStateResponse {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" state: ");
out.append("'").append(this->state).append("'");
out.append("\n");
out.append(" missing_state: ");
out.append(YESNO(this->missing_state));
out.append("\n");
out.append("}");
}
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->state = value.as_string();
return true;
}
default:
return false;
}
}
bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_string(2, this->state);
}
void SelectCommandRequest::dump_to(std::string &out) const {
char buffer[64];
out.append("SelectCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" state: ");
out.append("'").append(this->state).append("'");
out.append("\n");
out.append("}");
}
} // namespace api
} // namespace esphome

View file

@ -849,6 +849,45 @@ class NumberCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
class ListEntitiesSelectResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
std::vector<std::string> options{};
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 SelectStateResponse : public ProtoMessage {
public:
uint32_t key{0};
std::string state{};
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_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SelectCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
std::string state{};
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;
};
} // namespace api
} // namespace esphome

View file

@ -198,6 +198,20 @@ bool APIServerConnectionBase::send_number_state_response(const NumberStateRespon
#endif
#ifdef USE_NUMBER
#endif
#ifdef USE_SELECT
bool APIServerConnectionBase::send_list_entities_select_response(const ListEntitiesSelectResponse &msg) {
ESP_LOGVV(TAG, "send_list_entities_select_response: %s", msg.dump().c_str());
return this->send_message_<ListEntitiesSelectResponse>(msg, 52);
}
#endif
#ifdef USE_SELECT
bool APIServerConnectionBase::send_select_state_response(const SelectStateResponse &msg) {
ESP_LOGVV(TAG, "send_select_state_response: %s", msg.dump().c_str());
return this->send_message_<SelectStateResponse>(msg, 53);
}
#endif
#ifdef USE_SELECT
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) {
case 1: {
@ -372,6 +386,15 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
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;
}
case 54: {
#ifdef USE_SELECT
SelectCommandRequest msg;
msg.decode(msg_data, msg_size);
ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
this->on_select_command_request(msg);
#endif
break;
}
@ -583,6 +606,19 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest &
this->number_command(msg);
}
#endif
#ifdef USE_SELECT
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->select_command(msg);
}
#endif
} // namespace api
} // namespace esphome

View file

@ -120,6 +120,15 @@ class APIServerConnectionBase : public ProtoService {
#endif
#ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif
#ifdef USE_SELECT
bool send_list_entities_select_response(const ListEntitiesSelectResponse &msg);
#endif
#ifdef USE_SELECT
bool send_select_state_response(const SelectStateResponse &msg);
#endif
#ifdef USE_SELECT
virtual void on_select_command_request(const SelectCommandRequest &value){};
#endif
protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@ -159,6 +168,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
#ifdef USE_NUMBER
virtual void number_command(const NumberCommandRequest &msg) = 0;
#endif
#ifdef USE_SELECT
virtual void select_command(const SelectCommandRequest &msg) = 0;
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
@ -194,6 +206,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_NUMBER
void on_number_command_request(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
void on_select_command_request(const SelectCommandRequest &msg) override;
#endif
};
} // namespace api

View file

@ -206,6 +206,15 @@ void APIServer::on_number_update(number::Number *obj, float state) {
}
#endif
#ifdef USE_SELECT
void APIServer::on_select_update(select::Select *obj, const std::string &state) {
if (obj->is_internal())
return;
for (auto *c : this->clients_)
c->send_select_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

@ -62,6 +62,9 @@ class APIServer : public Component, public Controller {
#endif
#ifdef USE_NUMBER
void on_number_update(number::Number *obj, float state) override;
#endif
#ifdef USE_SELECT
void on_select_update(select::Select *obj, const std::string &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

@ -55,5 +55,9 @@ bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->
bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); }
#endif
#ifdef USE_SELECT
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
#endif
} // namespace api
} // namespace esphome

View file

@ -42,6 +42,9 @@ class ListEntitiesIterator : public ComponentIterator {
#endif
#ifdef USE_NUMBER
bool on_number(number::Number *number) override;
#endif
#ifdef USE_SELECT
bool on_select(select::Select *select) override;
#endif
bool on_end() override;

View file

@ -42,6 +42,11 @@ bool InitialStateIterator::on_number(number::Number *number) {
return this->client_->send_number_state(number, number->state);
}
#endif
#ifdef USE_SELECT
bool InitialStateIterator::on_select(select::Select *select) {
return this->client_->send_select_state(select, select->state);
}
#endif
InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
: ComponentIterator(server), client_(client) {}

View file

@ -39,6 +39,9 @@ class InitialStateIterator : public ComponentIterator {
#endif
#ifdef USE_NUMBER
bool on_number(number::Number *number) override;
#endif
#ifdef USE_SELECT
bool on_select(select::Select *select) override;
#endif
protected:
APIConnection *client_;

View file

@ -182,6 +182,21 @@ void ComponentIterator::advance() {
}
}
break;
#endif
#ifdef USE_SELECT
case IteratorState::SELECT:
if (this->at_ >= App.get_selects().size()) {
advance_platform = true;
} else {
auto *select = App.get_selects()[this->at_];
if (select->is_internal()) {
success = true;
break;
} else {
success = this->on_select(select);
}
}
break;
#endif
case IteratorState::MAX:
if (this->on_end()) {

View file

@ -50,6 +50,9 @@ class ComponentIterator {
#endif
#ifdef USE_NUMBER
virtual bool on_number(number::Number *number) = 0;
#endif
#ifdef USE_SELECT
virtual bool on_select(select::Select *select) = 0;
#endif
virtual bool on_end();
@ -87,6 +90,9 @@ class ComponentIterator {
#endif
#ifdef USE_NUMBER
NUMBER,
#endif
#ifdef USE_SELECT
SELECT,
#endif
MAX,
} state_{IteratorState::NONE};

View file

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

View file

@ -0,0 +1,58 @@
#include "mqtt_select.h"
#include "esphome/core/log.h"
#ifdef USE_SELECT
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.select";
using namespace esphome::select;
MQTTSelectComponent::MQTTSelectComponent(Select *select) : MQTTComponent(), select_(select) {}
void MQTTSelectComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
auto call = this->select_->make_call();
call.set_option(state);
call.perform();
});
this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); });
}
void MQTTSelectComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
}
std::string MQTTSelectComponent::component_type() const { return "select"; }
std::string MQTTSelectComponent::friendly_name() const { return this->select_->get_name(); }
void MQTTSelectComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) {
const auto &traits = select_->traits;
// https://www.home-assistant.io/integrations/select.mqtt/
if (!traits.get_icon().empty())
root["icon"] = traits.get_icon();
JsonArray &options = root.createNestedArray("options");
for (const auto &option : traits.get_options())
options.add(option);
config.command_topic = true;
}
bool MQTTSelectComponent::send_initial_state() {
if (this->select_->has_state()) {
return this->publish_state(this->select_->state);
} else {
return true;
}
}
bool MQTTSelectComponent::is_internal() { return this->select_->is_internal(); }
bool MQTTSelectComponent::publish_state(const std::string &value) {
return this->publish(this->get_state_topic_(), value);
}
} // namespace mqtt
} // namespace esphome
#endif

View file

@ -0,0 +1,46 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTSelectComponent : public mqtt::MQTTComponent {
public:
/** Construct this MQTTSelectComponent instance with the provided friendly_name and select
*
* @param select The select.
*/
explicit MQTTSelectComponent(select::Select *select);
// ========== 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(const std::string &value);
protected:
/// Override for MQTTComponent, returns "select".
std::string component_type() const override;
std::string friendly_name() const override;
select::Select *select_;
};
} // namespace mqtt
} // namespace esphome
#endif

View file

@ -0,0 +1,102 @@
from typing import List
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import mqtt
from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_INTERNAL,
CONF_ON_VALUE,
CONF_OPTION,
CONF_TRIGGER_ID,
CONF_NAME,
CONF_MQTT_ID,
ICON_EMPTY,
)
from esphome.core import CORE, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
select_ns = cg.esphome_ns.namespace("select")
Select = select_ns.class_("Select", cg.Nameable)
SelectPtr = Select.operator("ptr")
# Triggers
SelectStateTrigger = select_ns.class_(
"SelectStateTrigger", automation.Trigger.template(cg.float_)
)
# Actions
SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
icon = cv.icon
SELECT_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent),
cv.GenerateID(): cv.declare_id(Select),
cv.Optional(CONF_ICON, default=ICON_EMPTY): icon,
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SelectStateTrigger),
}
),
}
)
async def setup_select_core_(var, config, *, options: List[str]):
cg.add(var.set_name(config[CONF_NAME]))
if CONF_INTERNAL in config:
cg.add(var.set_internal(config[CONF_INTERNAL]))
cg.add(var.traits.set_icon(config[CONF_ICON]))
cg.add(var.traits.set_options(options))
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
await mqtt.register_mqtt_component(mqtt_, config)
async def register_select(var, config, *, options: List[str]):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_select(var))
await setup_select_core_(var, config, options=options)
async def new_select(config, *, options: List[str]):
var = cg.new_Pvariable(config[CONF_ID])
await register_select(var, config, options=options)
return var
@coroutine_with_priority(40.0)
async def to_code(config):
cg.add_define("USE_SELECT")
cg.add_global(select_ns.using)
@automation.register_action(
"select.set",
SelectSetAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(Select),
cv.Required(CONF_OPTION): cv.templatable(cv.string_strict),
}
),
)
async def select_set_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_OPTION], args, str)
cg.add(var.set_option(template_))
return var

View file

@ -0,0 +1,33 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "select.h"
namespace esphome {
namespace select {
class SelectStateTrigger : public Trigger<std::string> {
public:
explicit SelectStateTrigger(Select *parent) {
parent->add_on_state_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
template<typename... Ts> class SelectSetAction : public Action<Ts...> {
public:
SelectSetAction(Select *select) : select_(select) {}
TEMPLATABLE_VALUE(std::string, option)
void play(Ts... x) override {
auto call = this->select_->make_call();
call.set_option(this->option_.value(x...));
call.perform();
}
protected:
Select *select_;
};
} // namespace select
} // namespace esphome

View file

@ -0,0 +1,43 @@
#include "select.h"
#include "esphome/core/log.h"
namespace esphome {
namespace select {
static const char *const TAG = "select";
void SelectCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
if (!this->option_.has_value()) {
ESP_LOGW(TAG, "No value set for SelectCall");
return;
}
const auto &traits = this->parent_->traits;
auto value = *this->option_;
auto options = traits.get_options();
if (std::find(options.begin(), options.end(), value) == options.end()) {
ESP_LOGW(TAG, " Option %s is not a valid option.", value.c_str());
return;
}
ESP_LOGD(TAG, " Option: %s", (*this->option_).c_str());
this->parent_->control(*this->option_);
}
void Select::publish_state(const std::string &state) {
this->has_state_ = true;
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str());
this->state_callback_.call(state);
}
void Select::add_on_state_callback(std::function<void(std::string)> &&callback) {
this->state_callback_.add(std::move(callback));
}
uint32_t Select::hash_base() { return 2812997003UL; }
} // namespace select
} // namespace esphome

View file

@ -0,0 +1,87 @@
#pragma once
#include <set>
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace select {
#define LOG_SELECT(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \
if (!(obj)->traits.get_icon().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->traits.get_icon().c_str()); \
} \
}
class Select;
class SelectCall {
public:
explicit SelectCall(Select *parent) : parent_(parent) {}
void perform();
SelectCall &set_option(const std::string &option) {
option_ = option;
return *this;
}
const optional<std::string> &get_option() const { return option_; }
protected:
Select *const parent_;
optional<std::string> option_;
};
class SelectTraits {
public:
void set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
const std::vector<std::string> get_options() const { return this->options_; }
void set_icon(std::string icon) { icon_ = std::move(icon); }
const std::string &get_icon() const { return icon_; }
protected:
std::vector<std::string> options_;
std::string icon_;
};
/** Base-class for all selects.
*
* A select can use publish_state to send out a new value.
*/
class Select : public Nameable {
public:
std::string state;
void publish_state(const std::string &state);
SelectCall make_call() { return SelectCall(this); }
void set(const std::string &value) { make_call().set_option(value).perform(); }
void add_on_state_callback(std::function<void(std::string)> &&callback);
SelectTraits traits;
/// Return whether this select has gotten a full state yet.
bool has_state() const { return has_state_; }
protected:
friend class SelectCall;
/** Set the value of the select, this is a virtual method that each select integration must implement.
*
* This method is called by the SelectCall.
*
* @param value The value as validated by the SelectCall.
*/
virtual void control(const std::string &value) = 0;
uint32_t hash_base() override;
CallbackManager<void(std::string)> state_callback_;
bool has_state_{false};
};
} // namespace select
} // namespace esphome

View file

@ -0,0 +1,74 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import select
from esphome.const import (
CONF_ID,
CONF_INITIAL_OPTION,
CONF_LAMBDA,
CONF_OPTIONS,
CONF_OPTIMISTIC,
CONF_RESTORE_VALUE,
)
from .. import template_ns
TemplateSelect = template_ns.class_(
"TemplateSelect", select.Select, cg.PollingComponent
)
CONF_SET_ACTION = "set_action"
def validate_initial_value_in_options(config):
if CONF_INITIAL_OPTION in config:
if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]:
raise cv.Invalid(
f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]"
)
else:
config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0]
return config
CONFIG_SCHEMA = cv.All(
select.SELECT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TemplateSelect),
cv.Required(CONF_OPTIONS): cv.All(
cv.ensure_list(cv.string_strict), cv.Length(min=1)
),
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_OPTIMISTIC): cv.boolean,
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_INITIAL_OPTION): cv.string_strict,
cv.Optional(CONF_RESTORE_VALUE): cv.boolean,
}
).extend(cv.polling_component_schema("60s")),
validate_initial_value_in_options,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await select.register_select(var, config, options=config[CONF_OPTIONS])
if CONF_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(str)
)
cg.add(var.set_template(template_))
else:
if CONF_OPTIMISTIC in config:
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION]))
if CONF_RESTORE_VALUE in config:
cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE]))
if CONF_SET_ACTION in config:
await automation.build_automation(
var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION]
)

View file

@ -0,0 +1,74 @@
#include "template_select.h"
#include "esphome/core/log.h"
namespace esphome {
namespace template_ {
static const char *const TAG = "template.select";
void TemplateSelect::setup() {
if (this->f_.has_value())
return;
std::string value;
ESP_LOGD(TAG, "Setting up Template Number");
if (!this->restore_value_) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
} else {
size_t index;
this->pref_ = global_preferences.make_preference<size_t>(this->get_object_id_hash());
if (!this->pref_.load(&index)) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial (could not load): %s", value.c_str());
} else {
value = this->traits.get_options().at(index);
ESP_LOGD(TAG, "State from restore: %s", value.c_str());
}
}
this->publish_state(value);
}
void TemplateSelect::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
auto options = this->traits.get_options();
if (std::find(options.begin(), options.end(), *val) == options.end()) {
ESP_LOGE(TAG, "lambda returned an invalid option %s", (*val).c_str());
return;
}
this->publish_state(*val);
}
void TemplateSelect::control(const std::string &value) {
this->set_trigger_->trigger(value);
if (this->optimistic_)
this->publish_state(value);
if (this->restore_value_) {
auto options = this->traits.get_options();
size_t index = std::find(options.begin(), options.end(), value) - options.begin();
this->pref_.save(&index);
}
}
void TemplateSelect::dump_config() {
LOG_SELECT("", "Template Select", this);
LOG_UPDATE_INTERVAL(this);
if (this->f_.has_value())
return;
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
ESP_LOGCONFIG(TAG, " Initial Option: %s", this->initial_option_.c_str());
ESP_LOGCONFIG(TAG, " Restore Value: %s", YESNO(this->restore_value_));
}
} // namespace template_
} // namespace esphome

View file

@ -0,0 +1,37 @@
#pragma once
#include "esphome/components/select/select.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace template_ {
class TemplateSelect : public select::Select, public PollingComponent {
public:
void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_option(std::string initial_option) { this->initial_option_ = std::move(initial_option); }
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
protected:
void control(const std::string &value) override;
bool optimistic_ = false;
std::string initial_option_;
bool restore_value_ = false;
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
optional<std::function<optional<std::string>()>> f_;
ESPPreferenceObject pref_;
};
} // namespace template_
} // namespace esphome

View file

@ -129,6 +129,12 @@ void WebServer::setup() {
if (!obj->is_internal())
client->send(this->number_json(obj, obj->state).c_str(), "state");
#endif
#ifdef USE_SELECT
for (auto *obj : App.get_selects())
if (!obj->is_internal())
client->send(this->select_json(obj, obj->state).c_str(), "state");
#endif
});
#ifdef USE_LOGGER
@ -211,6 +217,11 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
write_row(stream, obj, "number", "");
#endif
#ifdef USE_SELECT
for (auto *obj : App.get_selects())
write_row(stream, obj, "select", "");
#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 "
@ -626,6 +637,31 @@ std::string WebServer::number_json(number::Number *obj, float value) {
}
#endif
#ifdef USE_SELECT
void WebServer::on_select_update(select::Select *obj, const std::string &state) {
this->events_.send(this->select_json(obj, state).c_str(), "state");
}
void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_selects()) {
if (obj->is_internal())
continue;
if (obj->get_object_id() != match.id)
continue;
std::string data = this->select_json(obj, obj->state);
request->send(200, "text/json", data.c_str());
return;
}
request->send(404);
}
std::string WebServer::select_json(select::Select *obj, const std::string &value) {
return json::build_json([obj, value](JsonObject &root) {
root["id"] = "select-" + obj->get_object_id();
root["state"] = value;
root["value"] = value;
});
}
#endif
bool WebServer::canHandle(AsyncWebServerRequest *request) {
if (request->url() == "/")
return true;
@ -683,6 +719,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
return true;
#endif
#ifdef USE_SELECT
if (request->method() == HTTP_GET && match.domain == "select")
return true;
#endif
return false;
}
void WebServer::handleRequest(AsyncWebServerRequest *request) {
@ -765,6 +806,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
return;
}
#endif
#ifdef USE_SELECT
if (match.domain == "select") {
this->handle_select_request(request, match);
return;
}
#endif
}
bool WebServer::isRequestHandlerTrivial() { return false; }

View file

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

View file

@ -278,6 +278,7 @@ CONF_INCLUDES = "includes"
CONF_INDEX = "index"
CONF_INDOOR = "indoor"
CONF_INITIAL_MODE = "initial_mode"
CONF_INITIAL_OPTION = "initial_option"
CONF_INITIAL_VALUE = "initial_value"
CONF_INTEGRATION_TIME = "integration_time"
CONF_INTENSITY = "intensity"
@ -407,6 +408,8 @@ CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt"
CONF_OPEN_DURATION = "open_duration"
CONF_OPEN_ENDSTOP = "open_endstop"
CONF_OPTIMISTIC = "optimistic"
CONF_OPTION = "option"
CONF_OPTIONS = "options"
CONF_OR = "or"
CONF_OSCILLATING = "oscillating"
CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"

View file

@ -35,6 +35,9 @@
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
namespace esphome {
@ -89,6 +92,10 @@ class Application {
void register_number(number::Number *number) { this->numbers_.push_back(number); }
#endif
#ifdef USE_SELECT
void register_select(select::Select *select) { this->selects_.push_back(select); }
#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");
@ -224,6 +231,15 @@ class Application {
return nullptr;
}
#endif
#ifdef USE_SELECT
const std::vector<select::Select *> &get_selects() { return this->selects_; }
select::Select *get_select_by_key(uint32_t key, bool include_internal = false) {
for (auto *obj : this->selects_)
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
return nullptr;
}
#endif
Scheduler scheduler;
@ -264,6 +280,9 @@ class Application {
#ifdef USE_NUMBER
std::vector<number::Number *> numbers_{};
#endif
#ifdef USE_SELECT
std::vector<select::Select *> selects_{};
#endif
std::string name_;
std::string compilation_time_;

View file

@ -59,6 +59,12 @@ void Controller::setup_controller() {
obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); });
}
#endif
#ifdef USE_SELECT
for (auto *obj : App.get_selects()) {
if (!obj->is_internal())
obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); });
}
#endif
}
} // namespace esphome

View file

@ -28,6 +28,9 @@
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
namespace esphome {
@ -61,6 +64,9 @@ class Controller {
#ifdef USE_NUMBER
virtual void on_number_update(number::Number *obj, float state){};
#endif
#ifdef USE_SELECT
virtual void on_select_update(select::Select *obj, const std::string &state){};
#endif
};
} // namespace esphome

View file

@ -14,6 +14,7 @@
#define USE_LIGHT
#define USE_CLIMATE
#define USE_NUMBER
#define USE_SELECT
#define USE_MQTT
#define USE_POWER_SUPPLY
#define USE_HOMEASSISTANT_TIME

View file

@ -563,6 +563,7 @@ def lint_inclusive_language(fname, match):
"esphome/components/output/binary_output.h",
"esphome/components/output/float_output.h",
"esphome/components/nextion/nextion_base.h",
"esphome/components/select/select.h",
"esphome/components/sensor/sensor.h",
"esphome/components/stepper/stepper.h",
"esphome/components/switch/switch.h",

View file

@ -82,6 +82,29 @@ number:
min_value: 0
step: 5
select:
- platform: template
name: My template select
id: template_select_id
optimistic: true
initial_option: two
restore_value: true
on_value:
- logger.log:
format: "Select changed to %s"
args: ["x.c_str()"]
set_action:
- logger.log:
format: "Template Select set to %s"
args: ["x.c_str()"]
- select.set:
id: template_select_id
option: two
options:
- one
- two
- three
sensor:
- platform: selec_meter
total_active_energy: