mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 22:48:10 +01:00
Add select entities and implement template select (#2067)
Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
parent
69c7cf783e
commit
76991cdcc4
35 changed files with 1053 additions and 0 deletions
|
@ -100,6 +100,7 @@ esphome/components/script/* @esphome/core
|
||||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
||||||
esphome/components/sdp3x/* @Azimath
|
esphome/components/sdp3x/* @Azimath
|
||||||
esphome/components/selec_meter/* @sourabhjaiswal
|
esphome/components/selec_meter/* @sourabhjaiswal
|
||||||
|
esphome/components/select/* @esphome/core
|
||||||
esphome/components/sensor/* @esphome/core
|
esphome/components/sensor/* @esphome/core
|
||||||
esphome/components/sgp40/* @SenexCrenshaw
|
esphome/components/sgp40/* @SenexCrenshaw
|
||||||
esphome/components/sht4x/* @sjtrny
|
esphome/components/sht4x/* @sjtrny
|
||||||
|
|
|
@ -39,6 +39,7 @@ service APIConnection {
|
||||||
rpc camera_image (CameraImageRequest) returns (void) {}
|
rpc camera_image (CameraImageRequest) returns (void) {}
|
||||||
rpc climate_command (ClimateCommandRequest) returns (void) {}
|
rpc climate_command (ClimateCommandRequest) returns (void) {}
|
||||||
rpc number_command (NumberCommandRequest) returns (void) {}
|
rpc number_command (NumberCommandRequest) returns (void) {}
|
||||||
|
rpc select_command (SelectCommandRequest) returns (void) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -867,3 +868,39 @@ message NumberCommandRequest {
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
float state = 2;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -609,6 +609,41 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
#ifdef USE_ESP32_CAMERA
|
||||||
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
|
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
|
||||||
if (!this->state_subscription_)
|
if (!this->state_subscription_)
|
||||||
|
|
|
@ -67,6 +67,11 @@ class APIConnection : public APIServerConnection {
|
||||||
bool send_number_state(number::Number *number, float state);
|
bool send_number_state(number::Number *number, float state);
|
||||||
bool send_number_info(number::Number *number);
|
bool send_number_info(number::Number *number);
|
||||||
void number_command(const NumberCommandRequest &msg) override;
|
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
|
#endif
|
||||||
bool send_log_message(int level, const char *tag, const char *line);
|
bool send_log_message(int level, const char *tag, const char *line);
|
||||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||||
|
|
|
@ -3574,6 +3574,172 @@ void NumberCommandRequest::dump_to(std::string &out) const {
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
out.append("}");
|
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 api
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -849,6 +849,45 @@ class NumberCommandRequest : public ProtoMessage {
|
||||||
protected:
|
protected:
|
||||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
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 api
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -198,6 +198,20 @@ bool APIServerConnectionBase::send_number_state_response(const NumberStateRespon
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
#endif
|
#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) {
|
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||||
switch (msg_type) {
|
switch (msg_type) {
|
||||||
case 1: {
|
case 1: {
|
||||||
|
@ -372,6 +386,15 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||||
msg.decode(msg_data, msg_size);
|
msg.decode(msg_data, msg_size);
|
||||||
ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str());
|
ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str());
|
||||||
this->on_number_command_request(msg);
|
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
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -583,6 +606,19 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest &
|
||||||
this->number_command(msg);
|
this->number_command(msg);
|
||||||
}
|
}
|
||||||
#endif
|
#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 api
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -120,6 +120,15 @@ class APIServerConnectionBase : public ProtoService {
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
virtual void on_number_command_request(const NumberCommandRequest &value){};
|
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
|
#endif
|
||||||
protected:
|
protected:
|
||||||
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
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
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
virtual void number_command(const NumberCommandRequest &msg) = 0;
|
virtual void number_command(const NumberCommandRequest &msg) = 0;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
virtual void select_command(const SelectCommandRequest &msg) = 0;
|
||||||
#endif
|
#endif
|
||||||
protected:
|
protected:
|
||||||
void on_hello_request(const HelloRequest &msg) override;
|
void on_hello_request(const HelloRequest &msg) override;
|
||||||
|
@ -194,6 +206,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace api
|
} // namespace api
|
||||||
|
|
|
@ -206,6 +206,15 @@ void APIServer::on_number_update(number::Number *obj, float state) {
|
||||||
}
|
}
|
||||||
#endif
|
#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; }
|
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
|
@ -62,6 +62,9 @@ class APIServer : public Component, public Controller {
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
void on_number_update(number::Number *obj, float state) override;
|
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
|
#endif
|
||||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||||
|
|
|
@ -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); }
|
bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
|
||||||
|
#endif
|
||||||
|
|
||||||
} // namespace api
|
} // namespace api
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -42,6 +42,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
bool on_number(number::Number *number) override;
|
bool on_number(number::Number *number) override;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
bool on_select(select::Select *select) override;
|
||||||
#endif
|
#endif
|
||||||
bool on_end() override;
|
bool on_end() override;
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,11 @@ bool InitialStateIterator::on_number(number::Number *number) {
|
||||||
return this->client_->send_number_state(number, number->state);
|
return this->client_->send_number_state(number, number->state);
|
||||||
}
|
}
|
||||||
#endif
|
#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)
|
InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
|
||||||
: ComponentIterator(server), client_(client) {}
|
: ComponentIterator(server), client_(client) {}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,9 @@ class InitialStateIterator : public ComponentIterator {
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
bool on_number(number::Number *number) override;
|
bool on_number(number::Number *number) override;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
bool on_select(select::Select *select) override;
|
||||||
#endif
|
#endif
|
||||||
protected:
|
protected:
|
||||||
APIConnection *client_;
|
APIConnection *client_;
|
||||||
|
|
|
@ -182,6 +182,21 @@ void ComponentIterator::advance() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
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
|
#endif
|
||||||
case IteratorState::MAX:
|
case IteratorState::MAX:
|
||||||
if (this->on_end()) {
|
if (this->on_end()) {
|
||||||
|
|
|
@ -50,6 +50,9 @@ class ComponentIterator {
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
virtual bool on_number(number::Number *number) = 0;
|
virtual bool on_number(number::Number *number) = 0;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
virtual bool on_select(select::Select *select) = 0;
|
||||||
#endif
|
#endif
|
||||||
virtual bool on_end();
|
virtual bool on_end();
|
||||||
|
|
||||||
|
@ -87,6 +90,9 @@ class ComponentIterator {
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
NUMBER,
|
NUMBER,
|
||||||
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
SELECT,
|
||||||
#endif
|
#endif
|
||||||
MAX,
|
MAX,
|
||||||
} state_{IteratorState::NONE};
|
} state_{IteratorState::NONE};
|
||||||
|
|
|
@ -92,6 +92,7 @@ MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent)
|
||||||
MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent)
|
MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent)
|
||||||
MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
|
MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
|
||||||
MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
|
MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
|
||||||
|
MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
|
||||||
|
|
||||||
|
|
||||||
def validate_config(value):
|
def validate_config(value):
|
||||||
|
|
58
esphome/components/mqtt/mqtt_select.cpp
Normal file
58
esphome/components/mqtt/mqtt_select.cpp
Normal 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
|
46
esphome/components/mqtt/mqtt_select.h
Normal file
46
esphome/components/mqtt/mqtt_select.h
Normal 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
|
102
esphome/components/select/__init__.py
Normal file
102
esphome/components/select/__init__.py
Normal 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
|
33
esphome/components/select/automation.h
Normal file
33
esphome/components/select/automation.h
Normal 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
|
43
esphome/components/select/select.cpp
Normal file
43
esphome/components/select/select.cpp
Normal 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
|
87
esphome/components/select/select.h
Normal file
87
esphome/components/select/select.h
Normal 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
|
74
esphome/components/template/select/__init__.py
Normal file
74
esphome/components/template/select/__init__.py
Normal 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]
|
||||||
|
)
|
74
esphome/components/template/select/template_select.cpp
Normal file
74
esphome/components/template/select/template_select.cpp
Normal 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
|
37
esphome/components/template/select/template_select.h
Normal file
37
esphome/components/template/select/template_select.h
Normal 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
|
|
@ -129,6 +129,12 @@ void WebServer::setup() {
|
||||||
if (!obj->is_internal())
|
if (!obj->is_internal())
|
||||||
client->send(this->number_json(obj, obj->state).c_str(), "state");
|
client->send(this->number_json(obj, obj->state).c_str(), "state");
|
||||||
#endif
|
#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
|
#ifdef USE_LOGGER
|
||||||
|
@ -211,6 +217,11 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||||
write_row(stream, obj, "number", "");
|
write_row(stream, obj, "number", "");
|
||||||
#endif
|
#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 "
|
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>"
|
"REST API documentation.</p>"
|
||||||
"<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
|
"<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
|
#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) {
|
bool WebServer::canHandle(AsyncWebServerRequest *request) {
|
||||||
if (request->url() == "/")
|
if (request->url() == "/")
|
||||||
return true;
|
return true;
|
||||||
|
@ -683,6 +719,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
if (request->method() == HTTP_GET && match.domain == "select")
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
||||||
|
@ -765,6 +806,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
if (match.domain == "select") {
|
||||||
|
this->handle_select_request(request, match);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WebServer::isRequestHandlerTrivial() { return false; }
|
bool WebServer::isRequestHandlerTrivial() { return false; }
|
||||||
|
|
|
@ -163,6 +163,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
||||||
std::string number_json(number::Number *obj, float value);
|
std::string number_json(number::Number *obj, float value);
|
||||||
#endif
|
#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.
|
/// Override the web handler's canHandle method.
|
||||||
bool canHandle(AsyncWebServerRequest *request) override;
|
bool canHandle(AsyncWebServerRequest *request) override;
|
||||||
/// Override the web handler's handleRequest method.
|
/// Override the web handler's handleRequest method.
|
||||||
|
|
|
@ -278,6 +278,7 @@ CONF_INCLUDES = "includes"
|
||||||
CONF_INDEX = "index"
|
CONF_INDEX = "index"
|
||||||
CONF_INDOOR = "indoor"
|
CONF_INDOOR = "indoor"
|
||||||
CONF_INITIAL_MODE = "initial_mode"
|
CONF_INITIAL_MODE = "initial_mode"
|
||||||
|
CONF_INITIAL_OPTION = "initial_option"
|
||||||
CONF_INITIAL_VALUE = "initial_value"
|
CONF_INITIAL_VALUE = "initial_value"
|
||||||
CONF_INTEGRATION_TIME = "integration_time"
|
CONF_INTEGRATION_TIME = "integration_time"
|
||||||
CONF_INTENSITY = "intensity"
|
CONF_INTENSITY = "intensity"
|
||||||
|
@ -407,6 +408,8 @@ CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt"
|
||||||
CONF_OPEN_DURATION = "open_duration"
|
CONF_OPEN_DURATION = "open_duration"
|
||||||
CONF_OPEN_ENDSTOP = "open_endstop"
|
CONF_OPEN_ENDSTOP = "open_endstop"
|
||||||
CONF_OPTIMISTIC = "optimistic"
|
CONF_OPTIMISTIC = "optimistic"
|
||||||
|
CONF_OPTION = "option"
|
||||||
|
CONF_OPTIONS = "options"
|
||||||
CONF_OR = "or"
|
CONF_OR = "or"
|
||||||
CONF_OSCILLATING = "oscillating"
|
CONF_OSCILLATING = "oscillating"
|
||||||
CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"
|
CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"
|
||||||
|
|
|
@ -35,6 +35,9 @@
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
#include "esphome/components/number/number.h"
|
#include "esphome/components/number/number.h"
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
#include "esphome/components/select/select.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
|
@ -89,6 +92,10 @@ class Application {
|
||||||
void register_number(number::Number *number) { this->numbers_.push_back(number); }
|
void register_number(number::Number *number) { this->numbers_.push_back(number); }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
void register_select(select::Select *select) { this->selects_.push_back(select); }
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Register the component in this Application instance.
|
/// Register the component in this Application instance.
|
||||||
template<class C> C *register_component(C *c) {
|
template<class C> C *register_component(C *c) {
|
||||||
static_assert(std::is_base_of<Component, C>::value, "Only Component subclasses can be registered");
|
static_assert(std::is_base_of<Component, C>::value, "Only Component subclasses can be registered");
|
||||||
|
@ -224,6 +231,15 @@ class Application {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
#endif
|
#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;
|
Scheduler scheduler;
|
||||||
|
|
||||||
|
@ -264,6 +280,9 @@ class Application {
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
std::vector<number::Number *> numbers_{};
|
std::vector<number::Number *> numbers_{};
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
std::vector<select::Select *> selects_{};
|
||||||
|
#endif
|
||||||
|
|
||||||
std::string name_;
|
std::string name_;
|
||||||
std::string compilation_time_;
|
std::string compilation_time_;
|
||||||
|
|
|
@ -59,6 +59,12 @@ void Controller::setup_controller() {
|
||||||
obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); });
|
obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); });
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
} // namespace esphome
|
||||||
|
|
|
@ -28,6 +28,9 @@
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
#include "esphome/components/number/number.h"
|
#include "esphome/components/number/number.h"
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
#include "esphome/components/select/select.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
|
@ -61,6 +64,9 @@ class Controller {
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
virtual void on_number_update(number::Number *obj, float state){};
|
virtual void on_number_update(number::Number *obj, float state){};
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
virtual void on_select_update(select::Select *obj, const std::string &state){};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#define USE_LIGHT
|
#define USE_LIGHT
|
||||||
#define USE_CLIMATE
|
#define USE_CLIMATE
|
||||||
#define USE_NUMBER
|
#define USE_NUMBER
|
||||||
|
#define USE_SELECT
|
||||||
#define USE_MQTT
|
#define USE_MQTT
|
||||||
#define USE_POWER_SUPPLY
|
#define USE_POWER_SUPPLY
|
||||||
#define USE_HOMEASSISTANT_TIME
|
#define USE_HOMEASSISTANT_TIME
|
||||||
|
|
|
@ -563,6 +563,7 @@ def lint_inclusive_language(fname, match):
|
||||||
"esphome/components/output/binary_output.h",
|
"esphome/components/output/binary_output.h",
|
||||||
"esphome/components/output/float_output.h",
|
"esphome/components/output/float_output.h",
|
||||||
"esphome/components/nextion/nextion_base.h",
|
"esphome/components/nextion/nextion_base.h",
|
||||||
|
"esphome/components/select/select.h",
|
||||||
"esphome/components/sensor/sensor.h",
|
"esphome/components/sensor/sensor.h",
|
||||||
"esphome/components/stepper/stepper.h",
|
"esphome/components/stepper/stepper.h",
|
||||||
"esphome/components/switch/switch.h",
|
"esphome/components/switch/switch.h",
|
||||||
|
|
|
@ -82,6 +82,29 @@ number:
|
||||||
min_value: 0
|
min_value: 0
|
||||||
step: 5
|
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:
|
sensor:
|
||||||
- platform: selec_meter
|
- platform: selec_meter
|
||||||
total_active_energy:
|
total_active_energy:
|
||||||
|
|
Loading…
Reference in a new issue