Select enhancement (#3423)

Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
This commit is contained in:
Maurice Makaay 2022-05-10 06:41:16 +02:00 committed by GitHub
parent 3a3d97dfa7
commit 44b68f140e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 461 additions and 78 deletions

View file

@ -64,6 +64,7 @@ from esphome.cpp_types import ( # noqa
uint64,
int32,
int64,
size_t,
const_char_ptr,
NAN,
esphome_ns,

View file

@ -255,7 +255,7 @@ 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) {
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)

View file

@ -64,7 +64,7 @@ class APIServer : public Component, public Controller {
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;
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;

View file

@ -7,7 +7,7 @@ namespace copy {
static const char *const TAG = "copy.select";
void CopySelect::setup() {
source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
traits.set_options(source_->traits.get_options());

View file

@ -21,7 +21,7 @@ void MQTTSelectComponent::setup() {
call.set_option(state);
call.perform();
});
this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); });
this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); });
}
void MQTTSelectComponent::dump_config() {

View file

@ -9,6 +9,10 @@ from esphome.const import (
CONF_OPTION,
CONF_TRIGGER_ID,
CONF_MQTT_ID,
CONF_CYCLE,
CONF_MODE,
CONF_OPERATION,
CONF_INDEX,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
@ -22,14 +26,27 @@ SelectPtr = Select.operator("ptr")
# Triggers
SelectStateTrigger = select_ns.class_(
"SelectStateTrigger", automation.Trigger.template(cg.float_)
"SelectStateTrigger",
automation.Trigger.template(cg.std_string, cg.size_t),
)
# Actions
SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action)
SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action)
# Enums
SelectOperation = select_ns.enum("SelectOperation")
SELECT_OPERATION_OPTIONS = {
"NEXT": SelectOperation.SELECT_OP_NEXT,
"PREVIOUS": SelectOperation.SELECT_OP_PREVIOUS,
"FIRST": SelectOperation.SELECT_OP_FIRST,
"LAST": SelectOperation.SELECT_OP_LAST,
}
icon = cv.icon
SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent),
@ -50,7 +67,9 @@ async def setup_select_core_(var, config, *, options: List[str]):
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)
await automation.build_automation(
trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf
)
if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
@ -76,12 +95,18 @@ async def to_code(config):
cg.add_global(select_ns.using)
OPERATION_BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(Select),
}
)
@automation.register_action(
"select.set",
SelectSetAction,
cv.Schema(
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.use_id(Select),
cv.Required(CONF_OPTION): cv.templatable(cv.string_strict),
}
),
@ -92,3 +117,96 @@ async def select_set_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string)
cg.add(var.set_option(template_))
return var
@automation.register_action(
"select.set_index",
SelectSetIndexAction,
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_INDEX): cv.templatable(cv.positive_int),
}
),
)
async def select_set_index_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_INDEX], args, cg.size_t)
cg.add(var.set_index(template_))
return var
@automation.register_action(
"select.operation",
SelectOperationAction,
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_OPERATION): cv.templatable(
cv.enum(SELECT_OPERATION_OPTIONS, upper=True)
),
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
}
),
)
@automation.register_action(
"select.next",
SelectOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="NEXT"): cv.one_of("NEXT", upper=True),
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
}
)
),
)
@automation.register_action(
"select.previous",
SelectOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="PREVIOUS"): cv.one_of(
"PREVIOUS", upper=True
),
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
}
)
),
)
@automation.register_action(
"select.first",
SelectOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="FIRST"): cv.one_of("FIRST", upper=True),
}
)
),
)
@automation.register_action(
"select.last",
SelectOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="LAST"): cv.one_of("LAST", upper=True),
}
)
),
)
async def select_operation_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_OPERATION in config:
op_ = await cg.templatable(config[CONF_OPERATION], args, SelectOperation)
cg.add(var.set_operation(op_))
if CONF_CYCLE in config:
cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool)
cg.add(var.set_cycle(cycle_))
if CONF_MODE in config:
cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[config[CONF_MODE]]))
if CONF_CYCLE in config:
cg.add(var.set_cycle(config[CONF_CYCLE]))
return var

View file

@ -7,16 +7,16 @@
namespace esphome {
namespace select {
class SelectStateTrigger : public Trigger<std::string> {
class SelectStateTrigger : public Trigger<std::string, size_t> {
public:
explicit SelectStateTrigger(Select *parent) {
parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); });
parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); });
}
};
template<typename... Ts> class SelectSetAction : public Action<Ts...> {
public:
SelectSetAction(Select *select) : select_(select) {}
explicit SelectSetAction(Select *select) : select_(select) {}
TEMPLATABLE_VALUE(std::string, option)
void play(Ts... x) override {
@ -29,5 +29,39 @@ template<typename... Ts> class SelectSetAction : public Action<Ts...> {
Select *select_;
};
template<typename... Ts> class SelectSetIndexAction : public Action<Ts...> {
public:
explicit SelectSetIndexAction(Select *select) : select_(select) {}
TEMPLATABLE_VALUE(size_t, index)
void play(Ts... x) override {
auto call = this->select_->make_call();
call.set_index(this->index_.value(x...));
call.perform();
}
protected:
Select *select_;
};
template<typename... Ts> class SelectOperationAction : public Action<Ts...> {
public:
explicit SelectOperationAction(Select *select) : select_(select) {}
TEMPLATABLE_VALUE(bool, cycle)
TEMPLATABLE_VALUE(SelectOperation, operation)
void play(Ts... x) override {
auto call = this->select_->make_call();
call.with_operation(this->operation_.value(x...));
if (this->cycle_.has_value()) {
call.with_cycle(this->cycle_.value(x...));
}
call.perform();
}
protected:
Select *select_;
};
} // namespace select
} // namespace esphome

View file

@ -6,37 +6,53 @@ 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);
auto index = this->index_of(state);
const auto *name = this->get_name().c_str();
if (index.has_value()) {
this->has_state_ = true;
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %s (index %d)", name, state.c_str(), index.value());
this->state_callback_.call(state, index.value());
} else {
ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str());
}
}
void Select::add_on_state_callback(std::function<void(std::string)> &&callback) {
void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) {
this->state_callback_.add(std::move(callback));
}
size_t Select::size() const {
auto options = traits.get_options();
return options.size();
}
optional<size_t> Select::index_of(const std::string &option) const {
auto options = traits.get_options();
auto it = std::find(options.begin(), options.end(), option);
if (it == options.end()) {
return {};
}
return std::distance(options.begin(), it);
}
optional<size_t> Select::active_index() const {
if (this->has_state()) {
return this->index_of(this->state);
} else {
return {};
}
}
optional<std::string> Select::at(size_t index) const {
auto options = traits.get_options();
if (index >= options.size()) {
return {};
}
return options.at(index);
}
uint32_t Select::hash_base() { return 2812997003UL; }
} // namespace select

View file

@ -1,10 +1,10 @@
#pragma once
#include <set>
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "select_call.h"
#include "select_traits.h"
namespace esphome {
namespace select {
@ -17,33 +17,6 @@ namespace select {
} \
}
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); }
std::vector<std::string> get_options() const { return this->options_; }
protected:
std::vector<std::string> options_;
};
/** Base-class for all selects.
*
* A select can use publish_state to send out a new value.
@ -51,18 +24,23 @@ class SelectTraits {
class Select : public EntityBase {
public:
std::string state;
SelectTraits traits;
void publish_state(const std::string &state);
/// Return whether this select has gotten a full state yet.
bool has_state() const { return has_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);
// Methods that provide an API to index-based access.
size_t size() const;
optional<size_t> index_of(const std::string &option) const;
optional<size_t> active_index() const;
optional<std::string> at(size_t index) const;
SelectTraits traits;
/// Return whether this select has gotten a full state yet.
bool has_state() const { return has_state_; }
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
protected:
friend class SelectCall;
@ -77,7 +55,7 @@ class Select : public EntityBase {
uint32_t hash_base() override;
CallbackManager<void(std::string)> state_callback_;
CallbackManager<void(std::string, size_t)> state_callback_;
bool has_state_{false};
};

View file

@ -0,0 +1,122 @@
#include "select_call.h"
#include "select.h"
#include "esphome/core/log.h"
namespace esphome {
namespace select {
static const char *const TAG = "select";
SelectCall &SelectCall::set_option(const std::string &option) {
return with_operation(SELECT_OP_SET).with_option(option);
}
SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); }
const optional<std::string> &SelectCall::get_option() const { return option_; }
SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); }
SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); }
SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); }
SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); }
SelectCall &SelectCall::with_operation(SelectOperation operation) {
this->operation_ = operation;
return *this;
}
SelectCall &SelectCall::with_cycle(bool cycle) {
this->cycle_ = cycle;
return *this;
}
SelectCall &SelectCall::with_option(const std::string &option) {
this->option_ = option;
return *this;
}
SelectCall &SelectCall::with_index(size_t index) {
this->index_ = index;
return *this;
}
void SelectCall::perform() {
auto *parent = this->parent_;
const auto *name = parent->get_name().c_str();
const auto &traits = parent->traits;
auto options = traits.get_options();
if (this->operation_ == SELECT_OP_NONE) {
ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name);
return;
}
if (options.empty()) {
ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name);
return;
}
std::string target_value;
if (this->operation_ == SELECT_OP_SET) {
ESP_LOGD(TAG, "'%s' - Setting", name);
if (!this->option_.has_value()) {
ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name);
return;
}
target_value = this->option_.value();
} else if (this->operation_ == SELECT_OP_SET_INDEX) {
if (!this->index_.has_value()) {
ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name);
return;
}
if (this->index_.value() >= options.size()) {
ESP_LOGW(TAG, "'%s' - Index value %d out of bounds", name, this->index_.value());
return;
}
target_value = options[this->index_.value()];
} else if (this->operation_ == SELECT_OP_FIRST) {
target_value = options.front();
} else if (this->operation_ == SELECT_OP_LAST) {
target_value = options.back();
} else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) {
auto cycle = this->cycle_;
ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous",
cycle ? "" : "out");
if (!parent->has_state()) {
target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
} else {
auto index = parent->index_of(parent->state);
if (index.has_value()) {
auto size = options.size();
if (cycle) {
auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size;
target_value = options[use_index];
} else {
if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) {
target_value = options[index.value() - 1];
} else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) {
target_value = options[index.value() + 1];
} else {
return;
}
}
} else {
target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
}
}
}
if (std::find(options.begin(), options.end(), target_value) == options.end()) {
ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str());
return;
}
ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str());
parent->control(target_value);
}
} // namespace select
} // namespace esphome

View file

@ -0,0 +1,48 @@
#pragma once
#include "esphome/core/helpers.h"
namespace esphome {
namespace select {
class Select;
enum SelectOperation {
SELECT_OP_NONE,
SELECT_OP_SET,
SELECT_OP_SET_INDEX,
SELECT_OP_NEXT,
SELECT_OP_PREVIOUS,
SELECT_OP_FIRST,
SELECT_OP_LAST
};
class SelectCall {
public:
explicit SelectCall(Select *parent) : parent_(parent) {}
void perform();
SelectCall &set_option(const std::string &option);
SelectCall &set_index(size_t index);
const optional<std::string> &get_option() const;
SelectCall &select_next(bool cycle);
SelectCall &select_previous(bool cycle);
SelectCall &select_first();
SelectCall &select_last();
SelectCall &with_operation(SelectOperation operation);
SelectCall &with_cycle(bool cycle);
SelectCall &with_option(const std::string &option);
SelectCall &with_index(size_t index);
protected:
Select *const parent_;
optional<std::string> option_;
optional<size_t> index_;
SelectOperation operation_{SELECT_OP_NONE};
bool cycle_;
};
} // namespace select
} // namespace esphome

View file

@ -0,0 +1,11 @@
#include "select_traits.h"
namespace esphome {
namespace select {
void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
std::vector<std::string> SelectTraits::get_options() const { return this->options_; }
} // namespace select
} // namespace esphome

View file

@ -0,0 +1,19 @@
#pragma once
#include <vector>
#include <string>
namespace esphome {
namespace select {
class SelectTraits {
public:
void set_options(std::vector<std::string> options);
std::vector<std::string> get_options() const;
protected:
std::vector<std::string> options_;
};
} // namespace select
} // namespace esphome

View file

@ -755,7 +755,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
#endif
#ifdef USE_SELECT
void WebServer::on_select_update(select::Select *obj, const std::string &state) {
void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
this->events_.send(this->select_json(obj, state, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {

View file

@ -185,7 +185,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_SELECT
void on_select_update(select::Select *obj, const std::string &state) override;
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
/// Handle a select request under '/select/<id>'.
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match);

View file

@ -138,6 +138,7 @@ CONF_CUSTOM_FAN_MODE = "custom_fan_mode"
CONF_CUSTOM_FAN_MODES = "custom_fan_modes"
CONF_CUSTOM_PRESET = "custom_preset"
CONF_CUSTOM_PRESETS = "custom_presets"
CONF_CYCLE = "cycle"
CONF_DALLAS_ID = "dallas_id"
CONF_DATA = "data"
CONF_DATA_PIN = "data_pin"
@ -458,6 +459,7 @@ CONF_OPEN_DRAIN = "open_drain"
CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt"
CONF_OPEN_DURATION = "open_duration"
CONF_OPEN_ENDSTOP = "open_endstop"
CONF_OPERATION = "operation"
CONF_OPTIMISTIC = "optimistic"
CONF_OPTION = "option"
CONF_OPTIONS = "options"

View file

@ -61,8 +61,10 @@ void Controller::setup_controller(bool include_internal) {
#endif
#ifdef USE_SELECT
for (auto *obj : App.get_selects()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); });
if (include_internal || !obj->is_internal()) {
obj->add_on_state_callback(
[this, obj](const std::string &state, size_t index) { this->on_select_update(obj, state, index); });
}
}
#endif
#ifdef USE_LOCK

View file

@ -71,7 +71,7 @@ class Controller {
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){};
virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){};
#endif
#ifdef USE_LOCK
virtual void on_lock_update(lock::Lock *obj){};

View file

@ -16,6 +16,7 @@ uint32 = global_ns.namespace("uint32_t")
uint64 = global_ns.namespace("uint64_t")
int32 = global_ns.namespace("int32_t")
int64 = global_ns.namespace("int64_t")
size_t = global_ns.namespace("size_t")
const_char_ptr = global_ns.namespace("const char *")
NAN = global_ns.namespace("NAN")
esphome_ns = global_ns # using namespace esphome;

View file

@ -154,8 +154,8 @@ select:
restore_value: true
on_value:
- logger.log:
format: "Select changed to %s"
args: ["x.c_str()"]
format: "Select changed to %s (index %d)"
args: ["x.c_str()", "i"]
set_action:
- logger.log:
format: "Template Select set to %s"
@ -163,11 +163,42 @@ select:
- select.set:
id: template_select_id
option: two
- select.first: template_select_id
- select.last:
id: template_select_id
- select.previous: template_select_id
- select.next:
id: template_select_id
cycle: false
- select.operation:
id: template_select_id
operation: Previous
cycle: false
- select.operation:
id: template_select_id
operation: !lambda "return SELECT_OP_PREVIOUS;"
cycle: !lambda "return true;"
- select.set_index:
id: template_select_id
index: 1
- select.set_index:
id: template_select_id
index: !lambda "return 1 + 1;"
options:
- one
- two
- three
- platform: modbus_controller
name: "Modbus Select Register 1000"
address: 1000
value_type: U_WORD
optionsmap:
"Zero": 0
"One": 1
"Two": 2
"Three": 3
sensor:
- platform: selec_meter
total_active_energy: