merge dev

This commit is contained in:
j0ta29 2023-10-25 21:17:18 +00:00
commit e6261e1050
82 changed files with 2999 additions and 27 deletions

View file

@ -19,6 +19,8 @@
- [ ] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx
## Example entry for `config.yaml`:
<!--

View file

@ -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]

View file

@ -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

View file

@ -39,6 +39,7 @@ service APIConnection {
rpc camera_image (CameraImageRequest) returns (void) {}
rpc climate_command (ClimateCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {}
rpc 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;
}

View file

@ -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_)

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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()) {

View file

@ -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

View file

@ -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())

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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;

View 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))

View 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

View 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

View file

View 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)

View 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

View 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

View file

View 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

View 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

View 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)

View file

@ -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)

View 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

View 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

View file

@ -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
}

View file

@ -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)

View file

@ -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);

View file

@ -0,0 +1 @@
CODEOWNERS = ["@AGalfra"]

View 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)

View 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

View 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

View file

@ -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"

View file

@ -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]:

View file

@ -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:

View 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]
)

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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");

View file

@ -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

View file

@ -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);

View file

@ -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>'.

View 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))

View file

@ -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_() {

View file

@ -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;

View file

View 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)

View 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

View 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

View file

@ -182,6 +182,7 @@ RESERVED_IDS = [
"struct",
"switch",
"template",
"text",
"this",
"thread_local",
"throw",

View file

@ -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"

View file

@ -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

View file

@ -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()) {

View file

@ -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

View file

@ -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()) {

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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",

View 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

View 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

View file

@ -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};

View file

@ -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

View file

@ -49,3 +49,8 @@ sensor:
- platform: wireguard
latest_handshake:
name: 'WireGuard Latest Handshake'
text_sensor:
- platform: wireguard
address:
name: 'WireGuard Address'

View file

@ -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