mirror of
https://github.com/esphome/esphome.git
synced 2024-11-27 17:27:59 +01:00
merge dev
This commit is contained in:
commit
e6261e1050
82 changed files with 2999 additions and 27 deletions
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -19,6 +19,8 @@
|
|||
- [ ] ESP32 IDF
|
||||
- [ ] ESP8266
|
||||
- [ ] RP2040
|
||||
- [ ] BK72xx
|
||||
- [ ] RTL87xx
|
||||
|
||||
## Example entry for `config.yaml`:
|
||||
<!--
|
||||
|
|
|
@ -27,7 +27,7 @@ repos:
|
|||
- --branch=release
|
||||
- --branch=beta
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.13.0
|
||||
rev: v3.15.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py39-plus]
|
||||
|
|
|
@ -110,6 +110,7 @@ esphome/components/gp8403/* @jesserockz
|
|||
esphome/components/gpio/* @esphome/core
|
||||
esphome/components/gps/* @coogle
|
||||
esphome/components/graph/* @synco
|
||||
esphome/components/gree/* @orestismers
|
||||
esphome/components/grove_tb6612fng/* @max246
|
||||
esphome/components/growatt_solar/* @leeuwte
|
||||
esphome/components/haier/* @paveldn
|
||||
|
@ -131,6 +132,7 @@ esphome/components/i2s_audio/* @jesserockz
|
|||
esphome/components/i2s_audio/media_player/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
esphome/components/improv_base/* @esphome/core
|
||||
esphome/components/improv_serial/* @esphome/core
|
||||
|
@ -207,6 +209,7 @@ esphome/components/nextion/sensor/* @senexcrenshaw
|
|||
esphome/components/nextion/switch/* @senexcrenshaw
|
||||
esphome/components/nextion/text_sensor/* @senexcrenshaw
|
||||
esphome/components/nfc/* @jesserockz
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/optolink/* @j0ta29
|
||||
esphome/components/ota/* @esphome/core
|
||||
|
@ -301,6 +304,7 @@ esphome/components/tcl112/* @glmnet
|
|||
esphome/components/tee501/* @Stock-M
|
||||
esphome/components/teleinfo/* @0hax
|
||||
esphome/components/template/alarm_control_panel/* @grahambrown11
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @OttoWinter
|
||||
esphome/components/tlc5947/* @rnauber
|
||||
|
@ -346,4 +350,5 @@ esphome/components/xiaomi_mhoc401/* @vevsvevs
|
|||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
||||
esphome/components/xl9535/* @mreditor97
|
||||
esphome/components/xpt2046/* @nielsnl68 @numo68
|
||||
esphome/components/zhlt01/* @cfeenstra1024
|
||||
esphome/components/zio_ultrasonic/* @kahrendt
|
||||
|
|
|
@ -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 text_command (TextCommandRequest) returns (void) {}
|
||||
rpc select_command (SelectCommandRequest) returns (void) {}
|
||||
rpc button_command (ButtonCommandRequest) returns (void) {}
|
||||
rpc lock_command (LockCommandRequest) returns (void) {}
|
||||
|
@ -1536,3 +1537,48 @@ message AlarmControlPanelCommandRequest {
|
|||
AlarmControlPanelStateCommand command = 2;
|
||||
string code = 3;
|
||||
}
|
||||
|
||||
// ===================== TEXT =====================
|
||||
enum TextMode {
|
||||
TEXT_MODE_TEXT = 0;
|
||||
TEXT_MODE_PASSWORD = 1;
|
||||
}
|
||||
message ListEntitiesTextResponse {
|
||||
option (id) = 97;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_TEXT";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string unique_id = 4;
|
||||
string icon = 5;
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
|
||||
uint32 min_length = 8;
|
||||
uint32 max_length = 9;
|
||||
string pattern = 10;
|
||||
TextMode mode = 11;
|
||||
}
|
||||
message TextStateResponse {
|
||||
option (id) = 98;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_TEXT";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
string state = 2;
|
||||
// If the Text does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 3;
|
||||
}
|
||||
message TextCommandRequest {
|
||||
option (id) = 99;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_TEXT";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
string state = 2;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "api_connection.h"
|
||||
#include <cerrno>
|
||||
#include <cinttypes>
|
||||
#include <utility>
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
@ -655,6 +656,44 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
bool APIConnection::send_text_state(text::Text *text, std::string state) {
|
||||
if (!this->state_subscription_)
|
||||
return false;
|
||||
|
||||
TextStateResponse resp{};
|
||||
resp.key = text->get_object_id_hash();
|
||||
resp.state = std::move(state);
|
||||
resp.missing_state = !text->has_state();
|
||||
return this->send_text_state_response(resp);
|
||||
}
|
||||
bool APIConnection::send_text_info(text::Text *text) {
|
||||
ListEntitiesTextResponse msg;
|
||||
msg.key = text->get_object_id_hash();
|
||||
msg.object_id = text->get_object_id();
|
||||
msg.name = text->get_name();
|
||||
msg.icon = text->get_icon();
|
||||
msg.disabled_by_default = text->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(text->get_entity_category());
|
||||
msg.mode = static_cast<enums::TextMode>(text->traits.get_mode());
|
||||
|
||||
msg.min_length = text->traits.get_min_length();
|
||||
msg.max_length = text->traits.get_max_length();
|
||||
msg.pattern = text->traits.get_pattern();
|
||||
|
||||
return this->send_list_entities_text_response(msg);
|
||||
}
|
||||
void APIConnection::text_command(const TextCommandRequest &msg) {
|
||||
text::Text *text = App.get_text_by_key(msg.key);
|
||||
if (text == nullptr)
|
||||
return;
|
||||
|
||||
auto call = text->make_call();
|
||||
call.set_value(msg.state);
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
bool APIConnection::send_select_state(select::Select *select, std::string state) {
|
||||
if (!this->state_subscription_)
|
||||
|
|
|
@ -72,6 +72,11 @@ class APIConnection : public APIServerConnection {
|
|||
bool send_number_info(number::Number *number);
|
||||
void number_command(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool send_text_state(text::Text *text, std::string state);
|
||||
bool send_text_info(text::Text *text);
|
||||
void text_command(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool send_select_state(select::Select *select, std::string state);
|
||||
bool send_select_info(select::Select *select);
|
||||
|
|
|
@ -512,6 +512,18 @@ const char *proto_enum_to_string<enums::AlarmControlPanelStateCommand>(enums::Al
|
|||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
template<> const char *proto_enum_to_string<enums::TextMode>(enums::TextMode value) {
|
||||
switch (value) {
|
||||
case enums::TEXT_MODE_TEXT:
|
||||
return "TEXT_MODE_TEXT";
|
||||
case enums::TEXT_MODE_PASSWORD:
|
||||
return "TEXT_MODE_PASSWORD";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
|
@ -6795,6 +6807,227 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
|
|||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 6: {
|
||||
this->disabled_by_default = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 7: {
|
||||
this->entity_category = value.as_enum<enums::EntityCategory>();
|
||||
return true;
|
||||
}
|
||||
case 8: {
|
||||
this->min_length = value.as_uint32();
|
||||
return true;
|
||||
}
|
||||
case 9: {
|
||||
this->max_length = value.as_uint32();
|
||||
return true;
|
||||
}
|
||||
case 11: {
|
||||
this->mode = value.as_enum<enums::TextMode>();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ListEntitiesTextResponse::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 10: {
|
||||
this->pattern = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ListEntitiesTextResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->object_id);
|
||||
buffer.encode_fixed32(2, this->key);
|
||||
buffer.encode_string(3, this->name);
|
||||
buffer.encode_string(4, this->unique_id);
|
||||
buffer.encode_string(5, this->icon);
|
||||
buffer.encode_bool(6, this->disabled_by_default);
|
||||
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
|
||||
buffer.encode_uint32(8, this->min_length);
|
||||
buffer.encode_uint32(9, this->max_length);
|
||||
buffer.encode_string(10, this->pattern);
|
||||
buffer.encode_enum<enums::TextMode>(11, this->mode);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void ListEntitiesTextResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("ListEntitiesTextResponse {\n");
|
||||
out.append(" object_id: ");
|
||||
out.append("'").append(this->object_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" name: ");
|
||||
out.append("'").append(this->name).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" unique_id: ");
|
||||
out.append("'").append(this->unique_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" icon: ");
|
||||
out.append("'").append(this->icon).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" disabled_by_default: ");
|
||||
out.append(YESNO(this->disabled_by_default));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" entity_category: ");
|
||||
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" min_length: ");
|
||||
sprintf(buffer, "%" PRIu32, this->min_length);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" max_length: ");
|
||||
sprintf(buffer, "%" PRIu32, this->max_length);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" pattern: ");
|
||||
out.append("'").append(this->pattern).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" mode: ");
|
||||
out.append(proto_enum_to_string<enums::TextMode>(this->mode));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool TextStateResponse::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 TextStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->state = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool TextStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void TextStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
buffer.encode_string(2, this->state);
|
||||
buffer.encode_bool(3, this->missing_state);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void TextStateResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("TextStateResponse {\n");
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, 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("}");
|
||||
}
|
||||
#endif
|
||||
bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->state = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void TextCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
buffer.encode_string(2, this->state);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void TextCommandRequest::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("TextCommandRequest {\n");
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" state: ");
|
||||
out.append("'").append(this->state).append("'");
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
|
|
@ -208,6 +208,10 @@ enum AlarmControlPanelStateCommand : uint32_t {
|
|||
ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5,
|
||||
ALARM_CONTROL_PANEL_TRIGGER = 6,
|
||||
};
|
||||
enum TextMode : uint32_t {
|
||||
TEXT_MODE_TEXT = 0,
|
||||
TEXT_MODE_PASSWORD = 1,
|
||||
};
|
||||
|
||||
} // namespace enums
|
||||
|
||||
|
@ -1778,6 +1782,57 @@ class AlarmControlPanelCommandRequest : public ProtoMessage {
|
|||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class ListEntitiesTextResponse : public ProtoMessage {
|
||||
public:
|
||||
std::string object_id{};
|
||||
uint32_t key{0};
|
||||
std::string name{};
|
||||
std::string unique_id{};
|
||||
std::string icon{};
|
||||
bool disabled_by_default{false};
|
||||
enums::EntityCategory entity_category{};
|
||||
uint32_t min_length{0};
|
||||
uint32_t max_length{0};
|
||||
std::string pattern{};
|
||||
enums::TextMode mode{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
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 TextStateResponse : public ProtoMessage {
|
||||
public:
|
||||
uint32_t key{0};
|
||||
std::string state{};
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
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 TextCommandRequest : public ProtoMessage {
|
||||
public:
|
||||
uint32_t key{0};
|
||||
std::string state{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
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
|
||||
|
|
|
@ -495,6 +495,24 @@ bool APIServerConnectionBase::send_alarm_control_panel_state_response(const Alar
|
|||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool APIServerConnectionBase::send_list_entities_text_response(const ListEntitiesTextResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_list_entities_text_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<ListEntitiesTextResponse>(msg, 97);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool APIServerConnectionBase::send_text_state_response(const TextStateResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_text_state_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<TextStateResponse>(msg, 98);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
#endif
|
||||
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||
switch (msg_type) {
|
||||
case 1: {
|
||||
|
@ -913,6 +931,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||
ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_alarm_control_panel_command_request(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case 99: {
|
||||
#ifdef USE_TEXT
|
||||
TextCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_text_command_request(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
@ -1124,6 +1153,19 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest &
|
|||
this->number_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->text_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
|
|
|
@ -248,6 +248,15 @@ class APIServerConnectionBase : public ProtoService {
|
|||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool send_list_entities_text_response(const ListEntitiesTextResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool send_text_state_response(const TextStateResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
virtual void on_text_command_request(const TextCommandRequest &value){};
|
||||
#endif
|
||||
protected:
|
||||
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
|
@ -288,6 +297,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||
#ifdef USE_NUMBER
|
||||
virtual void number_command(const NumberCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
virtual void text_command(const TextCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
virtual void select_command(const SelectCommandRequest &msg) = 0;
|
||||
#endif
|
||||
|
@ -371,6 +383,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||
#ifdef USE_NUMBER
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
|
|
|
@ -254,6 +254,15 @@ void APIServer::on_number_update(number::Number *obj, float state) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
void APIServer::on_text_update(text::Text *obj, const std::string &state) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_text_state(obj, state);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
|
||||
if (obj->is_internal())
|
||||
|
|
|
@ -65,6 +65,9 @@ class APIServer : public Component, public Controller {
|
|||
#ifdef USE_NUMBER
|
||||
void on_number_update(number::Number *obj, float state) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_update(text::Text *obj, const std::string &state) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
|
||||
#endif
|
||||
|
|
|
@ -60,6 +60,10 @@ 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_TEXT
|
||||
bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
|
||||
#endif
|
||||
|
|
|
@ -46,6 +46,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
|||
#ifdef USE_NUMBER
|
||||
bool on_number(number::Number *number) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool on_text(text::Text *text) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool on_select(select::Select *select) override;
|
||||
#endif
|
||||
|
|
|
@ -42,6 +42,9 @@ bool InitialStateIterator::on_number(number::Number *number) {
|
|||
return this->client_->send_number_state(number, number->state);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text, text->state); }
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool InitialStateIterator::on_select(select::Select *select) {
|
||||
return this->client_->send_select_state(select, select->state);
|
||||
|
|
|
@ -43,6 +43,9 @@ class InitialStateIterator : public ComponentIterator {
|
|||
#ifdef USE_NUMBER
|
||||
bool on_number(number::Number *number) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool on_text(text::Text *text) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool on_select(select::Select *select) override;
|
||||
#endif
|
||||
|
|
|
@ -17,6 +17,7 @@ CONF_ON_FRAME = "on_frame"
|
|||
|
||||
|
||||
def validate_id(config):
|
||||
if CONF_CAN_ID in config:
|
||||
can_id = config[CONF_CAN_ID]
|
||||
id_ext = config[CONF_USE_EXTENDED_ID]
|
||||
if not id_ext:
|
||||
|
@ -151,22 +152,18 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
|
|||
if can_id := config.get(CONF_CAN_ID):
|
||||
can_id = await cg.templatable(can_id, args, cg.uint32)
|
||||
cg.add(var.set_can_id(can_id))
|
||||
use_extended_id = await cg.templatable(
|
||||
config[CONF_USE_EXTENDED_ID], args, cg.uint32
|
||||
)
|
||||
cg.add(var.set_use_extended_id(use_extended_id))
|
||||
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
|
||||
|
||||
remote_transmission_request = await cg.templatable(
|
||||
config[CONF_REMOTE_TRANSMISSION_REQUEST], args, bool
|
||||
cg.add(
|
||||
var.set_remote_transmission_request(config[CONF_REMOTE_TRANSMISSION_REQUEST])
|
||||
)
|
||||
cg.add(var.set_remote_transmission_request(remote_transmission_request))
|
||||
|
||||
data = config[CONF_DATA]
|
||||
if isinstance(data, bytes):
|
||||
data = [int(x) for x in data]
|
||||
if cg.is_template(data):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
if isinstance(data, bytes):
|
||||
data = [int(x) for x in data]
|
||||
cg.add(var.set_data_static(data))
|
||||
return var
|
||||
|
|
|
@ -213,6 +213,8 @@ ClimateCall &ClimateCall::set_preset(const std::string &preset) {
|
|||
this->set_preset(CLIMATE_PRESET_SLEEP);
|
||||
} else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
|
||||
this->set_preset(CLIMATE_PRESET_ACTIVITY);
|
||||
} else if (str_equals_case_insensitive(preset, "NONE")) {
|
||||
this->set_preset(CLIMATE_PRESET_NONE);
|
||||
} else {
|
||||
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
||||
this->custom_preset_ = preset;
|
||||
|
|
36
esphome/components/copy/text/__init__.py
Normal file
36
esphome/components/copy/text/__init__.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import text
|
||||
from esphome.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_MODE,
|
||||
CONF_SOURCE_ID,
|
||||
)
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
|
||||
from .. import copy_ns
|
||||
|
||||
CopyText = copy_ns.class_("CopyText", text.Text, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CopyText),
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
|
||||
inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
|
||||
inherit_property_from(CONF_MODE, CONF_SOURCE_ID),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await text.new_text(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
source = await cg.get_variable(config[CONF_SOURCE_ID])
|
||||
cg.add(var.set_source(source))
|
25
esphome/components/copy/text/copy_text.cpp
Normal file
25
esphome/components/copy/text/copy_text.cpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#include "copy_text.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace copy {
|
||||
|
||||
static const char *const TAG = "copy.text";
|
||||
|
||||
void CopyText::setup() {
|
||||
source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
|
||||
|
||||
if (source_->has_state())
|
||||
this->publish_state(source_->state);
|
||||
}
|
||||
|
||||
void CopyText::dump_config() { LOG_TEXT("", "Copy Text", this); }
|
||||
|
||||
void CopyText::control(const std::string &value) {
|
||||
auto call2 = source_->make_call();
|
||||
call2.set_value(value);
|
||||
call2.perform();
|
||||
}
|
||||
|
||||
} // namespace copy
|
||||
} // namespace esphome
|
23
esphome/components/copy/text/copy_text.h
Normal file
23
esphome/components/copy/text/copy_text.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/text/text.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace copy {
|
||||
|
||||
class CopyText : public text::Text, public Component {
|
||||
public:
|
||||
void set_source(text::Text *source) { source_ = source; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
|
||||
text::Text *source_;
|
||||
};
|
||||
|
||||
} // namespace copy
|
||||
} // namespace esphome
|
0
esphome/components/gree/__init__.py
Normal file
0
esphome/components/gree/__init__.py
Normal file
33
esphome/components/gree/climate.py
Normal file
33
esphome/components/gree/climate.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate_ir
|
||||
from esphome.const import CONF_ID, CONF_MODEL
|
||||
|
||||
CODEOWNERS = ["@orestismers"]
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
|
||||
gree_ns = cg.esphome_ns.namespace("gree")
|
||||
GreeClimate = gree_ns.class_("GreeClimate", climate_ir.ClimateIR)
|
||||
|
||||
Model = gree_ns.enum("Model")
|
||||
MODELS = {
|
||||
"generic": Model.GREE_GENERIC,
|
||||
"yan": Model.GREE_YAN,
|
||||
"yaa": Model.GREE_YAA,
|
||||
"yac": Model.GREE_YAC,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(GreeClimate),
|
||||
cv.Required(CONF_MODEL): cv.enum(MODELS),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
|
||||
await climate_ir.register_climate_ir(var, config)
|
157
esphome/components/gree/gree.cpp
Normal file
157
esphome/components/gree/gree.cpp
Normal file
|
@ -0,0 +1,157 @@
|
|||
#include "gree.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace gree {
|
||||
|
||||
static const char *const TAG = "gree.climate";
|
||||
|
||||
void GreeClimate::set_model(Model model) { this->model_ = model; }
|
||||
|
||||
void GreeClimate::transmit_state() {
|
||||
uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
|
||||
|
||||
remote_state[0] = this->fan_speed_() | this->operation_mode_();
|
||||
remote_state[1] = this->temperature_();
|
||||
|
||||
if (this->model_ == GREE_YAN) {
|
||||
remote_state[2] = 0x60;
|
||||
remote_state[3] = 0x50;
|
||||
remote_state[4] = this->vertical_swing_();
|
||||
}
|
||||
|
||||
if (this->model_ == GREE_YAC) {
|
||||
remote_state[4] |= (this->horizontal_swing_() << 4);
|
||||
}
|
||||
|
||||
if (this->model_ == GREE_YAA || this->model_ == GREE_YAC) {
|
||||
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO,LIGHT,HEALTH,X-FAN
|
||||
remote_state[3] = 0x50; // bits 4..7 always 0101
|
||||
remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010
|
||||
|
||||
if (this->vertical_swing_() == GREE_VDIR_SWING) {
|
||||
remote_state[0] |= (1 << 6); // Enable swing by setting bit 6
|
||||
} else if (this->vertical_swing_() != GREE_VDIR_AUTO) {
|
||||
remote_state[5] = this->vertical_swing_();
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the checksum
|
||||
if (this->model_ == GREE_YAN) {
|
||||
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
|
||||
} else {
|
||||
remote_state[7] =
|
||||
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
|
||||
((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) &
|
||||
0x0F)
|
||||
<< 4) |
|
||||
(remote_state[7] & 0x0F);
|
||||
}
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
data->set_carrier_frequency(GREE_IR_FREQUENCY);
|
||||
|
||||
data->mark(GREE_HEADER_MARK);
|
||||
data->space(GREE_HEADER_SPACE);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(GREE_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? GREE_ONE_SPACE : GREE_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(GREE_ZERO_SPACE);
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(GREE_ONE_SPACE);
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(GREE_ZERO_SPACE);
|
||||
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(GREE_MESSAGE_SPACE);
|
||||
|
||||
for (int i = 4; i < 8; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(GREE_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? GREE_ONE_SPACE : GREE_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
uint8_t GreeClimate::operation_mode_() {
|
||||
uint8_t operating_mode = GREE_MODE_ON;
|
||||
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
operating_mode |= GREE_MODE_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
operating_mode |= GREE_MODE_DRY;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
operating_mode |= GREE_MODE_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
operating_mode |= GREE_MODE_AUTO;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
operating_mode |= GREE_MODE_FAN;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
operating_mode = GREE_MODE_OFF;
|
||||
break;
|
||||
}
|
||||
|
||||
return operating_mode;
|
||||
}
|
||||
|
||||
uint8_t GreeClimate::fan_speed_() {
|
||||
switch (this->fan_mode.value()) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
return GREE_FAN_1;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
return GREE_FAN_2;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
return GREE_FAN_3;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
return GREE_FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t GreeClimate::horizontal_swing_() {
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
return GREE_HDIR_SWING;
|
||||
default:
|
||||
return GREE_HDIR_MANUAL;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t GreeClimate::vertical_swing_() {
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
return GREE_VDIR_SWING;
|
||||
default:
|
||||
return GREE_VDIR_MANUAL;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t GreeClimate::temperature_() {
|
||||
return (uint8_t) roundf(clamp<float>(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX));
|
||||
}
|
||||
|
||||
} // namespace gree
|
||||
} // namespace esphome
|
97
esphome/components/gree/gree.h
Normal file
97
esphome/components/gree/gree.h
Normal file
|
@ -0,0 +1,97 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace gree {
|
||||
|
||||
// Values for GREE IR Controllers
|
||||
// Temperature
|
||||
const uint8_t GREE_TEMP_MIN = 16; // Celsius
|
||||
const uint8_t GREE_TEMP_MAX = 30; // Celsius
|
||||
|
||||
// Modes
|
||||
const uint8_t GREE_MODE_AUTO = 0x00;
|
||||
const uint8_t GREE_MODE_COOL = 0x01;
|
||||
const uint8_t GREE_MODE_HEAT = 0x04;
|
||||
const uint8_t GREE_MODE_DRY = 0x02;
|
||||
const uint8_t GREE_MODE_FAN = 0x03;
|
||||
|
||||
const uint8_t GREE_MODE_OFF = 0x00;
|
||||
const uint8_t GREE_MODE_ON = 0x08;
|
||||
|
||||
// Fan Speed
|
||||
const uint8_t GREE_FAN_AUTO = 0x00;
|
||||
const uint8_t GREE_FAN_1 = 0x10;
|
||||
const uint8_t GREE_FAN_2 = 0x20;
|
||||
const uint8_t GREE_FAN_3 = 0x30;
|
||||
const uint8_t GREE_FAN_TURBO = 0x80;
|
||||
|
||||
// IR Transmission
|
||||
const uint32_t GREE_IR_FREQUENCY = 38000;
|
||||
const uint32_t GREE_HEADER_MARK = 9000;
|
||||
const uint32_t GREE_HEADER_SPACE = 4000;
|
||||
const uint32_t GREE_BIT_MARK = 620;
|
||||
const uint32_t GREE_ONE_SPACE = 1600;
|
||||
const uint32_t GREE_ZERO_SPACE = 540;
|
||||
const uint32_t GREE_MESSAGE_SPACE = 19000;
|
||||
|
||||
// Timing specific for YAC features (I-Feel mode)
|
||||
const uint32_t GREE_YAC_HEADER_MARK = 6000;
|
||||
const uint32_t GREE_YAC_HEADER_SPACE = 3000;
|
||||
const uint32_t GREE_YAC_BIT_MARK = 650;
|
||||
|
||||
// State Frame size
|
||||
const uint8_t GREE_STATE_FRAME_SIZE = 8;
|
||||
|
||||
// Only available on YAN
|
||||
// Vertical air directions. Note that these cannot be set on all heat pumps
|
||||
const uint8_t GREE_VDIR_AUTO = 0x00;
|
||||
const uint8_t GREE_VDIR_MANUAL = 0x00;
|
||||
const uint8_t GREE_VDIR_SWING = 0x01;
|
||||
const uint8_t GREE_VDIR_UP = 0x02;
|
||||
const uint8_t GREE_VDIR_MUP = 0x03;
|
||||
const uint8_t GREE_VDIR_MIDDLE = 0x04;
|
||||
const uint8_t GREE_VDIR_MDOWN = 0x05;
|
||||
const uint8_t GREE_VDIR_DOWN = 0x06;
|
||||
|
||||
// Only available on YAC
|
||||
// Horizontal air directions. Note that these cannot be set on all heat pumps
|
||||
const uint8_t GREE_HDIR_AUTO = 0x00;
|
||||
const uint8_t GREE_HDIR_MANUAL = 0x00;
|
||||
const uint8_t GREE_HDIR_SWING = 0x01;
|
||||
const uint8_t GREE_HDIR_LEFT = 0x02;
|
||||
const uint8_t GREE_HDIR_MLEFT = 0x03;
|
||||
const uint8_t GREE_HDIR_MIDDLE = 0x04;
|
||||
const uint8_t GREE_HDIR_MRIGHT = 0x05;
|
||||
const uint8_t GREE_HDIR_RIGHT = 0x06;
|
||||
|
||||
// Model codes
|
||||
enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC };
|
||||
|
||||
class GreeClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
GreeClimate()
|
||||
: climate_ir::ClimateIR(GREE_TEMP_MIN, GREE_TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
|
||||
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
|
||||
void set_model(Model model);
|
||||
|
||||
protected:
|
||||
// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
|
||||
uint8_t operation_mode_();
|
||||
uint8_t fan_speed_();
|
||||
uint8_t horizontal_swing_();
|
||||
uint8_t vertical_swing_();
|
||||
uint8_t temperature_();
|
||||
|
||||
Model model_{};
|
||||
};
|
||||
|
||||
} // namespace gree
|
||||
} // namespace esphome
|
0
esphome/components/iaqcore/__init__.py
Normal file
0
esphome/components/iaqcore/__init__.py
Normal file
99
esphome/components/iaqcore/iaqcore.cpp
Normal file
99
esphome/components/iaqcore/iaqcore.cpp
Normal file
|
@ -0,0 +1,99 @@
|
|||
#include "iaqcore.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace iaqcore {
|
||||
|
||||
static const char *const TAG = "iaqcore";
|
||||
|
||||
enum IAQCoreErrorCode : uint8_t { ERROR_OK = 0, ERROR_RUNIN = 0x10, ERROR_BUSY = 0x01, ERROR_ERROR = 0x80 };
|
||||
|
||||
struct SensorData {
|
||||
uint16_t co2;
|
||||
IAQCoreErrorCode status;
|
||||
int32_t resistance;
|
||||
uint16_t tvoc;
|
||||
|
||||
SensorData(const uint8_t *buffer) {
|
||||
this->co2 = encode_uint16(buffer[0], buffer[1]);
|
||||
this->status = static_cast<IAQCoreErrorCode>(buffer[2]);
|
||||
this->resistance = encode_uint32(buffer[3], buffer[4], buffer[5], buffer[6]);
|
||||
this->tvoc = encode_uint16(buffer[7], buffer[8]);
|
||||
}
|
||||
};
|
||||
|
||||
void IAQCore::setup() {
|
||||
if (this->write(nullptr, 0) != i2c::ERROR_OK) {
|
||||
ESP_LOGD(TAG, "Communication failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void IAQCore::update() {
|
||||
uint8_t buffer[sizeof(SensorData)];
|
||||
|
||||
if (this->read_register(0xB5, buffer, sizeof(buffer), false) != i2c::ERROR_OK) {
|
||||
ESP_LOGD(TAG, "Read failed");
|
||||
this->status_set_warning();
|
||||
this->publish_nans_();
|
||||
return;
|
||||
}
|
||||
|
||||
SensorData data(buffer);
|
||||
|
||||
switch (data.status) {
|
||||
case ERROR_OK:
|
||||
ESP_LOGD(TAG, "OK");
|
||||
break;
|
||||
case ERROR_RUNIN:
|
||||
ESP_LOGI(TAG, "Warming up");
|
||||
break;
|
||||
case ERROR_BUSY:
|
||||
ESP_LOGI(TAG, "Busy");
|
||||
break;
|
||||
case ERROR_ERROR:
|
||||
ESP_LOGE(TAG, "Error");
|
||||
break;
|
||||
}
|
||||
|
||||
if (data.status != ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
this->publish_nans_();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->co2_ != nullptr) {
|
||||
this->co2_->publish_state(data.co2);
|
||||
}
|
||||
if (this->tvoc_ != nullptr) {
|
||||
this->tvoc_->publish_state(data.tvoc);
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void IAQCore::publish_nans_() {
|
||||
if (this->co2_ != nullptr) {
|
||||
this->co2_->publish_state(NAN);
|
||||
}
|
||||
if (this->tvoc_ != nullptr) {
|
||||
this->tvoc_->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
|
||||
void IAQCore::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AMS iAQ Core:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with AMS iAQ Core failed!");
|
||||
}
|
||||
LOG_SENSOR(" ", "CO2", this->co2_);
|
||||
LOG_SENSOR(" ", "TVOC", this->tvoc_);
|
||||
}
|
||||
|
||||
} // namespace iaqcore
|
||||
} // namespace esphome
|
29
esphome/components/iaqcore/iaqcore.h
Normal file
29
esphome/components/iaqcore/iaqcore.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace iaqcore {
|
||||
|
||||
class IAQCore : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_co2(sensor::Sensor *co2) { co2_ = co2; }
|
||||
void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *co2_{nullptr};
|
||||
sensor::Sensor *tvoc_{nullptr};
|
||||
|
||||
void publish_nans_();
|
||||
};
|
||||
|
||||
} // namespace iaqcore
|
||||
} // namespace esphome
|
57
esphome/components/iaqcore/sensor.py
Normal file
57
esphome/components/iaqcore/sensor.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_CO2,
|
||||
CONF_ID,
|
||||
CONF_TVOC,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
CODEOWNERS = ["@yozik04"]
|
||||
|
||||
|
||||
iaqcore_ns = cg.esphome_ns.namespace("iaqcore")
|
||||
iAQCore = iaqcore_ns.class_("IAQCore", cg.PollingComponent, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(iAQCore),
|
||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TVOC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x5A))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if co2_config := config.get(CONF_CO2):
|
||||
sens = await sensor.new_sensor(co2_config)
|
||||
cg.add(var.set_co2(sens))
|
||||
|
||||
if tvoc_config := config.get(CONF_TVOC):
|
||||
sens = await sensor.new_sensor(tvoc_config)
|
||||
cg.add(var.set_tvoc(sens))
|
||||
|
||||
await i2c.register_i2c_device(var, config)
|
|
@ -111,6 +111,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)
|
||||
MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent)
|
||||
MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
|
||||
MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)
|
||||
MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent)
|
||||
|
|
63
esphome/components/mqtt/mqtt_text.cpp
Normal file
63
esphome/components/mqtt/mqtt_text.cpp
Normal file
|
@ -0,0 +1,63 @@
|
|||
#include "mqtt_text.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
#ifdef USE_MQTT
|
||||
#ifdef USE_TEXT
|
||||
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt.text";
|
||||
|
||||
using namespace esphome::text;
|
||||
|
||||
MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {}
|
||||
|
||||
void MQTTTextComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
|
||||
auto call = this->text_->make_call();
|
||||
call.set_value(state);
|
||||
call.perform();
|
||||
});
|
||||
|
||||
this->text_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); });
|
||||
}
|
||||
|
||||
void MQTTTextComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
|
||||
std::string MQTTTextComponent::component_type() const { return "text"; }
|
||||
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
||||
|
||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
switch (this->text_->traits.get_mode()) {
|
||||
case TEXT_MODE_TEXT:
|
||||
root[MQTT_MODE] = "text";
|
||||
break;
|
||||
case TEXT_MODE_PASSWORD:
|
||||
root[MQTT_MODE] = "password";
|
||||
break;
|
||||
}
|
||||
|
||||
config.command_topic = true;
|
||||
}
|
||||
bool MQTTTextComponent::send_initial_state() {
|
||||
if (this->text_->has_state()) {
|
||||
return this->publish_state(this->text_->state);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
bool MQTTTextComponent::publish_state(const std::string &value) {
|
||||
return this->publish(this->get_state_topic_(), value);
|
||||
}
|
||||
|
||||
} // namespace mqtt
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
#endif // USE_MQTT
|
46
esphome/components/mqtt/mqtt_text.h
Normal file
46
esphome/components/mqtt/mqtt_text.h
Normal file
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_MQTT
|
||||
#ifdef USE_TEXT
|
||||
|
||||
#include "esphome/components/text/text.h"
|
||||
#include "mqtt_component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
|
||||
class MQTTTextComponent : public mqtt::MQTTComponent {
|
||||
public:
|
||||
/** Construct this MQTTTextComponent instance with the provided friendly_name and text
|
||||
*
|
||||
* @param text The text input.
|
||||
*/
|
||||
explicit MQTTTextComponent(text::Text *text);
|
||||
|
||||
// ========== 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 publish_state(const std::string &value);
|
||||
|
||||
protected:
|
||||
/// Override for MQTTComponent, returns "text".
|
||||
std::string component_type() const override;
|
||||
const EntityBase *get_entity() const override;
|
||||
|
||||
text::Text *text_;
|
||||
};
|
||||
|
||||
} // namespace mqtt
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
#endif // USE_MQTT
|
|
@ -36,7 +36,7 @@ struct IPAddress {
|
|||
IPAddress(const std::string &in_address) { ipaddr_aton(in_address.c_str(), &ip_addr_); }
|
||||
IPAddress(ip4_addr_t *other_ip) {
|
||||
memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t));
|
||||
#if USE_ESP32
|
||||
#if USE_ESP32 && LWIP_IPV6
|
||||
ip_addr_.type = IPADDR_TYPE_V4;
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -623,6 +623,13 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
|||
* @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode.
|
||||
*/
|
||||
void sleep(bool sleep);
|
||||
/**
|
||||
* Sets Nextion Protocol Reparse mode between active or passive
|
||||
* @param True or false.
|
||||
* active_mode=true to enter active protocol reparse mode
|
||||
* active_mode=false to enter passive protocol reparse mode.
|
||||
*/
|
||||
void set_protocol_reparse_mode(bool active_mode);
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
|
|
|
@ -32,6 +32,25 @@ void Nextion::sleep(bool sleep) {
|
|||
}
|
||||
// End sleep safe commands
|
||||
|
||||
// Protocol reparse mode
|
||||
void Nextion::set_protocol_reparse_mode(bool active_mode) {
|
||||
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
|
||||
if (active_mode) { // Sets active protocol reparse mode
|
||||
this->write_str(
|
||||
"recmod=1"); // send_command_ cannot be used as Nextion might not be setup if incorrect reparse mode
|
||||
this->write_array(to_send, sizeof(to_send));
|
||||
} else { // Sets passive protocol reparse mode
|
||||
this->write_str("DRAKJHSUYDGBNCJHGJKSHBDN"); // To exit active reparse mode this sequence must be sent
|
||||
this->write_array(to_send, sizeof(to_send));
|
||||
this->write_str("recmod=0"); // Sending recmode=0 twice is recommended
|
||||
this->write_array(to_send, sizeof(to_send));
|
||||
this->write_str("recmod=0");
|
||||
this->write_array(to_send, sizeof(to_send));
|
||||
}
|
||||
this->write_str("connect");
|
||||
this->write_array(to_send, sizeof(to_send));
|
||||
}
|
||||
|
||||
// Set Colors
|
||||
void Nextion::set_component_background_color(const char *component, uint32_t color) {
|
||||
this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, color);
|
||||
|
|
1
esphome/components/noblex/__init__.py
Normal file
1
esphome/components/noblex/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
CODEOWNERS = ["@AGalfra"]
|
20
esphome/components/noblex/climate.py
Normal file
20
esphome/components/noblex/climate.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate_ir
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
|
||||
noblex_ns = cg.esphome_ns.namespace("noblex")
|
||||
NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(NoblexClimate),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await climate_ir.register_climate_ir(var, config)
|
309
esphome/components/noblex/noblex.cpp
Normal file
309
esphome/components/noblex/noblex.cpp
Normal file
|
@ -0,0 +1,309 @@
|
|||
#include "noblex.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace noblex {
|
||||
|
||||
static const char *const TAG = "noblex.climate";
|
||||
|
||||
const uint16_t NOBLEX_HEADER_MARK = 9000;
|
||||
const uint16_t NOBLEX_HEADER_SPACE = 4500;
|
||||
const uint16_t NOBLEX_BIT_MARK = 660;
|
||||
const uint16_t NOBLEX_ONE_SPACE = 1640;
|
||||
const uint16_t NOBLEX_ZERO_SPACE = 520;
|
||||
const uint32_t NOBLEX_GAP = 20000;
|
||||
const uint8_t NOBLEX_POWER = 0x10;
|
||||
|
||||
using IRNoblexMode = enum IRNoblexMode {
|
||||
IR_NOBLEX_MODE_AUTO = 0b000,
|
||||
IR_NOBLEX_MODE_COOL = 0b100,
|
||||
IR_NOBLEX_MODE_DRY = 0b010,
|
||||
IR_NOBLEX_MODE_FAN = 0b110,
|
||||
IR_NOBLEX_MODE_HEAT = 0b001,
|
||||
};
|
||||
|
||||
using IRNoblexFan = enum IRNoblexFan {
|
||||
IR_NOBLEX_FAN_AUTO = 0b00,
|
||||
IR_NOBLEX_FAN_LOW = 0b10,
|
||||
IR_NOBLEX_FAN_MEDIUM = 0b01,
|
||||
IR_NOBLEX_FAN_HIGH = 0b11,
|
||||
};
|
||||
|
||||
// Transmit via IR the state of this climate controller.
|
||||
void NoblexClimate::transmit_state() {
|
||||
uint8_t remote_state[8] = {0x80, 0x10, 0x00, 0x0A, 0x50, 0x00, 0x20, 0x00}; // OFF, COOL, 24C, FAN_AUTO
|
||||
|
||||
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
if (powered_on) {
|
||||
remote_state[0] |= 0x10; // power bit
|
||||
remote_state[2] = 0x02;
|
||||
}
|
||||
if (powered_on != this->powered_on_assumed)
|
||||
this->powered_on_assumed = powered_on;
|
||||
|
||||
auto temp = (uint8_t) roundf(clamp<float>(this->target_temperature, NOBLEX_TEMP_MIN, NOBLEX_TEMP_MAX));
|
||||
remote_state[1] = reverse_bits(uint8_t((temp - NOBLEX_TEMP_MIN) & 0x0F));
|
||||
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_AUTO << 5);
|
||||
remote_state[1] = 0x90; // NOBLEX_TEMP_MAP 25C
|
||||
break;
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_COOL << 5);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_DRY << 5);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_FAN << 5);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_HEAT << 5);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
powered_on = false;
|
||||
this->powered_on_assumed = powered_on;
|
||||
remote_state[0] &= 0xEF;
|
||||
remote_state[2] = 0x00;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (this->fan_mode.value()) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_LOW << 2);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_MEDIUM << 2);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_HIGH << 2);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_AUTO << 2);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
remote_state[0] |= 0x02;
|
||||
remote_state[4] = 0x58;
|
||||
break;
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
default:
|
||||
remote_state[0] &= 0xFD;
|
||||
remote_state[4] = 0x50;
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t crc = 0;
|
||||
for (uint8_t i : remote_state) {
|
||||
crc += reverse_bits(i);
|
||||
}
|
||||
crc = reverse_bits(uint8_t(crc & 0x0F)) >> 4;
|
||||
|
||||
ESP_LOGD(TAG, "Sending noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1],
|
||||
remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]);
|
||||
|
||||
ESP_LOGV(TAG, "CRC: %01X", crc);
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
data->set_carrier_frequency(38000);
|
||||
|
||||
// Header
|
||||
data->mark(NOBLEX_HEADER_MARK);
|
||||
data->space(NOBLEX_HEADER_SPACE);
|
||||
// Data (sent remote_state from the MSB to the LSB)
|
||||
for (uint8_t i : remote_state) {
|
||||
for (int8_t j = 7; j >= 0; j--) {
|
||||
if ((i == 4) & (j == 4)) {
|
||||
// Header intermediate
|
||||
data->mark(NOBLEX_BIT_MARK);
|
||||
data->space(NOBLEX_GAP); // gap en bit 36
|
||||
} else {
|
||||
data->mark(NOBLEX_BIT_MARK);
|
||||
bool bit = i & (1 << j);
|
||||
data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// send crc
|
||||
for (int8_t i = 3; i >= 0; i--) {
|
||||
data->mark(NOBLEX_BIT_MARK);
|
||||
bool bit = crc & (1 << i);
|
||||
data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE);
|
||||
}
|
||||
// Footer
|
||||
data->mark(NOBLEX_BIT_MARK);
|
||||
|
||||
transmit.perform();
|
||||
} // end transmit_state()
|
||||
|
||||
// Handle received IR Buffer
|
||||
bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t remote_state[8] = {0};
|
||||
uint8_t crc = 0, crc_calculated = 0;
|
||||
|
||||
if (!receiving_) {
|
||||
// Validate header
|
||||
if (data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) {
|
||||
ESP_LOGV(TAG, "Header");
|
||||
receiving_ = true;
|
||||
// Read first 36 bits
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// Read bit
|
||||
for (int j = 7; j >= 0; j--) {
|
||||
if ((i == 4) & (j == 4)) {
|
||||
remote_state[i] |= 1 << j;
|
||||
// Header intermediate
|
||||
ESP_LOGVV(TAG, "GAP");
|
||||
return false;
|
||||
} else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
|
||||
remote_state[i] |= 1 << j;
|
||||
} else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
|
||||
ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]);
|
||||
}
|
||||
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Header fail");
|
||||
receiving_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Read the remaining 28 bits
|
||||
for (int i = 4; i < 8; i++) {
|
||||
// Read bit
|
||||
for (int j = 7; j >= 0; j--) {
|
||||
if ((i == 4) & (j >= 4)) {
|
||||
// nothing
|
||||
} else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
|
||||
remote_state[i] |= 1 << j;
|
||||
} else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
|
||||
ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]);
|
||||
}
|
||||
|
||||
// Read crc
|
||||
for (int i = 3; i >= 0; i--) {
|
||||
if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
|
||||
crc |= 1 << i;
|
||||
} else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
|
||||
ESP_LOGVV(TAG, "Bit %d CRC fail", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "CRC %02X", crc);
|
||||
|
||||
// Validate footer
|
||||
if (!data.expect_mark(NOBLEX_BIT_MARK)) {
|
||||
ESP_LOGV(TAG, "Footer fail");
|
||||
return false;
|
||||
}
|
||||
receiving_ = false;
|
||||
}
|
||||
|
||||
for (uint8_t i : remote_state)
|
||||
crc_calculated += reverse_bits(i);
|
||||
crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4;
|
||||
ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated);
|
||||
|
||||
if (crc != crc_calculated) {
|
||||
ESP_LOGV(TAG, "CRC fail");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1],
|
||||
remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]);
|
||||
|
||||
auto powered_on = false;
|
||||
if ((remote_state[0] & NOBLEX_POWER) == NOBLEX_POWER) {
|
||||
powered_on = true;
|
||||
this->powered_on_assumed = powered_on;
|
||||
} else {
|
||||
powered_on = false;
|
||||
this->powered_on_assumed = powered_on;
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
// powr on/off button
|
||||
ESP_LOGV(TAG, "Power: %01X", powered_on);
|
||||
|
||||
// Set received mode
|
||||
if (powered_on_assumed) {
|
||||
auto mode = (remote_state[0] & 0xE0) >> 5;
|
||||
ESP_LOGV(TAG, "Mode: %02X", mode);
|
||||
switch (mode) {
|
||||
case IRNoblexMode::IR_NOBLEX_MODE_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
case IRNoblexMode::IR_NOBLEX_MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
case IRNoblexMode::IR_NOBLEX_MODE_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case IRNoblexMode::IR_NOBLEX_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
case IRNoblexMode::IR_NOBLEX_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set received temp
|
||||
uint8_t temp = remote_state[1];
|
||||
ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
|
||||
|
||||
temp = 0x0F & reverse_bits(temp);
|
||||
temp += NOBLEX_TEMP_MIN;
|
||||
ESP_LOGV(TAG, "Temperature Climate: %u", temp);
|
||||
this->target_temperature = temp;
|
||||
|
||||
// Set received fan speed
|
||||
auto fan = (remote_state[0] & 0x0C) >> 2;
|
||||
ESP_LOGV(TAG, "Fan: %02X", fan);
|
||||
switch (fan) {
|
||||
case IRNoblexFan::IR_NOBLEX_FAN_HIGH:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
case IRNoblexFan::IR_NOBLEX_FAN_MEDIUM:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
case IRNoblexFan::IR_NOBLEX_FAN_LOW:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case IRNoblexFan::IR_NOBLEX_FAN_AUTO:
|
||||
default:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
|
||||
// Set received swing status
|
||||
if (remote_state[0] & 0x02) {
|
||||
ESP_LOGV(TAG, "Swing vertical");
|
||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Swing OFF");
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
|
||||
for (uint8_t &i : remote_state)
|
||||
i = 0;
|
||||
this->publish_state();
|
||||
return true;
|
||||
} // end on_receive()
|
||||
|
||||
} // namespace noblex
|
||||
} // namespace esphome
|
47
esphome/components/noblex/noblex.h
Normal file
47
esphome/components/noblex/noblex.h
Normal file
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace noblex {
|
||||
|
||||
// Temperature
|
||||
const uint8_t NOBLEX_TEMP_MIN = 16; // Celsius
|
||||
const uint8_t NOBLEX_TEMP_MAX = 30; // Celsius
|
||||
|
||||
class NoblexClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
NoblexClimate()
|
||||
: climate_ir::ClimateIR(NOBLEX_TEMP_MIN, NOBLEX_TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}
|
||||
|
||||
void setup() override {
|
||||
climate_ir::ClimateIR::setup();
|
||||
this->powered_on_assumed = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
|
||||
// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override {
|
||||
send_swing_cmd_ = call.get_swing_mode().has_value();
|
||||
// swing resets after unit powered off
|
||||
if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF)
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
climate_ir::ClimateIR::control(call);
|
||||
}
|
||||
|
||||
// used to track when to send the power toggle command.
|
||||
bool powered_on_assumed;
|
||||
|
||||
protected:
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
/// Handle received IR Buffer.
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
bool send_swing_cmd_{false};
|
||||
bool receiving_ = false;
|
||||
};
|
||||
|
||||
} // namespace noblex
|
||||
} // namespace esphome
|
|
@ -19,6 +19,7 @@ from esphome.const import (
|
|||
ENTITY_CATEGORY_CONFIG,
|
||||
UNIT_MINUTE,
|
||||
UNIT_SECOND,
|
||||
CONF_SET_ACTION,
|
||||
)
|
||||
|
||||
AUTO_LOAD = ["number", "switch"]
|
||||
|
@ -46,7 +47,6 @@ CONF_QUEUE_ENABLE_SWITCH = "queue_enable_switch"
|
|||
CONF_REPEAT_NUMBER = "repeat_number"
|
||||
CONF_REVERSE_SWITCH = "reverse_switch"
|
||||
CONF_RUN_DURATION_NUMBER = "run_duration_number"
|
||||
CONF_SET_ACTION = "set_action"
|
||||
CONF_STANDBY_SWITCH = "standby_switch"
|
||||
CONF_VALVE_NUMBER = "valve_number"
|
||||
CONF_VALVE_OPEN_DELAY = "valve_open_delay"
|
||||
|
|
|
@ -11,6 +11,7 @@ from esphome.const import (
|
|||
CONF_OPTIMISTIC,
|
||||
CONF_RESTORE_VALUE,
|
||||
CONF_STEP,
|
||||
CONF_SET_ACTION,
|
||||
)
|
||||
from .. import template_ns
|
||||
|
||||
|
@ -18,8 +19,6 @@ TemplateNumber = template_ns.class_(
|
|||
"TemplateNumber", number.Number, cg.PollingComponent
|
||||
)
|
||||
|
||||
CONF_SET_ACTION = "set_action"
|
||||
|
||||
|
||||
def validate_min_max(config):
|
||||
if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]:
|
||||
|
|
|
@ -9,6 +9,7 @@ from esphome.const import (
|
|||
CONF_OPTIONS,
|
||||
CONF_OPTIMISTIC,
|
||||
CONF_RESTORE_VALUE,
|
||||
CONF_SET_ACTION,
|
||||
)
|
||||
from .. import template_ns
|
||||
|
||||
|
@ -16,8 +17,6 @@ TemplateSelect = template_ns.class_(
|
|||
"TemplateSelect", select.Select, cg.PollingComponent
|
||||
)
|
||||
|
||||
CONF_SET_ACTION = "set_action"
|
||||
|
||||
|
||||
def validate(config):
|
||||
if CONF_LAMBDA in config:
|
||||
|
|
92
esphome/components/template/text/__init__.py
Normal file
92
esphome/components/template/text/__init__.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import text
|
||||
from esphome.const import (
|
||||
CONF_INITIAL_VALUE,
|
||||
CONF_LAMBDA,
|
||||
CONF_OPTIMISTIC,
|
||||
CONF_RESTORE_VALUE,
|
||||
CONF_MAX_LENGTH,
|
||||
CONF_MIN_LENGTH,
|
||||
CONF_PATTERN,
|
||||
CONF_SET_ACTION,
|
||||
)
|
||||
from .. import template_ns
|
||||
|
||||
TemplateText = template_ns.class_("TemplateText", text.Text, cg.PollingComponent)
|
||||
|
||||
TextSaverBase = template_ns.class_("TemplateTextSaverBase")
|
||||
TextSaverTemplate = template_ns.class_("TextSaver", TextSaverBase)
|
||||
|
||||
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
|
||||
|
||||
|
||||
def validate(config):
|
||||
if CONF_LAMBDA in config:
|
||||
if config[CONF_OPTIMISTIC]:
|
||||
raise cv.Invalid("optimistic cannot be used with lambda")
|
||||
if CONF_INITIAL_VALUE in config:
|
||||
raise cv.Invalid("initial_value cannot be used with lambda")
|
||||
if CONF_RESTORE_VALUE in config:
|
||||
raise cv.Invalid("restore_value cannot be used with lambda")
|
||||
elif CONF_INITIAL_VALUE not in config:
|
||||
config[CONF_INITIAL_VALUE] = ""
|
||||
|
||||
if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config:
|
||||
raise cv.Invalid(
|
||||
"Either optimistic mode must be enabled, or set_action must be set, to handle the text input being set."
|
||||
)
|
||||
|
||||
with cv.prepend_path(CONF_MIN_LENGTH):
|
||||
if config[CONF_MIN_LENGTH] >= config[CONF_MAX_LENGTH]:
|
||||
raise cv.Invalid("min_length must be less than max_length")
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
text.TEXT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TemplateText),
|
||||
cv.Optional(CONF_MIN_LENGTH, default=0): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_MAX_LENGTH, default=255): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_PATTERN): cv.string,
|
||||
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
|
||||
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
|
||||
}
|
||||
).extend(cv.polling_component_schema("60s")),
|
||||
validate,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await text.new_text(
|
||||
config,
|
||||
min_length=config[CONF_MIN_LENGTH],
|
||||
max_length=config[CONF_MAX_LENGTH],
|
||||
pattern=config.get(CONF_PATTERN),
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string)
|
||||
)
|
||||
cg.add(var.set_template(template_))
|
||||
|
||||
else:
|
||||
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
|
||||
if initial_value_config := config.get(CONF_INITIAL_VALUE):
|
||||
cg.add(var.set_initial_value(initial_value_config))
|
||||
if config[CONF_RESTORE_VALUE]:
|
||||
args = cg.TemplateArguments(config[CONF_MAX_LENGTH])
|
||||
saver = TextSaverTemplate.template(args).new()
|
||||
cg.add(var.set_value_saver(saver))
|
||||
|
||||
if CONF_SET_ACTION in config:
|
||||
await automation.build_automation(
|
||||
var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION]
|
||||
)
|
64
esphome/components/template/text/template_text.cpp
Normal file
64
esphome/components/template/text/template_text.cpp
Normal file
|
@ -0,0 +1,64 @@
|
|||
#include "template_text.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace template_ {
|
||||
|
||||
static const char *const TAG = "template.text";
|
||||
|
||||
void TemplateText::setup() {
|
||||
if (!(this->f_ == nullptr)) {
|
||||
if (this->f_.has_value())
|
||||
return;
|
||||
}
|
||||
|
||||
std::string value;
|
||||
ESP_LOGD(TAG, "Setting up Template Text Input");
|
||||
value = this->initial_value_;
|
||||
if (!this->pref_) {
|
||||
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
|
||||
} else {
|
||||
uint32_t key = this->get_object_id_hash();
|
||||
key += this->traits.get_min_length() << 2;
|
||||
key += this->traits.get_max_length() << 4;
|
||||
key += fnv1_hash(this->traits.get_pattern()) << 6;
|
||||
this->pref_->setup(key, value);
|
||||
}
|
||||
if (!value.empty())
|
||||
this->publish_state(value);
|
||||
}
|
||||
|
||||
void TemplateText::update() {
|
||||
if (this->f_ == nullptr)
|
||||
return;
|
||||
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
if (!val.has_value())
|
||||
return;
|
||||
|
||||
this->publish_state(*val);
|
||||
}
|
||||
|
||||
void TemplateText::control(const std::string &value) {
|
||||
this->set_trigger_->trigger(value);
|
||||
|
||||
if (this->optimistic_)
|
||||
this->publish_state(value);
|
||||
|
||||
if (this->pref_) {
|
||||
if (!this->pref_->save(value)) {
|
||||
ESP_LOGW(TAG, "Text value too long to save");
|
||||
}
|
||||
}
|
||||
}
|
||||
void TemplateText::dump_config() {
|
||||
LOG_TEXT("", "Template Text Input", this);
|
||||
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
} // namespace template_
|
||||
} // namespace esphome
|
87
esphome/components/template/text/template_text.h
Normal file
87
esphome/components/template/text/template_text.h
Normal file
|
@ -0,0 +1,87 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/text/text.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace template_ {
|
||||
|
||||
// We keep this separate so we don't have to template and duplicate
|
||||
// the text input for each different size flash allocation.
|
||||
class TemplateTextSaverBase {
|
||||
public:
|
||||
virtual bool save(const std::string &value) { return true; }
|
||||
|
||||
virtual void setup(uint32_t id, std::string &value) {}
|
||||
|
||||
protected:
|
||||
ESPPreferenceObject pref_;
|
||||
std::string prev_;
|
||||
};
|
||||
|
||||
template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase {
|
||||
public:
|
||||
bool save(const std::string &value) override {
|
||||
int diff = value.compare(this->prev_);
|
||||
if (diff != 0) {
|
||||
// If string is bigger than the allocation, do not save it.
|
||||
// We don't need to waste ram setting prev_value either.
|
||||
int size = value.size();
|
||||
if (size <= SZ) {
|
||||
// Make it into a length prefixed thing
|
||||
unsigned char temp[SZ + 1];
|
||||
memcpy(temp + 1, value.c_str(), size);
|
||||
// SZ should be pre checked at the schema level, it can't go past the char range.
|
||||
temp[0] = ((unsigned char) size);
|
||||
this->pref_.save(&temp);
|
||||
this->prev_.assign(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make the preference object. Fill the provided location with the saved data
|
||||
// If it is available, else leave it alone
|
||||
void setup(uint32_t id, std::string &value) override {
|
||||
this->pref_ = global_preferences->make_preference<uint8_t[SZ + 1]>(id);
|
||||
|
||||
char temp[SZ + 1];
|
||||
bool hasdata = this->pref_.load(&temp);
|
||||
|
||||
if (hasdata) {
|
||||
value.assign(temp + 1, (size_t) temp[0]);
|
||||
}
|
||||
|
||||
this->prev_.assign(value);
|
||||
}
|
||||
};
|
||||
|
||||
class TemplateText : public text::Text, 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_value(const std::string &initial_value) { this->initial_value_ = initial_value; }
|
||||
void set_value_saver(TemplateTextSaverBase *restore_value_saver) { this->pref_ = restore_value_saver; }
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
bool optimistic_ = false;
|
||||
std::string initial_value_;
|
||||
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
||||
optional<std::function<optional<std::string>()>> f_{nullptr};
|
||||
|
||||
TemplateTextSaverBase *pref_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace template_
|
||||
} // namespace esphome
|
138
esphome/components/text/__init__.py
Normal file
138
esphome/components/text/__init__.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
from typing import Optional
|
||||
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_ID,
|
||||
CONF_MODE,
|
||||
CONF_ON_VALUE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_VALUE,
|
||||
)
|
||||
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
CODEOWNERS = ["@mauritskorse"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
text_ns = cg.esphome_ns.namespace("text")
|
||||
Text = text_ns.class_("Text", cg.EntityBase)
|
||||
TextPtr = Text.operator("ptr")
|
||||
|
||||
# Triggers
|
||||
TextStateTrigger = text_ns.class_(
|
||||
"TextStateTrigger", automation.Trigger.template(cg.std_string)
|
||||
)
|
||||
|
||||
# Actions
|
||||
TextSetAction = text_ns.class_("TextSetAction", automation.Action)
|
||||
|
||||
# Conditions
|
||||
TextMode = text_ns.enum("TextMode")
|
||||
|
||||
TEXT_MODES = {
|
||||
"TEXT": TextMode.TEXT_MODE_TEXT,
|
||||
"PASSWORD": TextMode.TEXT_MODE_PASSWORD, # to be implemented for keys, passwords, etc.
|
||||
}
|
||||
|
||||
TEXT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextComponent),
|
||||
cv.GenerateID(): cv.declare_id(Text),
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TextStateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Required(CONF_MODE): cv.enum(TEXT_MODES, upper=True),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def setup_text_core_(
|
||||
var,
|
||||
config,
|
||||
*,
|
||||
min_length: Optional[int],
|
||||
max_length: Optional[int],
|
||||
pattern: Optional[str],
|
||||
):
|
||||
await setup_entity(var, config)
|
||||
|
||||
cg.add(var.traits.set_min_length(min_length))
|
||||
cg.add(var.traits.set_max_length(max_length))
|
||||
if pattern is not None:
|
||||
cg.add(var.traits.set_pattern(pattern))
|
||||
|
||||
cg.add(var.traits.set_mode(config[CONF_MODE]))
|
||||
|
||||
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_text(
|
||||
var,
|
||||
config,
|
||||
*,
|
||||
min_length: Optional[int] = 0,
|
||||
max_length: Optional[int] = 255,
|
||||
pattern: Optional[str] = None,
|
||||
):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_text(var))
|
||||
await setup_text_core_(
|
||||
var, config, min_length=min_length, max_length=max_length, pattern=pattern
|
||||
)
|
||||
|
||||
|
||||
async def new_text(
|
||||
config,
|
||||
*,
|
||||
min_length: Optional[int] = 0,
|
||||
max_length: Optional[int] = 255,
|
||||
pattern: Optional[str] = None,
|
||||
):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await register_text(
|
||||
var, config, min_length=min_length, max_length=max_length, pattern=pattern
|
||||
)
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(40.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_TEXT")
|
||||
cg.add_global(text_ns.using)
|
||||
|
||||
|
||||
OPERATION_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Text),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"text.set",
|
||||
TextSetAction,
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.string_strict),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def text_set_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
template_ = await cg.templatable(config[CONF_VALUE], args, cg.std_string)
|
||||
cg.add(var.set_value(template_))
|
||||
return var
|
33
esphome/components/text/automation.h
Normal file
33
esphome/components/text/automation.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "text.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace text {
|
||||
|
||||
class TextStateTrigger : public Trigger<std::string> {
|
||||
public:
|
||||
explicit TextStateTrigger(Text *parent) {
|
||||
parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class TextSetAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit TextSetAction(Text *text) : text_(text) {}
|
||||
TEMPLATABLE_VALUE(std::string, value)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto call = this->text_->make_call();
|
||||
call.set_value(this->value_.value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
Text *text_;
|
||||
};
|
||||
|
||||
} // namespace text
|
||||
} // namespace esphome
|
26
esphome/components/text/text.cpp
Normal file
26
esphome/components/text/text.cpp
Normal file
|
@ -0,0 +1,26 @@
|
|||
#include "text.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace text {
|
||||
|
||||
static const char *const TAG = "text";
|
||||
|
||||
void Text::publish_state(const std::string &state) {
|
||||
this->has_state_ = true;
|
||||
this->state = state;
|
||||
if (this->traits.get_mode() == TEXT_MODE_PASSWORD) {
|
||||
ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), state.c_str());
|
||||
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str());
|
||||
}
|
||||
this->state_callback_.call(state);
|
||||
}
|
||||
|
||||
void Text::add_on_state_callback(std::function<void(std::string)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace text
|
||||
} // namespace esphome
|
55
esphome/components/text/text.h
Normal file
55
esphome/components/text/text.h
Normal file
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "text_call.h"
|
||||
#include "text_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace text {
|
||||
|
||||
#define LOG_TEXT(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_icon().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
|
||||
} \
|
||||
}
|
||||
|
||||
/** Base-class for all text inputs.
|
||||
*
|
||||
* A text input can use publish_state to send out a new value.
|
||||
*/
|
||||
class Text : public EntityBase {
|
||||
public:
|
||||
std::string state;
|
||||
TextTraits traits;
|
||||
|
||||
void publish_state(const std::string &state);
|
||||
|
||||
/// Return whether this text input has gotten a full state yet.
|
||||
bool has_state() const { return has_state_; }
|
||||
|
||||
/// Instantiate a TextCall object to modify this text component's state.
|
||||
TextCall make_call() { return TextCall(this); }
|
||||
|
||||
void add_on_state_callback(std::function<void(std::string)> &&callback);
|
||||
|
||||
protected:
|
||||
friend class TextCall;
|
||||
|
||||
/** Set the value of the text input, this is a virtual method that each text input integration must implement.
|
||||
*
|
||||
* This method is called by the TextCall.
|
||||
*
|
||||
* @param value The value as validated by the TextCall.
|
||||
*/
|
||||
virtual void control(const std::string &value) = 0;
|
||||
|
||||
CallbackManager<void(std::string)> state_callback_;
|
||||
bool has_state_{false};
|
||||
};
|
||||
|
||||
} // namespace text
|
||||
} // namespace esphome
|
56
esphome/components/text/text_call.cpp
Normal file
56
esphome/components/text/text_call.cpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
#include "text_call.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "text.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace text {
|
||||
|
||||
static const char *const TAG = "text";
|
||||
|
||||
TextCall &TextCall::set_value(const std::string &value) {
|
||||
this->value_ = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void TextCall::validate_() {
|
||||
const auto *name = this->parent_->get_name().c_str();
|
||||
|
||||
if (!this->value_.has_value()) {
|
||||
ESP_LOGW(TAG, "'%s' - No value set for TextCall", name);
|
||||
return;
|
||||
}
|
||||
|
||||
int sz = this->value_.value().size();
|
||||
|
||||
if (sz > this->parent_->traits.get_max_length()) {
|
||||
ESP_LOGW(TAG, "'%s' - Value set for TextCall is too long", name);
|
||||
this->value_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sz < this->parent_->traits.get_min_length()) {
|
||||
ESP_LOGW(TAG, "'%s' - Value set for TextCall is too short", name);
|
||||
this->value_.reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void TextCall::perform() {
|
||||
this->validate_();
|
||||
if (!this->value_.has_value()) {
|
||||
ESP_LOGW(TAG, "'%s' - No value set for TextCall", this->parent_->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
std::string target_value = this->value_.value();
|
||||
|
||||
if (this->parent_->traits.get_mode() == TEXT_MODE_PASSWORD) {
|
||||
ESP_LOGD(TAG, "'%s' - Setting password value: " LOG_SECRET("'%s'"), this->parent_->get_name().c_str(),
|
||||
target_value.c_str());
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s' - Setting text value: %s", this->parent_->get_name().c_str(), target_value.c_str());
|
||||
}
|
||||
this->parent_->control(target_value);
|
||||
}
|
||||
|
||||
} // namespace text
|
||||
} // namespace esphome
|
25
esphome/components/text/text_call.h
Normal file
25
esphome/components/text/text_call.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "text_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace text {
|
||||
|
||||
class Text;
|
||||
|
||||
class TextCall {
|
||||
public:
|
||||
explicit TextCall(Text *parent) : parent_(parent) {}
|
||||
void perform();
|
||||
|
||||
TextCall &set_value(const std::string &value);
|
||||
|
||||
protected:
|
||||
Text *const parent_;
|
||||
optional<std::string> value_;
|
||||
void validate_();
|
||||
};
|
||||
|
||||
} // namespace text
|
||||
} // namespace esphome
|
39
esphome/components/text/text_traits.h
Normal file
39
esphome/components/text/text_traits.h
Normal file
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace text {
|
||||
|
||||
enum TextMode : uint8_t {
|
||||
TEXT_MODE_TEXT = 0,
|
||||
TEXT_MODE_PASSWORD = 1,
|
||||
};
|
||||
|
||||
class TextTraits {
|
||||
public:
|
||||
// Set/get the number value boundaries.
|
||||
void set_min_length(int min_length) { this->min_length_ = min_length; }
|
||||
int get_min_length() const { return this->min_length_; }
|
||||
void set_max_length(int max_length) { this->max_length_ = max_length; }
|
||||
int get_max_length() const { return this->max_length_; }
|
||||
|
||||
// Set/get the pattern.
|
||||
void set_pattern(std::string pattern) { this->pattern_ = std::move(pattern); }
|
||||
std::string get_pattern() const { return this->pattern_; }
|
||||
|
||||
// Set/get the frontend mode.
|
||||
void set_mode(TextMode mode) { this->mode_ = mode; }
|
||||
TextMode get_mode() const { return this->mode_; }
|
||||
|
||||
protected:
|
||||
int min_length_;
|
||||
int max_length_;
|
||||
std::string pattern_;
|
||||
TextMode mode_{TEXT_MODE_TEXT};
|
||||
};
|
||||
|
||||
} // namespace text
|
||||
} // namespace esphome
|
|
@ -82,6 +82,13 @@ bool ListEntitiesIterator::on_number(number::Number *number) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
bool ListEntitiesIterator::on_text(text::Text *text) {
|
||||
this->web_server_->events_.send(this->web_server_->text_json(text, text->state, DETAIL_ALL).c_str(), "state");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
bool ListEntitiesIterator::on_select(select::Select *select) {
|
||||
this->web_server_->events_.send(this->web_server_->select_json(select, select->state, DETAIL_ALL).c_str(), "state");
|
||||
|
|
|
@ -41,6 +41,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
|||
#ifdef USE_NUMBER
|
||||
bool on_number(number::Number *number) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool on_text(text::Text *text) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool on_select(select::Select *select) override;
|
||||
#endif
|
||||
|
|
|
@ -264,6 +264,32 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
for (auto *obj : App.get_texts()) {
|
||||
if (this->include_internal_ || !obj->is_internal()) {
|
||||
write_row(stream, obj, "text", "", [](AsyncResponseStream &stream, EntityBase *obj) {
|
||||
text::Text *text = (text::Text *) obj;
|
||||
auto mode = (int) text->traits.get_mode();
|
||||
stream.print(R"(<input type=")");
|
||||
if (mode == 2) {
|
||||
stream.print(R"(password)");
|
||||
} else { // default
|
||||
stream.print(R"(text)");
|
||||
}
|
||||
stream.print(R"(" minlength=")");
|
||||
stream.print(text->traits.get_min_length());
|
||||
stream.print(R"(" maxlength=")");
|
||||
stream.print(text->traits.get_max_length());
|
||||
stream.print(R"(" pattern=")");
|
||||
stream.print(text->traits.get_pattern().c_str());
|
||||
stream.print(R"(" value=")");
|
||||
stream.print(text->state.c_str());
|
||||
stream.print(R"("/>)");
|
||||
});
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
for (auto *obj : App.get_selects()) {
|
||||
if (this->include_internal_ || !obj->is_internal()) {
|
||||
|
@ -795,6 +821,57 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
|
|||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
void WebServer::on_text_update(text::Text *obj, const std::string &state) {
|
||||
this->events_.send(this->text_json(obj, state, DETAIL_STATE).c_str(), "state");
|
||||
}
|
||||
void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (auto *obj : App.get_texts()) {
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
std::string data = this->text_json(obj, obj->state, DETAIL_STATE);
|
||||
request->send(200, "text/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
if (match.method != "set") {
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
call.set_value(value.c_str());
|
||||
}
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) {
|
||||
return json::build_json([obj, value, start_config](JsonObject root) {
|
||||
set_json_id(root, obj, "text-" + obj->get_object_id(), start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root["mode"] = (int) obj->traits.get_mode();
|
||||
}
|
||||
root["min_length"] = obj->traits.get_min_length();
|
||||
root["max_length"] = obj->traits.get_max_length();
|
||||
root["pattern"] = obj->traits.get_pattern();
|
||||
if (obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD) {
|
||||
root["state"] = "********";
|
||||
} else {
|
||||
root["state"] = value;
|
||||
}
|
||||
root["value"] = value;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
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");
|
||||
|
@ -1116,6 +1193,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
|
|||
return true;
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text")
|
||||
return true;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "select")
|
||||
return true;
|
||||
|
@ -1222,6 +1304,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
if (match.domain == "text") {
|
||||
this->handle_text_request(request, match);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
if (match.domain == "select") {
|
||||
this->handle_select_request(request, match);
|
||||
|
|
|
@ -216,6 +216,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
|||
std::string number_json(number::Number *obj, float value, JsonDetail start_config);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
void on_text_update(text::Text *obj, const std::string &state) override;
|
||||
/// Handle a text input request under '/text/<id>'.
|
||||
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
/// Dump the text state with its value as a JSON string.
|
||||
std::string text_json(text::Text *obj, const std::string &value, JsonDetail start_config);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
|
||||
/// Handle a select request under '/select/<id>'.
|
||||
|
|
28
esphome/components/wireguard/text_sensor.py
Normal file
28
esphome/components/wireguard/text_sensor.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import text_sensor
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
|
||||
from . import Wireguard
|
||||
|
||||
CONF_WIREGUARD_ID = "wireguard_id"
|
||||
|
||||
DEPENDENCIES = ["wireguard"]
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_WIREGUARD_ID): cv.use_id(Wireguard),
|
||||
cv.Optional(CONF_ADDRESS): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
parent = await cg.get_variable(config[CONF_WIREGUARD_ID])
|
||||
|
||||
if address_config := config.get(CONF_ADDRESS):
|
||||
sens = await text_sensor.new_text_sensor(address_config)
|
||||
cg.add(parent.set_address_sensor(sens))
|
|
@ -54,6 +54,12 @@ void Wireguard::setup() {
|
|||
this->wg_peer_offline_time_ = millis();
|
||||
this->srctime_->add_on_time_sync_callback(std::bind(&Wireguard::start_connection_, this));
|
||||
this->defer(std::bind(&Wireguard::start_connection_, this)); // defer to avoid blocking setup
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->address_sensor_ != nullptr) {
|
||||
this->address_sensor_->publish_state(this->address_);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
ESP_LOGE(TAG, "cannot initialize WireGuard, error code %d", this->wg_initialized_);
|
||||
this->mark_failed();
|
||||
|
@ -186,6 +192,10 @@ void Wireguard::set_status_sensor(binary_sensor::BinarySensor *sensor) { this->s
|
|||
void Wireguard::set_handshake_sensor(sensor::Sensor *sensor) { this->handshake_sensor_ = sensor; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void Wireguard::set_address_sensor(text_sensor::TextSensor *sensor) { this->address_sensor_ = sensor; }
|
||||
#endif
|
||||
|
||||
void Wireguard::disable_auto_proceed() { this->proceed_allowed_ = false; }
|
||||
|
||||
void Wireguard::start_connection_() {
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
#include <esp_wireguard.h>
|
||||
|
||||
namespace esphome {
|
||||
|
@ -55,6 +59,10 @@ class Wireguard : public PollingComponent {
|
|||
void set_handshake_sensor(sensor::Sensor *sensor);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void set_address_sensor(text_sensor::TextSensor *sensor);
|
||||
#endif
|
||||
|
||||
/// Block the setup step until peer is connected.
|
||||
void disable_auto_proceed();
|
||||
|
||||
|
@ -85,6 +93,10 @@ class Wireguard : public PollingComponent {
|
|||
sensor::Sensor *handshake_sensor_ = nullptr;
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *address_sensor_ = nullptr;
|
||||
#endif
|
||||
|
||||
/// Set to false to block the setup step until peer is connected.
|
||||
bool proceed_allowed_ = true;
|
||||
|
||||
|
|
0
esphome/components/zhlt01/__init__.py
Normal file
0
esphome/components/zhlt01/__init__.py
Normal file
19
esphome/components/zhlt01/climate.py
Normal file
19
esphome/components/zhlt01/climate.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate_ir
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
CODEOWNERS = ["@cfeenstra1024"]
|
||||
|
||||
zhlt01_ns = cg.esphome_ns.namespace("zhlt01")
|
||||
ZHLT01Climate = zhlt01_ns.class_("ZHLT01Climate", climate_ir.ClimateIR)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
|
||||
{cv.GenerateID(): cv.declare_id(ZHLT01Climate)}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await climate_ir.register_climate_ir(var, config)
|
238
esphome/components/zhlt01/zhlt01.cpp
Normal file
238
esphome/components/zhlt01/zhlt01.cpp
Normal file
|
@ -0,0 +1,238 @@
|
|||
#include "zhlt01.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace zhlt01 {
|
||||
|
||||
static const char *const TAG = "zhlt01.climate";
|
||||
|
||||
void ZHLT01Climate::transmit_state() {
|
||||
uint8_t ir_message[12] = {0};
|
||||
|
||||
// Byte 1 : Timer
|
||||
ir_message[1] = 0x00; // Timer off
|
||||
|
||||
// Byte 3 : Turbo mode
|
||||
if (this->preset.value() == climate::CLIMATE_PRESET_BOOST) {
|
||||
ir_message[3] = AC1_FAN_TURBO;
|
||||
}
|
||||
|
||||
// Byte 5 : Last pressed button
|
||||
ir_message[5] = 0x00; // fixed as power button
|
||||
|
||||
// Byte 7 : Power | Swing | Fan
|
||||
// -- Power
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
ir_message[7] = AC1_POWER_OFF;
|
||||
} else {
|
||||
ir_message[7] = AC1_POWER_ON;
|
||||
}
|
||||
|
||||
// -- Swing
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
ir_message[7] |= AC1_HDIR_FIXED | AC1_VDIR_FIXED;
|
||||
break;
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
ir_message[7] |= AC1_HDIR_SWING | AC1_VDIR_FIXED;
|
||||
break;
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
ir_message[7] |= AC1_HDIR_FIXED | AC1_VDIR_SWING;
|
||||
break;
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
ir_message[7] |= AC1_HDIR_SWING | AC1_VDIR_SWING;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Fan
|
||||
switch (this->preset.value()) {
|
||||
case climate::CLIMATE_PRESET_BOOST:
|
||||
ir_message[7] |= AC1_FAN3;
|
||||
break;
|
||||
case climate::CLIMATE_PRESET_SLEEP:
|
||||
ir_message[7] |= AC1_FAN_SILENT;
|
||||
break;
|
||||
default:
|
||||
switch (this->fan_mode.value()) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
ir_message[7] |= AC1_FAN1;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
ir_message[7] |= AC1_FAN2;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
ir_message[7] |= AC1_FAN3;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
ir_message[7] |= AC1_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Byte 9 : AC Mode | Temperature
|
||||
// -- AC Mode
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_AUTO:
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
ir_message[9] = AC1_MODE_AUTO;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
ir_message[9] = AC1_MODE_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
ir_message[9] = AC1_MODE_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
ir_message[9] = AC1_MODE_DRY;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
ir_message[9] = AC1_MODE_FAN;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Temperature
|
||||
ir_message[9] |= (uint8_t) (this->target_temperature - 16.0f);
|
||||
|
||||
// Byte 11 : Remote control ID
|
||||
ir_message[11] = 0xD5;
|
||||
|
||||
// Set checksum bytes
|
||||
for (int i = 0; i < 12; i += 2) {
|
||||
ir_message[i] = ~ir_message[i + 1];
|
||||
}
|
||||
|
||||
// Send the code
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
|
||||
data->set_carrier_frequency(38000); // 38 kHz PWM
|
||||
|
||||
// Header
|
||||
data->mark(AC1_HDR_MARK);
|
||||
data->space(AC1_HDR_SPACE);
|
||||
|
||||
// Data
|
||||
for (uint8_t i : ir_message) {
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
data->mark(AC1_BIT_MARK);
|
||||
bool bit = i & (1 << j);
|
||||
data->space(bit ? AC1_ONE_SPACE : AC1_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
data->mark(AC1_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
bool ZHLT01Climate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
// Validate header
|
||||
if (!data.expect_item(AC1_HDR_MARK, AC1_HDR_SPACE)) {
|
||||
ESP_LOGV(TAG, "Header fail");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decode IR message
|
||||
uint8_t ir_message[12] = {0};
|
||||
// Read all bytes
|
||||
for (int i = 0; i < 12; i++) {
|
||||
// Read bit
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (data.expect_item(AC1_BIT_MARK, AC1_ONE_SPACE)) {
|
||||
ir_message[i] |= 1 << j;
|
||||
} else if (!data.expect_item(AC1_BIT_MARK, AC1_ZERO_SPACE)) {
|
||||
ESP_LOGV(TAG, "Byte %d bit %d fail", i, j);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ESP_LOGVV(TAG, "Byte %d %02X", i, ir_message[i]);
|
||||
}
|
||||
|
||||
// Validate footer
|
||||
if (!data.expect_mark(AC1_BIT_MARK)) {
|
||||
ESP_LOGV(TAG, "Footer fail");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate checksum
|
||||
for (int i = 0; i < 12; i += 2) {
|
||||
if (ir_message[i] != (uint8_t) (~ir_message[i + 1])) {
|
||||
ESP_LOGV(TAG, "Byte %d checksum incorrect (%02X != %02X)", i, ir_message[i], (uint8_t) (~ir_message[i + 1]));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate remote control ID
|
||||
if (ir_message[11] != 0xD5) {
|
||||
ESP_LOGV(TAG, "Invalid remote control ID");
|
||||
return false;
|
||||
}
|
||||
|
||||
// All is good to go
|
||||
|
||||
if ((ir_message[7] & AC1_POWER_ON) == 0) {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
} else {
|
||||
// Vertical swing
|
||||
if ((ir_message[7] & 0x0C) == AC1_VDIR_FIXED) {
|
||||
if ((ir_message[7] & 0x10) == AC1_HDIR_FIXED) {
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
} else {
|
||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||
}
|
||||
} else {
|
||||
if ((ir_message[7] & 0x10) == AC1_HDIR_FIXED) {
|
||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
} else {
|
||||
this->swing_mode = climate::CLIMATE_SWING_BOTH;
|
||||
}
|
||||
}
|
||||
|
||||
// Preset + Fan speed
|
||||
if ((ir_message[3] & AC1_FAN_TURBO) == AC1_FAN_TURBO) {
|
||||
this->preset = climate::CLIMATE_PRESET_BOOST;
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
} else if ((ir_message[7] & 0xE1) == AC1_FAN_SILENT) {
|
||||
this->preset = climate::CLIMATE_PRESET_SLEEP;
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
} else if ((ir_message[7] & 0xE1) == AC1_FAN_AUTO) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
} else if ((ir_message[7] & 0xE1) == AC1_FAN1) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
} else if ((ir_message[7] & 0xE1) == AC1_FAN2) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
} else if ((ir_message[7] & 0xE1) == AC1_FAN3) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
}
|
||||
|
||||
// AC Mode
|
||||
if ((ir_message[9] & 0xE0) == AC1_MODE_COOL) {
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
} else if ((ir_message[9] & 0xE0) == AC1_MODE_HEAT) {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
} else if ((ir_message[9] & 0xE0) == AC1_MODE_DRY) {
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
} else if ((ir_message[9] & 0xE0) == AC1_MODE_FAN) {
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||
}
|
||||
|
||||
// Taregt Temperature
|
||||
this->target_temperature = (ir_message[9] & 0x1F) + 16.0f;
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace zhlt01
|
||||
} // namespace esphome
|
167
esphome/components/zhlt01/zhlt01.h
Normal file
167
esphome/components/zhlt01/zhlt01.h
Normal file
|
@ -0,0 +1,167 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
/***********************************************************************************
|
||||
* SOURCE
|
||||
***********************************************************************************
|
||||
* The IR codes and the functional description below were taken from
|
||||
* 'arduino-heatpumpir/ZHLT01HeatpumpIR.h' as can be found on GitHub
|
||||
* https://github.com/ToniA/arduino-heatpumpir/blob/master/ZHLT01HeatpumpIR.h
|
||||
*
|
||||
************************************************************************************
|
||||
* Airconditional remote control encoder for:
|
||||
*
|
||||
* ZH/LT-01 Remote control https://www.google.com/search?q=zh/lt-01
|
||||
*
|
||||
* The ZH/LT-01 remote control is used for many locally branded Split
|
||||
* airconditioners, so it is better to name this protocol by the name of the
|
||||
* REMOTE rather then the name of the Airconditioner. For this project I used
|
||||
* a 2014 model Eurom-airconditioner, which is Dutch-branded and sold in
|
||||
* the Netherlands at Hornbach.
|
||||
*
|
||||
* For airco-brands:
|
||||
* Eurom
|
||||
* Chigo
|
||||
* Tristar
|
||||
* Tecnomaster
|
||||
* Elgin
|
||||
* Geant
|
||||
* Tekno
|
||||
* Topair
|
||||
* Proma
|
||||
* Sumikura
|
||||
* JBS
|
||||
* Turbo Air
|
||||
* Nakatomy
|
||||
* Celestial Air
|
||||
* Ager
|
||||
* Blueway
|
||||
* Airlux
|
||||
* Etc.
|
||||
*
|
||||
***********************************************************************************
|
||||
* SUMMARY FUNCTIONAL DESCRIPTION
|
||||
***********************************************************************************
|
||||
* The remote sends a 12 Byte message which contains all possible settings every
|
||||
* time.
|
||||
*
|
||||
* Byte 11 (and 10) contain the remote control identifier and are always 0xD5 and
|
||||
* 0x2A respectively for the ZH/LT-01 remote control.
|
||||
* Every UNeven Byte (01,03,05,07 and 09) holds command data
|
||||
* Every EVEN Byte (00,02,04,06,08 and 10) holds a checksum of the corresponding
|
||||
* command-, or identifier-byte by _inverting_ the bits, for example:
|
||||
*
|
||||
* The identifier byte[11] = 0xD5 = B1101 0101
|
||||
* The checksum byte[10] = 0x2A = B0010 1010
|
||||
*
|
||||
* So, you can check the message by:
|
||||
* - inverting the bits of the checksum byte with the corresponding command-, or
|
||||
* identifier byte, they should be the same, or
|
||||
* - Summing up the checksum byte and the corresponding command-, or identifier byte,
|
||||
* they should always add up to 0xFF = B11111111 = 255
|
||||
*
|
||||
* Control bytes:
|
||||
* [01] - Timer (1-24 hours, Off)
|
||||
* Time is hardcoded to OFF
|
||||
*
|
||||
* [03] - LAMP ON/OFF, TURBO ON/OFF, HOLD ON/OFF
|
||||
* Lamp and Hold are hardcoded to OFF
|
||||
* Turbo is used for the BOOST preset
|
||||
*
|
||||
* [05] - Indicates which button the user _pressed_ on the remote control
|
||||
* Hardcoded to POWER-button
|
||||
*
|
||||
* [07] - POWER ON/OFF, FAN AUTO/3/2/1, SLEEP ON/OFF, AIRFLOW ON/OFF,
|
||||
* VERTICAL SWING/WIND/FIXED
|
||||
* SLEEP is used for preset SLEEP
|
||||
* Vertical Swing supports Fixed, Swing and "Wind". The Wind option
|
||||
* is ignored in this implementation
|
||||
*
|
||||
* [09] - MODE AUTO/COOL/VENT/DRY/HEAT, TEMPERATURE (16 - 32°C)
|
||||
*
|
||||
***********************************************************************************/
|
||||
|
||||
namespace esphome {
|
||||
namespace zhlt01 {
|
||||
|
||||
/********************************************************************************
|
||||
* TIMINGS
|
||||
* Space: Not used
|
||||
* Header Mark: 6100 us
|
||||
* Header Space: 7400 us
|
||||
* Bit Mark: 500 us
|
||||
* Zero Space: 600 us
|
||||
* One Space: 1800 us
|
||||
*
|
||||
* Note : These timings are slightly different than those of ZHLT01HeatpumpIR
|
||||
* The values below were measured by taking the average of 2 different
|
||||
* remote controls each sending 10 commands
|
||||
*******************************************************************************/
|
||||
static const uint32_t AC1_HDR_MARK = 6100;
|
||||
static const uint32_t AC1_HDR_SPACE = 7400;
|
||||
static const uint32_t AC1_BIT_MARK = 500;
|
||||
static const uint32_t AC1_ZERO_SPACE = 600;
|
||||
static const uint32_t AC1_ONE_SPACE = 1800;
|
||||
|
||||
/********************************************************************************
|
||||
*
|
||||
* ZHLT01 codes
|
||||
*
|
||||
*******************************************************************************/
|
||||
|
||||
// Power
|
||||
static const uint8_t AC1_POWER_OFF = 0x00;
|
||||
static const uint8_t AC1_POWER_ON = 0x02;
|
||||
|
||||
// Operating Modes
|
||||
static const uint8_t AC1_MODE_AUTO = 0x00;
|
||||
static const uint8_t AC1_MODE_COOL = 0x20;
|
||||
static const uint8_t AC1_MODE_DRY = 0x40;
|
||||
static const uint8_t AC1_MODE_FAN = 0x60;
|
||||
static const uint8_t AC1_MODE_HEAT = 0x80;
|
||||
|
||||
// Fan control
|
||||
static const uint8_t AC1_FAN_AUTO = 0x00;
|
||||
static const uint8_t AC1_FAN_SILENT = 0x01;
|
||||
static const uint8_t AC1_FAN1 = 0x60;
|
||||
static const uint8_t AC1_FAN2 = 0x40;
|
||||
static const uint8_t AC1_FAN3 = 0x20;
|
||||
static const uint8_t AC1_FAN_TURBO = 0x08;
|
||||
|
||||
// Vertical Swing
|
||||
static const uint8_t AC1_VDIR_WIND = 0x00; // "Natural Wind", ignore
|
||||
static const uint8_t AC1_VDIR_SWING = 0x04; // Swing
|
||||
static const uint8_t AC1_VDIR_FIXED = 0x08; // Fixed
|
||||
|
||||
// Horizontal Swing
|
||||
static const uint8_t AC1_HDIR_SWING = 0x00; // Swing
|
||||
static const uint8_t AC1_HDIR_FIXED = 0x10; // Fixed
|
||||
|
||||
// Temperature range
|
||||
static const float AC1_TEMP_MIN = 16.0f;
|
||||
static const float AC1_TEMP_MAX = 32.0f;
|
||||
static const float AC1_TEMP_INC = 1.0f;
|
||||
|
||||
class ZHLT01Climate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
ZHLT01Climate()
|
||||
: climate_ir::ClimateIR(
|
||||
AC1_TEMP_MIN, AC1_TEMP_MAX, AC1_TEMP_INC, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_BOTH},
|
||||
{climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_BOOST}) {}
|
||||
|
||||
void setup() override { climate_ir::ClimateIR::setup(); }
|
||||
|
||||
protected:
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
/// Handle received IR Buffer
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
};
|
||||
|
||||
} // namespace zhlt01
|
||||
} // namespace esphome
|
|
@ -182,6 +182,7 @@ RESERVED_IDS = [
|
|||
"struct",
|
||||
"switch",
|
||||
"template",
|
||||
"text",
|
||||
"this",
|
||||
"thread_local",
|
||||
"throw",
|
||||
|
|
|
@ -542,6 +542,7 @@ CONF_PANASONIC = "panasonic"
|
|||
CONF_PARAMETERS = "parameters"
|
||||
CONF_PASSWORD = "password"
|
||||
CONF_PATH = "path"
|
||||
CONF_PATTERN = "pattern"
|
||||
CONF_PAYLOAD = "payload"
|
||||
CONF_PAYLOAD_AVAILABLE = "payload_available"
|
||||
CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available"
|
||||
|
@ -679,6 +680,7 @@ CONF_SERVERS = "servers"
|
|||
CONF_SERVICE = "service"
|
||||
CONF_SERVICE_UUID = "service_uuid"
|
||||
CONF_SERVICES = "services"
|
||||
CONF_SET_ACTION = "set_action"
|
||||
CONF_SET_POINT_MINIMUM_DIFFERENTIAL = "set_point_minimum_differential"
|
||||
CONF_SETUP_MODE = "setup_mode"
|
||||
CONF_SETUP_PRIORITY = "setup_priority"
|
||||
|
|
|
@ -39,6 +39,9 @@
|
|||
#ifdef USE_NUMBER
|
||||
#include "esphome/components/number/number.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
#include "esphome/components/text/text.h"
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
|
@ -117,6 +120,10 @@ class Application {
|
|||
void register_number(number::Number *number) { this->numbers_.push_back(number); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
void register_text(text::Text *text) { this->texts_.push_back(text); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void register_select(select::Select *select) { this->selects_.push_back(select); }
|
||||
#endif
|
||||
|
@ -277,6 +284,15 @@ class Application {
|
|||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
const std::vector<text::Text *> &get_texts() { return this->texts_; }
|
||||
text::Text *get_text_by_key(uint32_t key, bool include_internal = false) {
|
||||
for (auto *obj : this->texts_)
|
||||
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
|
||||
return obj;
|
||||
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) {
|
||||
|
@ -364,6 +380,9 @@ class Application {
|
|||
#ifdef USE_SELECT
|
||||
std::vector<select::Select *> selects_{};
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
std::vector<text::Text *> texts_{};
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
std::vector<lock::Lock *> locks_{};
|
||||
#endif
|
||||
|
|
|
@ -202,6 +202,21 @@ void ComponentIterator::advance() {
|
|||
}
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
case IteratorState::TEXT:
|
||||
if (this->at_ >= App.get_texts().size()) {
|
||||
advance_platform = true;
|
||||
} else {
|
||||
auto *text = App.get_texts()[this->at_];
|
||||
if (text->is_internal() && !this->include_internal_) {
|
||||
success = true;
|
||||
break;
|
||||
} else {
|
||||
success = this->on_text(text);
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
case IteratorState::SELECT:
|
||||
if (this->at_ >= App.get_selects().size()) {
|
||||
|
|
|
@ -57,6 +57,9 @@ class ComponentIterator {
|
|||
#ifdef USE_NUMBER
|
||||
virtual bool on_number(number::Number *number) = 0;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
virtual bool on_text(text::Text *text) = 0;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
virtual bool on_select(select::Select *select) = 0;
|
||||
#endif
|
||||
|
@ -111,6 +114,9 @@ class ComponentIterator {
|
|||
#ifdef USE_NUMBER
|
||||
NUMBER,
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
TEXT,
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
SELECT,
|
||||
#endif
|
||||
|
|
|
@ -59,6 +59,12 @@ void Controller::setup_controller(bool include_internal) {
|
|||
obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); });
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
for (auto *obj : App.get_texts()) {
|
||||
if (include_internal || !obj->is_internal())
|
||||
obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_update(obj, state); });
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
for (auto *obj : App.get_selects()) {
|
||||
if (include_internal || !obj->is_internal()) {
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
#ifdef USE_NUMBER
|
||||
#include "esphome/components/number/number.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
#include "esphome/components/text/text.h"
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
|
@ -76,6 +79,9 @@ class Controller {
|
|||
#ifdef USE_NUMBER
|
||||
virtual void on_number_update(number::Number *obj, float state){};
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
virtual void on_text_update(text::Text *obj, const std::string &state){};
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){};
|
||||
#endif
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
#define USE_SENSOR
|
||||
#define USE_STATUS_LED
|
||||
#define USE_SWITCH
|
||||
#define USE_TEXT
|
||||
#define USE_TEXT_SENSOR
|
||||
#define USE_TIME
|
||||
#define USE_TOUCHSCREEN
|
||||
|
|
|
@ -17,6 +17,8 @@ from zeroconf import (
|
|||
current_time_millis,
|
||||
)
|
||||
|
||||
from esphome.storage_json import StorageJSON, ext_storage_path
|
||||
|
||||
_CLASS_IN = 1
|
||||
_FLAGS_QR_QUERY = 0x0000 # query
|
||||
_TYPE_A = 1
|
||||
|
@ -131,6 +133,7 @@ TXT_RECORD_PROJECT_NAME = b"project_name"
|
|||
TXT_RECORD_PROJECT_VERSION = b"project_version"
|
||||
TXT_RECORD_NETWORK = b"network"
|
||||
TXT_RECORD_FRIENDLY_NAME = b"friendly_name"
|
||||
TXT_RECORD_VERSION = b"version"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -186,6 +189,10 @@ class DashboardImportDiscovery:
|
|||
]
|
||||
if any(key not in info.properties for key in required_keys):
|
||||
# Not a dashboard import device
|
||||
version = info.properties.get(TXT_RECORD_VERSION)
|
||||
if version is not None:
|
||||
version = version.decode()
|
||||
self.update_device_mdns(node_name, version)
|
||||
return
|
||||
|
||||
import_url = info.properties[TXT_RECORD_PACKAGE_IMPORT_URL].decode()
|
||||
|
@ -208,6 +215,22 @@ class DashboardImportDiscovery:
|
|||
def cancel(self) -> None:
|
||||
self.service_browser.cancel()
|
||||
|
||||
def update_device_mdns(self, node_name: str, version: str):
|
||||
storage_path = ext_storage_path(node_name + ".yaml")
|
||||
storage_json = StorageJSON.load(storage_path)
|
||||
|
||||
if storage_json is not None:
|
||||
storage_version = storage_json.esphome_version
|
||||
if version != storage_version:
|
||||
storage_json.esphome_version = version
|
||||
storage_json.save(storage_path)
|
||||
_LOGGER.info(
|
||||
"Updated %s with mdns version %s (was %s)",
|
||||
node_name,
|
||||
version,
|
||||
storage_version,
|
||||
)
|
||||
|
||||
|
||||
class EsphomeZeroconf(Zeroconf):
|
||||
def resolve_host(self, host: str, timeout=3.0):
|
||||
|
|
|
@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile
|
|||
esptool==4.6.2
|
||||
click==8.1.7
|
||||
esphome-dashboard==20230904.0
|
||||
aioesphomeapi==18.0.7
|
||||
aioesphomeapi==18.0.12
|
||||
zeroconf==0.119.0
|
||||
|
||||
# esp-idf requires this, but doesn't bundle it by default
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
pylint==2.17.6
|
||||
flake8==6.1.0 # also change in .pre-commit-config.yaml when updating
|
||||
black==23.10.0 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.13.0 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==7.4.2
|
||||
pytest-cov==4.1.0
|
||||
pytest-mock==3.11.1
|
||||
pytest-mock==3.12.0
|
||||
pytest-asyncio==0.21.1
|
||||
asyncmock==0.4.2
|
||||
hypothesis==5.49.0
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
It's pretty crappy spaghetti code, but it works.
|
||||
|
||||
you need to install protobuf-compiler:
|
||||
running protc --version should return
|
||||
running protoc --version should return
|
||||
libprotoc 3.6.1
|
||||
|
||||
then run this script with python3 and the files
|
||||
|
|
|
@ -617,6 +617,7 @@ def lint_trailing_whitespace(fname, match):
|
|||
"esphome/components/lock/lock.h",
|
||||
"esphome/components/mqtt/mqtt_component.h",
|
||||
"esphome/components/number/number.h",
|
||||
"esphome/components/text/text.h",
|
||||
"esphome/components/output/binary_output.h",
|
||||
"esphome/components/output/float_output.h",
|
||||
"esphome/components/nextion/nextion_base.h",
|
||||
|
|
56
tests/component_tests/text/test_text.py
Normal file
56
tests/component_tests/text/test_text.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
"""Tests for the binary sensor component."""
|
||||
|
||||
|
||||
def test_text_is_setup(generate_main):
|
||||
"""
|
||||
When the binary sensor is set in the yaml file, it should be registered in main
|
||||
"""
|
||||
# Given
|
||||
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert "new template_::TemplateText();" in main_cpp
|
||||
assert "App.register_text" in main_cpp
|
||||
|
||||
|
||||
def test_text_sets_mandatory_fields(generate_main):
|
||||
"""
|
||||
When the mandatory fields are set in the yaml, they should be set in main
|
||||
"""
|
||||
# Given
|
||||
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert 'it_1->set_name("test 1 text");' in main_cpp
|
||||
|
||||
|
||||
def test_text_config_value_internal_set(generate_main):
|
||||
"""
|
||||
Test that the "internal" config value is correctly set
|
||||
"""
|
||||
# Given
|
||||
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert "it_2->set_internal(false);" in main_cpp
|
||||
assert "it_3->set_internal(true);" in main_cpp
|
||||
|
||||
|
||||
def test_text_config_value_mode_set(generate_main):
|
||||
"""
|
||||
Test that the "internal" config value is correctly set
|
||||
"""
|
||||
# Given
|
||||
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert "it_1->traits.set_mode(text::TEXT_MODE_TEXT);" in main_cpp
|
||||
assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp
|
33
tests/component_tests/text/test_text.yaml
Normal file
33
tests/component_tests/text/test_text.yaml
Normal file
|
@ -0,0 +1,33 @@
|
|||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
text:
|
||||
- platform: template
|
||||
name: "test 1 text"
|
||||
id: "it_1"
|
||||
optimistic: true
|
||||
mode: text
|
||||
|
||||
- platform: template
|
||||
name: "test 2 text"
|
||||
id: "it_2"
|
||||
icon: "mdi:text"
|
||||
optimistic: true
|
||||
min_length: 5
|
||||
max_length: 255
|
||||
internal: false
|
||||
initial_value: "Welcome ESPHOME"
|
||||
restore_value: true
|
||||
mode: text
|
||||
|
||||
- platform: template
|
||||
name: "test 3 key"
|
||||
id: "it_3"
|
||||
icon: "mdi:text"
|
||||
mode: "password"
|
||||
optimistic: true
|
||||
internal: true
|
||||
max_length: 255
|
|
@ -203,3 +203,19 @@ light:
|
|||
from: 20
|
||||
to: 25
|
||||
- single_light_id: ${roomname}_lights
|
||||
|
||||
canbus:
|
||||
- platform: esp32_can
|
||||
id: esp32_internal_can
|
||||
rx_pin: GPIO04
|
||||
tx_pin: GPIO05
|
||||
can_id: 4
|
||||
bit_rate: 50kbps
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Canbus Actions
|
||||
on_press:
|
||||
- canbus.send: "abc"
|
||||
- canbus.send: [0, 1, 2]
|
||||
- canbus.send: !lambda return {0, 1, 2};
|
||||
|
|
|
@ -1457,6 +1457,12 @@ sensor:
|
|||
div_ratio: 100
|
||||
unit_of_measurement: °C
|
||||
device_class: temperature
|
||||
- platform: iaqcore
|
||||
i2c_id: i2c_bus
|
||||
co2:
|
||||
name: "iAQ Core CO2 Sensor"
|
||||
tvoc:
|
||||
name: "iAQ Core TVOC Sensor"
|
||||
- platform: tmp1075
|
||||
name: "Temperature TMP1075"
|
||||
update_interval: 10s
|
||||
|
@ -2315,6 +2321,16 @@ climate:
|
|||
heat_mode: extended
|
||||
- platform: whynter
|
||||
name: Whynter
|
||||
- platform: noblex
|
||||
name: AC Living
|
||||
id: noblex_ac
|
||||
sensor: ${sensorname}_sensor
|
||||
receiver_id: rcvr
|
||||
- platform: gree
|
||||
name: GREE
|
||||
model: generic
|
||||
- platform: zhlt01
|
||||
name: ZH/LT-01 Climate
|
||||
|
||||
script:
|
||||
- id: climate_custom
|
||||
|
@ -2537,7 +2553,22 @@ switch:
|
|||
name: Haier
|
||||
turn_on_action:
|
||||
remote_transmitter.transmit_haier:
|
||||
code: [0xA6, 0xDA, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x05]
|
||||
code:
|
||||
[
|
||||
0xA6,
|
||||
0xDA,
|
||||
0x00,
|
||||
0x00,
|
||||
0x40,
|
||||
0x40,
|
||||
0x00,
|
||||
0x80,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
]
|
||||
- platform: template
|
||||
name: Living Room Lights
|
||||
id: livingroom_lights
|
||||
|
@ -2950,6 +2981,7 @@ tm1651:
|
|||
dio_pin: GPIO23
|
||||
|
||||
remote_receiver:
|
||||
id: rcvr
|
||||
pin: GPIO32
|
||||
dump: all
|
||||
on_coolix:
|
||||
|
@ -3236,6 +3268,10 @@ text_sensor:
|
|||
canbus_id: mcp2515_can
|
||||
can_id: 23
|
||||
data: [0x10, 0x20, 0x30]
|
||||
- canbus.send:
|
||||
canbus_id: mcp2515_can
|
||||
can_id: 23
|
||||
data: !lambda return {0x10, 0x20, 0x30};
|
||||
- canbus.send:
|
||||
canbus_id: esp32_internal_can
|
||||
can_id: 23
|
||||
|
|
|
@ -49,3 +49,8 @@ sensor:
|
|||
- platform: wireguard
|
||||
latest_handshake:
|
||||
name: 'WireGuard Latest Handshake'
|
||||
|
||||
text_sensor:
|
||||
- platform: wireguard
|
||||
address:
|
||||
name: 'WireGuard Address'
|
||||
|
|
|
@ -795,7 +795,23 @@ switch:
|
|||
value: !lambda |-
|
||||
return {0x13, 0x37};
|
||||
|
||||
|
||||
esp32_ble_server:
|
||||
id: ble
|
||||
manufacturer_data: [0x72, 0x4, 0x00, 0x23]
|
||||
|
||||
text:
|
||||
- platform: template
|
||||
name: My Text
|
||||
id: my_text
|
||||
min_length: 0
|
||||
max_length: 20
|
||||
mode: text
|
||||
pattern: "[a-z]+"
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
initial_value: "Hello World"
|
||||
- platform: copy
|
||||
name: My Text Copy
|
||||
id: my_text_copy
|
||||
source_id: my_text
|
||||
mode: password
|
||||
|
|
Loading…
Reference in a new issue