Merge branch 'dev' into http_request_check_update

This commit is contained in:
dentra 2024-09-30 16:06:35 +03:00 committed by GitHub
commit cd552e11d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
128 changed files with 3859 additions and 318 deletions

View file

@ -36,7 +36,7 @@ jobs:
python ./script/sync-device_class.py
- name: Commit changes
uses: peter-evans/create-pull-request@v7.0.2
uses: peter-evans/create-pull-request@v7.0.5
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>

View file

@ -86,7 +86,7 @@ esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @OttoWinter
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @jesterret
esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz
@ -152,6 +152,7 @@ esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb
@ -166,6 +167,7 @@ esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
@ -289,6 +291,7 @@ esphome/components/noblex/* @AGalfra
esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931
@ -396,6 +399,7 @@ esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core
esphome/components/t6615/* @tylermenezes
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax

View file

@ -33,7 +33,7 @@ RUN \
python3-venv=3.11.2-1+b1 \
python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1 \
git=1:2.39.2-1.1 \
git=1:2.39.5-0+deb12u1 \
curl=7.88.1-10+deb12u7 \
openssh-client=1:9.2p1-2+deb12u3 \
python3-cffi=1.15.1-5 \

View file

@ -62,6 +62,8 @@ service APIConnection {
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {}
rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
}
@ -1118,6 +1120,7 @@ message MediaPlayerSupportedFormat {
uint32 sample_rate = 2;
uint32 num_channels = 3;
MediaPlayerFormatPurpose purpose = 4;
uint32 sample_bytes = 5;
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
@ -1570,6 +1573,36 @@ message VoiceAssistantAnnounceFinished {
bool success = 1;
}
message VoiceAssistantWakeWord {
string id = 1;
string wake_word = 2;
repeated string trained_languages = 3;
}
message VoiceAssistantConfigurationRequest {
option (id) = 121;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
}
message VoiceAssistantConfigurationResponse {
option (id) = 122;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated VoiceAssistantWakeWord available_wake_words = 1;
repeated string active_wake_words = 2;
uint32 max_active_wake_words = 3;
}
message VoiceAssistantSetConfiguration {
option (id) = 123;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated string active_wake_words = 1;
}
// ==================== ALARM CONTROL PANEL ====================
enum AlarmControlPanelState {
ALARM_STATE_DISARMED = 0;

View file

@ -1032,6 +1032,7 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play
media_format.sample_rate = supported_format.sample_rate;
media_format.num_channels = supported_format.num_channels;
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
media_format.sample_bytes = supported_format.sample_bytes;
msg.supported_formats.push_back(media_format);
}
@ -1223,6 +1224,42 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
}
}
VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp;
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return resp;
}
auto &config = voice_assistant::global_voice_assistant->get_configuration();
for (auto &wake_word : config.available_wake_words) {
VoiceAssistantWakeWord resp_wake_word;
resp_wake_word.id = wake_word.id;
resp_wake_word.wake_word = wake_word.wake_word;
for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang);
}
resp.available_wake_words.push_back(std::move(resp_wake_word));
}
for (auto &wake_word_id : config.active_wake_words) {
resp.active_wake_words.push_back(wake_word_id);
}
resp.max_active_wake_words = config.max_active_wake_words;
}
return resp;
}
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
}
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL

View file

@ -152,6 +152,9 @@ class APIConnection : public APIServerConnection {
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) override;
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL

View file

@ -5149,6 +5149,10 @@ bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt va
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
return true;
}
case 5: {
this->sample_bytes = value.as_uint32();
return true;
}
default:
return false;
}
@ -5168,6 +5172,7 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->sample_rate);
buffer.encode_uint32(3, this->num_channels);
buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose);
buffer.encode_uint32(5, this->sample_bytes);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
@ -5190,6 +5195,11 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
out.append(" purpose: ");
out.append(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose));
out.append("\n");
out.append(" sample_bytes: ");
sprintf(buffer, "%" PRIu32, this->sample_bytes);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -7114,6 +7124,140 @@ void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->id = value.as_string();
return true;
}
case 2: {
this->wake_word = value.as_string();
return true;
}
case 3: {
this->trained_languages.push_back(value.as_string());
return true;
}
default:
return false;
}
}
void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->id);
buffer.encode_string(2, this->wake_word);
for (auto &it : this->trained_languages) {
buffer.encode_string(3, it, true);
}
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantWakeWord::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantWakeWord {\n");
out.append(" id: ");
out.append("'").append(this->id).append("'");
out.append("\n");
out.append(" wake_word: ");
out.append("'").append(this->wake_word).append("'");
out.append("\n");
for (const auto &it : this->trained_languages) {
out.append(" trained_languages: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append("}");
}
#endif
void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
out.append("VoiceAssistantConfigurationRequest {}");
}
#endif
bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 3: {
this->max_active_wake_words = value.as_uint32();
return true;
}
default:
return false;
}
}
bool VoiceAssistantConfigurationResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->available_wake_words.push_back(value.as_message<VoiceAssistantWakeWord>());
return true;
}
case 2: {
this->active_wake_words.push_back(value.as_string());
return true;
}
default:
return false;
}
}
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->available_wake_words) {
buffer.encode_message<VoiceAssistantWakeWord>(1, it, true);
}
for (auto &it : this->active_wake_words) {
buffer.encode_string(2, it, true);
}
buffer.encode_uint32(3, this->max_active_wake_words);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantConfigurationResponse {\n");
for (const auto &it : this->available_wake_words) {
out.append(" available_wake_words: ");
it.dump_to(out);
out.append("\n");
}
for (const auto &it : this->active_wake_words) {
out.append(" active_wake_words: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append(" max_active_wake_words: ");
sprintf(buffer, "%" PRIu32, this->max_active_wake_words);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->active_wake_words.push_back(value.as_string());
return true;
}
default:
return false;
}
}
void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->active_wake_words) {
buffer.encode_string(1, it, true);
}
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantSetConfiguration::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantSetConfiguration {\n");
for (const auto &it : this->active_wake_words) {
out.append(" active_wake_words: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append("}");
}
#endif
bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {

View file

@ -1277,6 +1277,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
uint32_t sample_rate{0};
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
uint32_t sample_bytes{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1848,6 +1849,53 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantWakeWord : public ProtoMessage {
public:
std::string id{};
std::string wake_word{};
std::vector<std::string> trained_languages{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class VoiceAssistantConfigurationRequest : public ProtoMessage {
public:
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class VoiceAssistantConfigurationResponse : public ProtoMessage {
public:
std::vector<VoiceAssistantWakeWord> available_wake_words{};
std::vector<std::string> active_wake_words{};
uint32_t max_active_wake_words{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantSetConfiguration : public ProtoMessage {
public:
std::vector<std::string> active_wake_words{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
public:
std::string object_id{};

View file

@ -496,6 +496,19 @@ bool APIServerConnectionBase::send_voice_assistant_announce_finished(const Voice
return this->send_message_<VoiceAssistantAnnounceFinished>(msg, 120);
}
#endif
#ifdef USE_VOICE_ASSISTANT
#endif
#ifdef USE_VOICE_ASSISTANT
bool APIServerConnectionBase::send_voice_assistant_configuration_response(
const VoiceAssistantConfigurationResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_voice_assistant_configuration_response: %s", msg.dump().c_str());
#endif
return this->send_message_<VoiceAssistantConfigurationResponse>(msg, 122);
}
#endif
#ifdef USE_VOICE_ASSISTANT
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
const ListEntitiesAlarmControlPanelResponse &msg) {
@ -1156,6 +1169,28 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_announce_request(msg);
#endif
break;
}
case 121: {
#ifdef USE_VOICE_ASSISTANT
VoiceAssistantConfigurationRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_configuration_request(msg);
#endif
break;
}
case 123: {
#ifdef USE_VOICE_ASSISTANT
VoiceAssistantSetConfiguration msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_set_configuration(msg);
#endif
break;
}
@ -1646,6 +1681,35 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
this->subscribe_voice_assistant(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg);
if (!this->send_voice_assistant_configuration_response(ret)) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->voice_assistant_set_configuration(msg);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
if (!this->is_connection_setup()) {

View file

@ -253,6 +253,15 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_VOICE_ASSISTANT
bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg);
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
bool send_voice_assistant_configuration_response(const VoiceAssistantConfigurationResponse &msg);
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
#endif
@ -425,6 +434,13 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VOICE_ASSISTANT
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
#endif
@ -526,6 +542,12 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
#endif

View file

@ -145,8 +145,9 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
),
)
async def reset_energy_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
async def to_code(config):

View file

@ -1,18 +1,20 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.i2c import I2CBus
import esphome.config_validation as cv
from esphome.const import (
CONF_I2C_ID,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
CONF_RESTORE_VALUE,
)
CODEOWNERS = ["@jesterret"]
CODEOWNERS = ["@jesterret", "@clydebarrow"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
ch422g_ns = cg.esphome_ns.namespace("ch422g")
@ -23,29 +25,36 @@ CH422GGPIOPin = ch422g_ns.class_(
)
CONF_CH422G = "ch422g"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(CH422GComponent),
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x24))
)
# Note that no address is configurable - each register in the CH422G has a dedicated i2c address
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(CH422GComponent),
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE]))
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
# Can't use register_i2c_device because there is no CONF_ADDRESS
parent = await cg.get_variable(config[CONF_I2C_ID])
cg.add(var.set_i2c_bus(parent))
# This is used as a final validation step so that modes have been fully transformed.
def pin_mode_check(pin_config, _):
if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8:
raise cv.Invalid("CH422G only supports input on pins 0-7")
if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8:
raise cv.Invalid("CH422G only supports open drain output on pins 8-11")
CH422G_PIN_SCHEMA = pins.gpio_base_schema(
CH422GGPIOPin,
cv.int_range(min=0, max=7),
modes=[CONF_INPUT, CONF_OUTPUT],
cv.int_range(min=0, max=11),
modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN],
).extend(
{
cv.Required(CONF_CH422G): cv.use_id(CH422GComponent),
@ -53,7 +62,7 @@ CH422G_PIN_SCHEMA = pins.gpio_base_schema(
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA, pin_mode_check)
async def ch422g_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_CH422G])

View file

@ -4,33 +4,33 @@
namespace esphome {
namespace ch422g {
const uint8_t CH422G_REG_IN = 0x26;
const uint8_t CH422G_REG_OUT = 0x38;
const uint8_t OUT_REG_DEFAULT_VAL = 0xdf;
static const uint8_t CH422G_REG_MODE = 0x24;
static const uint8_t CH422G_MODE_OUTPUT = 0x01; // enables output mode on 0-7
static const uint8_t CH422G_MODE_OPEN_DRAIN = 0x04; // enables open drain mode on 8-11
static const uint8_t CH422G_REG_IN = 0x26; // read reg for input bits
static const uint8_t CH422G_REG_OUT = 0x38; // write reg for output bits 0-7
static const uint8_t CH422G_REG_OUT_UPPER = 0x23; // write reg for output bits 8-11
static const char *const TAG = "ch422g";
void CH422GComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up CH422G...");
// Test to see if device exists
if (!this->read_inputs_()) {
// set outputs before mode
this->write_outputs_();
// Set mode and check for errors
if (!this->set_mode_(this->mode_value_) || !this->read_inputs_()) {
ESP_LOGE(TAG, "CH422G not detected at 0x%02X", this->address_);
this->mark_failed();
return;
}
// restore defaults over whatever got saved on last boot
if (!this->restore_value_) {
this->write_output_(OUT_REG_DEFAULT_VAL);
}
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
}
void CH422GComponent::loop() {
// Clear all the previously read flags.
this->pin_read_cache_ = 0x00;
this->pin_read_flags_ = 0x00;
}
void CH422GComponent::dump_config() {
@ -41,82 +41,99 @@ void CH422GComponent::dump_config() {
}
}
// ch422g doesn't have any flag support (needs docs?)
void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) {}
void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
if (pin < 8) {
if (flags & gpio::FLAG_OUTPUT) {
this->mode_value_ |= CH422G_MODE_OUTPUT;
}
} else {
if (flags & gpio::FLAG_OPEN_DRAIN) {
this->mode_value_ |= CH422G_MODE_OPEN_DRAIN;
}
}
}
bool CH422GComponent::digital_read(uint8_t pin) {
if (this->pin_read_cache_ == 0 || this->pin_read_cache_ & (1 << pin)) {
if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) {
// Read values on first access or in case it's being read again in the same loop
this->read_inputs_();
}
this->pin_read_cache_ |= (1 << pin);
return this->state_mask_ & (1 << pin);
this->pin_read_flags_ |= (1 << pin);
return (this->input_bits_ & (1 << pin)) != 0;
}
void CH422GComponent::digital_write(uint8_t pin, bool value) {
if (value) {
this->write_output_(this->state_mask_ | (1 << pin));
this->output_bits_ |= (1 << pin);
} else {
this->write_output_(this->state_mask_ & ~(1 << pin));
this->output_bits_ &= ~(1 << pin);
}
this->write_outputs_();
}
bool CH422GComponent::read_inputs_() {
if (this->is_failed()) {
return false;
}
uint8_t temp = 0;
if ((this->last_error_ = this->read(&temp, 1)) != esphome::i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("read_inputs_(): I2C I/O error: %d", (int) this->last_error_).c_str());
return false;
uint8_t result;
// reading inputs requires the chip to be in input mode, possibly temporarily.
if (this->mode_value_ & CH422G_MODE_OUTPUT) {
this->set_mode_(this->mode_value_ & ~CH422G_MODE_OUTPUT);
result = this->read_reg_(CH422G_REG_IN);
this->set_mode_(this->mode_value_);
} else {
result = this->read_reg_(CH422G_REG_IN);
}
uint8_t output = 0;
if ((this->last_error_ = this->bus_->read(CH422G_REG_IN, &output, 1)) != esphome::i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("read_inputs_(): I2C I/O error: %d", (int) this->last_error_).c_str());
return false;
}
this->state_mask_ = output;
this->input_bits_ = result;
this->status_clear_warning();
return true;
}
bool CH422GComponent::write_output_(uint8_t value) {
const uint8_t temp = 1;
if ((this->last_error_ = this->write(&temp, 1, false)) != esphome::i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("write_output_(): I2C I/O error: %d", (int) this->last_error_).c_str());
// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address.
bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
auto err = this->bus_->write(reg, &value, 1);
if (err != i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str());
return false;
}
uint8_t write_mask = value;
if ((this->last_error_ = this->bus_->write(CH422G_REG_OUT, &write_mask, 1)) != esphome::i2c::ERROR_OK) {
this->status_set_warning(
str_sprintf("write_output_(): I2C I/O error: %d for write_mask: %d", (int) this->last_error_, (int) write_mask)
.c_str());
return false;
}
this->state_mask_ = value;
this->status_clear_warning();
return true;
}
uint8_t CH422GComponent::read_reg_(uint8_t reg) {
uint8_t value;
auto err = this->bus_->read(reg, &value, 1);
if (err != i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str());
return 0;
}
this->status_clear_warning();
return value;
}
bool CH422GComponent::set_mode_(uint8_t mode) { return this->write_reg_(CH422G_REG_MODE, mode); }
bool CH422GComponent::write_outputs_() {
return this->write_reg_(CH422G_REG_OUT, static_cast<uint8_t>(this->output_bits_)) &&
this->write_reg_(CH422G_REG_OUT_UPPER, static_cast<uint8_t>(this->output_bits_ >> 8));
}
float CH422GComponent::get_setup_priority() const { return setup_priority::IO; }
// Run our loop() method very early in the loop, so that we cache read values
// before other components call our digital_read() method.
float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI
void CH422GGPIOPin::setup() { pin_mode(flags_); }
void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
std::string CH422GGPIOPin::dump_summary() const { return str_sprintf("EXIO%u via CH422G", pin_); }
void CH422GGPIOPin::set_flags(gpio::Flags flags) {
flags_ = flags;
this->parent_->pin_mode(this->pin_, flags);
}
} // namespace ch422g
} // namespace esphome

View file

@ -23,32 +23,30 @@ class CH422GComponent : public Component, public i2c::I2CDevice {
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
float get_loop_priority() const override;
void dump_config() override;
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
protected:
bool write_reg_(uint8_t reg, uint8_t value);
uint8_t read_reg_(uint8_t reg);
bool set_mode_(uint8_t mode);
bool read_inputs_();
bool write_output_(uint8_t value);
bool write_outputs_();
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint8_t state_mask_{0x00};
uint16_t output_bits_{0x00};
/// Flags to check if read previously during this loop
uint8_t pin_read_cache_ = {0x00};
/// Storage for last I2C error seen
esphome::i2c::ErrorCode last_error_;
/// Whether we want to override stored values on expander
bool restore_value_{false};
uint8_t pin_read_flags_ = {0x00};
/// Copy of last read values
uint8_t input_bits_ = {0x00};
/// Copy of the mode value
uint8_t mode_value_{};
};
/// Helper class to expose a CH422G pin as an internal input GPIO pin.
/// Helper class to expose a CH422G pin as a GPIO pin.
class CH422GGPIOPin : public GPIOPin {
public:
void setup() override;
void setup() override{};
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
@ -57,13 +55,13 @@ class CH422GGPIOPin : public GPIOPin {
void set_parent(CH422GComponent *parent) { parent_ = parent; }
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags) { flags_ = flags; }
void set_flags(gpio::Flags flags);
protected:
CH422GComponent *parent_;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
CH422GComponent *parent_{};
uint8_t pin_{};
bool inverted_{};
gpio::Flags flags_{};
};
} // namespace ch422g

View file

@ -147,6 +147,7 @@ void CSE7766Component::parse_data_() {
float power = 0.0f;
if (power_cycle_exceeds_range) {
// Datasheet: power cycle exceeding range means active power is 0
have_power = true;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
@ -178,6 +179,15 @@ void CSE7766Component::parse_data_() {
if (this->apparent_power_sensor_ != nullptr) {
this->apparent_power_sensor_->publish_state(apparent_power);
}
if (have_power && this->reactive_power_sensor_ != nullptr) {
const float reactive_power = apparent_power - power;
if (reactive_power < 0.0f) {
ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power);
this->reactive_power_sensor_->publish_state(0.0f);
} else {
this->reactive_power_sensor_->publish_state(reactive_power);
}
}
if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) {
float pf = NAN;
if (apparent_power > 0) {
@ -232,6 +242,7 @@ void CSE7766Component::dump_config() {
LOG_SENSOR(" ", "Power", this->power_sensor_);
LOG_SENSOR(" ", "Energy", this->energy_sensor_);
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
this->check_uart_settings(4800);
}

View file

@ -16,6 +16,9 @@ class CSE7766Component : public Component, public uart::UARTDevice {
void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) {
apparent_power_sensor_ = apparent_power_sensor;
}
void set_reactive_power_sensor(sensor::Sensor *reactive_power_sensor) {
reactive_power_sensor_ = reactive_power_sensor;
}
void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; }
void loop() override;
@ -35,6 +38,7 @@ class CSE7766Component : public Component, public uart::UARTDevice {
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *energy_sensor_{nullptr};
sensor::Sensor *apparent_power_sensor_{nullptr};
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
uint32_t cf_pulses_total_{0};
uint16_t cf_pulses_last_{0};

View file

@ -8,18 +8,21 @@ from esphome.const import (
CONF_ID,
CONF_POWER,
CONF_POWER_FACTOR,
CONF_REACTIVE_POWER,
CONF_VOLTAGE,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
UNIT_WATT_HOURS,
)
@ -62,6 +65,12 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
accuracy_decimals=1,
device_class=DEVICE_CLASS_REACTIVE_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
@ -94,6 +103,9 @@ async def to_code(config):
if apparent_power_config := config.get(CONF_APPARENT_POWER):
sens = await sensor.new_sensor(apparent_power_config)
cg.add(var.set_apparent_power_sensor(sens))
if reactive_power_config := config.get(CONF_REACTIVE_POWER):
sens = await sensor.new_sensor(reactive_power_config)
cg.add(var.set_reactive_power_sensor(sens))
if power_factor_config := config.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(sens))

View file

@ -26,7 +26,6 @@ from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@rfdarter", "@jesserockz"]
DEPENDENCIES = ["time"]
IS_PLATFORM_COMPONENT = True
@ -62,20 +61,28 @@ DATETIME_MODES = [
]
_DATETIME_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
def _validate_time_present(config):
config = config.copy()
if CONF_ON_TIME in config and CONF_TIME_ID not in config:
time_id = cv.use_id(time.RealTimeClock)(None)
config[CONF_TIME_ID] = time_id
return config
_DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
web_server.WEBSERVER_SORTING_SCHEMA,
cv.MQTT_COMMAND_COMPONENT_SCHEMA,
cv.Schema(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger),
}
),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
)
),
).add_extra(_validate_time_present)
def date_schema(class_: MockObjClass) -> cv.Schema:
@ -138,8 +145,9 @@ async def setup_datetime_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf)
rtc = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_rtc(rtc))
if CONF_TIME_ID in config:
rtc = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_rtc(rtc))
for conf in config.get(CONF_ON_TIME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])

View file

@ -4,8 +4,9 @@
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/time.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
namespace esphome {
namespace datetime {
@ -19,23 +20,29 @@ class DateTimeBase : public EntityBase {
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
#ifdef USE_TIME
void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
time::RealTimeClock *get_rtc() const { return this->rtc_; }
#endif
protected:
CallbackManager<void()> state_callback_;
#ifdef USE_TIME
time::RealTimeClock *rtc_;
#endif
bool has_state_{false};
};
#ifdef USE_TIME
class DateTimeStateTrigger : public Trigger<ESPTime> {
public:
explicit DateTimeStateTrigger(DateTimeBase *parent) {
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
}
};
#endif
} // namespace datetime
} // namespace esphome

View file

@ -192,6 +192,7 @@ void DateTimeEntityRestoreState::apply(DateTimeEntity *time) {
time->publish_state();
}
#ifdef USE_TIME
static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider
// there has been a drastic time synchronization
@ -245,6 +246,7 @@ bool OnDateTimeTrigger::matches_(const ESPTime &time) const {
time.day_of_month == this->parent_->day && time.hour == this->parent_->hour &&
time.minute == this->parent_->minute && time.second == this->parent_->second;
}
#endif
} // namespace datetime
} // namespace esphome

View file

@ -134,6 +134,7 @@ template<typename... Ts> class DateTimeSetAction : public Action<Ts...>, public
}
};
#ifdef USE_TIME
class OnDateTimeTrigger : public Trigger<>, public Component, public Parented<DateTimeEntity> {
public:
void loop() override;
@ -143,6 +144,7 @@ class OnDateTimeTrigger : public Trigger<>, public Component, public Parented<Da
optional<ESPTime> last_check_;
};
#endif
} // namespace datetime
} // namespace esphome

View file

@ -94,6 +94,7 @@ void TimeEntityRestoreState::apply(TimeEntity *time) {
time->publish_state();
}
#ifdef USE_TIME
static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider
// there has been a drastic time synchronization
@ -145,6 +146,7 @@ bool OnTimeTrigger::matches_(const ESPTime &time) const {
return time.is_valid() && time.hour == this->parent_->hour && time.minute == this->parent_->minute &&
time.second == this->parent_->second;
}
#endif
} // namespace datetime
} // namespace esphome

View file

@ -113,6 +113,7 @@ template<typename... Ts> class TimeSetAction : public Action<Ts...>, public Pare
}
};
#ifdef USE_TIME
class OnTimeTrigger : public Trigger<>, public Component, public Parented<TimeEntity> {
public:
void loop() override;
@ -122,6 +123,7 @@ class OnTimeTrigger : public Trigger<>, public Component, public Parented<TimeEn
optional<ESPTime> last_check_;
};
#endif
} // namespace datetime
} // namespace esphome

View file

@ -1,7 +1,8 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor, esp32_ble_server, output
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
AUTO_LOAD = ["esp32_ble_server"]
CODEOWNERS = ["@jesserockz"]
@ -11,13 +12,36 @@ CONF_AUTHORIZED_DURATION = "authorized_duration"
CONF_AUTHORIZER = "authorizer"
CONF_BLE_SERVER_ID = "ble_server_id"
CONF_IDENTIFY_DURATION = "identify_duration"
CONF_ON_PROVISIONED = "on_provisioned"
CONF_ON_PROVISIONING = "on_provisioning"
CONF_ON_START = "on_start"
CONF_ON_STOP = "on_stop"
CONF_STATUS_INDICATOR = "status_indicator"
CONF_WIFI_TIMEOUT = "wifi_timeout"
improv_ns = cg.esphome_ns.namespace("improv")
Error = improv_ns.enum("Error")
State = improv_ns.enum("State")
esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
ESP32ImprovComponent = esp32_improv_ns.class_(
"ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent
)
ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisionedTrigger", automation.Trigger.template()
)
ESP32ImprovProvisioningTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisioningTrigger", automation.Trigger.template()
)
ESP32ImprovStartTrigger = esp32_improv_ns.class_(
"ESP32ImprovStartTrigger", automation.Trigger.template()
)
ESP32ImprovStateTrigger = esp32_improv_ns.class_(
"ESP32ImprovStateTrigger", automation.Trigger.template()
)
ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
"ESP32ImprovStoppedTrigger", automation.Trigger.template()
)
CONFIG_SCHEMA = cv.Schema(
@ -37,6 +61,37 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(
CONF_WIFI_TIMEOUT, default="1min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisionedTrigger
),
}
),
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisioningTrigger
),
}
),
cv.Optional(CONF_ON_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
}
),
cv.Optional(CONF_ON_STOP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStoppedTrigger
),
}
),
}
).extend(cv.COMPONENT_SCHEMA)
@ -63,3 +118,29 @@ async def to_code(config):
if CONF_STATUS_INDICATOR in config:
status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR])
cg.add(var.set_status_indicator(status_indicator))
use_state_callback = False
for conf in config.get(CONF_ON_PROVISIONED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_PROVISIONING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(State, "state"), (Error, "error")], conf
)
use_state_callback = True
for conf in config.get(CONF_ON_STOP, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
if use_state_callback:
cg.add_define("USE_ESP32_IMPROV_STATE_CALLBACK")

View file

@ -0,0 +1,72 @@
#pragma once
#ifdef USE_ESP32
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#include "esp32_improv_component.h"
#include "esphome/core/automation.h"
#include <improv.h>
namespace esphome {
namespace esp32_improv {
class ESP32ImprovProvisionedTrigger : public Trigger<> {
public:
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONED && !parent->is_failed()) {
trigger();
}
});
}
};
class ESP32ImprovProvisioningTrigger : public Trigger<> {
public:
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONING && !parent->is_failed()) {
trigger();
}
});
}
};
class ESP32ImprovStartTrigger : public Trigger<> {
public:
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
!parent->is_failed()) {
trigger();
}
});
}
};
class ESP32ImprovStateTrigger : public Trigger<improv::State, improv::Error> {
public:
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (!parent->is_failed()) {
trigger(state, error);
}
});
}
};
class ESP32ImprovStoppedTrigger : public Trigger<> {
public:
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_STOPPED && !parent->is_failed()) {
trigger();
}
});
}
};
} // namespace esp32_improv
} // namespace esphome
#endif
#endif

View file

@ -68,7 +68,12 @@ void ESP32ImprovComponent::setup_characteristics() {
void ESP32ImprovComponent::loop() {
if (!global_ble_server->is_running()) {
this->state_ = improv::STATE_STOPPED;
if (this->state_ != improv::STATE_STOPPED) {
this->state_ = improv::STATE_STOPPED;
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
this->incoming_data_.clear();
return;
}
@ -217,6 +222,9 @@ void ESP32ImprovComponent::set_state_(improv::State state) {
service_data[7] = 0x00; // Reserved
esp32_ble::global_ble->advertising_set_service_data(service_data);
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
void ESP32ImprovComponent::set_error_(improv::Error error) {
@ -270,7 +278,7 @@ void ESP32ImprovComponent::dump_config() {
void ESP32ImprovComponent::process_incoming_data_() {
uint8_t length = this->incoming_data_[1];
ESP_LOGD(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str());
ESP_LOGV(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str());
if (this->incoming_data_.size() - 3 == length) {
this->set_error_(improv::ERROR_NONE);
improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_);
@ -295,7 +303,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta, false);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
@ -313,7 +321,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
this->incoming_data_.clear();
}
} else if (this->incoming_data_.size() - 2 > length) {
ESP_LOGV(TAG, "Too much data came in, or malformed resetting buffer...");
ESP_LOGV(TAG, "Too much data received or data malformed; resetting buffer...");
this->incoming_data_.clear();
} else {
ESP_LOGV(TAG, "Waiting for split data packets...");
@ -327,7 +335,7 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
if (this->authorizer_ != nullptr)
this->authorized_start_ = millis();
#endif
ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network");
ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
wifi::global_wifi_component->clear_sta();
}

View file

@ -9,6 +9,10 @@
#include "esphome/components/esp32_ble_server/ble_server.h"
#include "esphome/components/wifi/wifi_component.h"
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#include "esphome/core/automation.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
@ -42,6 +46,11 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent {
void stop() override;
bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
void add_on_state_callback(std::function<void(improv::State, improv::Error)> &&callback) {
this->state_callback_.add(std::move(callback));
}
#endif
#ifdef USE_BINARY_SENSOR
void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; }
#endif
@ -54,6 +63,9 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent {
void set_wifi_timeout(uint32_t wifi_timeout) { this->wifi_timeout_ = wifi_timeout; }
uint32_t get_wifi_timeout() const { return this->wifi_timeout_; }
improv::State get_improv_state() const { return this->state_; }
improv::Error get_improv_error_state() const { return this->error_state_; }
protected:
bool should_start_{false};
bool setup_complete_{false};
@ -84,6 +96,9 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent {
improv::State state_{improv::STATE_STOPPED};
improv::Error error_state_{improv::ERROR_NONE};
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
CallbackManager<void(improv::State, improv::Error)> state_callback_{};
#endif
bool status_indicator_state_{false};
void set_status_indicator_state_(bool state);

View file

@ -59,6 +59,7 @@ ETHERNET_TYPES = {
"KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
"KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA,
"W5500": EthernetType.ETHERNET_TYPE_W5500,
"OPENETH": EthernetType.ETHERNET_TYPE_OPENETH,
}
SPI_ETHERNET_TYPES = ["W5500"]
@ -171,6 +172,7 @@ CONFIG_SCHEMA = cv.All(
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5500": SPI_SCHEMA,
"OPENETH": BASE_SCHEMA,
},
upper=True,
),
@ -240,6 +242,9 @@ async def to_code(config):
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True)
elif config[CONF_TYPE] == "OPENETH":
cg.add_define("USE_ETHERNET_OPENETH")
add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True)
else:
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))

View file

@ -120,6 +120,8 @@ void EthernetComponent::setup() {
phy_config.reset_gpio_num = this->reset_pin_;
esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
#elif defined(USE_ETHERNET_OPENETH)
esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config);
#else
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
@ -143,6 +145,13 @@ void EthernetComponent::setup() {
#endif
switch (this->type_) {
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH: {
phy_config.autonego_timeout_ms = 1000;
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
break;
}
#endif
#if CONFIG_ETH_USE_ESP32_EMAC
case ETHERNET_TYPE_LAN8720: {
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
@ -302,6 +311,10 @@ void EthernetComponent::dump_config() {
eth_type = "W5500";
break;
case ETHERNET_TYPE_OPENETH:
eth_type = "OPENETH";
break;
default:
eth_type = "Unknown";
break;

View file

@ -25,6 +25,7 @@ enum EthernetType {
ETHERNET_TYPE_KSZ8081,
ETHERNET_TYPE_KSZ8081RNA,
ETHERNET_TYPE_W5500,
ETHERNET_TYPE_OPENETH,
};
struct ManualIP {

View file

@ -0,0 +1,67 @@
#include "gp2y1010au0f.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace gp2y1010au0f {
static const char *const TAG = "gp2y1010au0f";
static const float MIN_VOLTAGE = 0.0f;
static const float MAX_VOLTAGE = 4.0f;
void GP2Y1010AU0FSensor::dump_config() {
LOG_SENSOR("", "Sharp GP2Y1010AU0F PM2.5 Sensor", this);
ESP_LOGCONFIG(TAG, " Sampling duration: %" PRId32 " ms", this->sample_duration_);
ESP_LOGCONFIG(TAG, " ADC voltage multiplier: %.3f", this->voltage_multiplier_);
LOG_UPDATE_INTERVAL(this);
}
void GP2Y1010AU0FSensor::update() {
is_sampling_ = true;
this->set_timeout("read", this->sample_duration_, [this]() {
this->is_sampling_ = false;
if (this->num_samples_ == 0)
return;
float mean = this->sample_sum_ / float(this->num_samples_);
ESP_LOGD(TAG, "ADC read voltage: %.3f V (mean from %" PRId32 " samples)", mean, this->num_samples_);
// PM2.5 calculation
// ref: https://www.howmuchsnow.com/arduino/airquality/
int16_t pm_2_5_value = 170 * mean;
this->publish_state(pm_2_5_value);
});
// reset readings
this->num_samples_ = 0;
this->sample_sum_ = 0.0f;
}
void GP2Y1010AU0FSensor::loop() {
if (!this->is_sampling_)
return;
// enable the internal IR LED
this->led_output_->turn_on();
// wait for the sensor to stabilize
delayMicroseconds(this->sample_wait_before_);
// perform a single sample
float read_voltage = this->source_->sample();
// disable the internal IR LED
this->led_output_->turn_off();
if (std::isnan(read_voltage))
return;
read_voltage = read_voltage * this->voltage_multiplier_ - this->voltage_offset_;
if (read_voltage < MIN_VOLTAGE || read_voltage > MAX_VOLTAGE)
return;
this->num_samples_++;
this->sample_sum_ += read_voltage;
}
} // namespace gp2y1010au0f
} // namespace esphome

View file

@ -0,0 +1,52 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/components/output/binary_output.h"
namespace esphome {
namespace gp2y1010au0f {
class GP2Y1010AU0FSensor : public sensor::Sensor, public PollingComponent {
public:
void update() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override {
// after the base sensor has been initialized
return setup_priority::DATA - 1.0f;
}
void set_adc_source(voltage_sampler::VoltageSampler *source) { source_ = source; }
void set_voltage_refs(float offset, float multiplier) {
this->voltage_offset_ = offset;
this->voltage_multiplier_ = multiplier;
}
void set_led_output(output::BinaryOutput *output) { led_output_ = output; }
protected:
// duration in ms of the sampling phase
uint32_t sample_duration_ = 100;
// duration in us of the wait before sampling
// ref: https://global.sharp/products/device/lineup/data/pdf/datasheet/gp2y1010au_appl_e.pdf
uint32_t sample_wait_before_ = 280;
// duration in us of the wait after sampling
// it seems no need to delay on purpose since one ADC sampling takes longer than that (300-400 us on ESP8266)
// uint32_t sample_wait_after_ = 40;
// the sampling source to read voltage from
voltage_sampler::VoltageSampler *source_;
// ADC voltage reading offset
float voltage_offset_ = 0.0f;
// ADC voltage reading multiplier
float voltage_multiplier_ = 1.0f;
// the binary output to control the sampling LED
output::BinaryOutput *led_output_;
float sample_sum_ = 0.0f;
uint32_t num_samples_ = 0;
bool is_sampling_ = false;
};
} // namespace gp2y1010au0f
} // namespace esphome

View file

@ -0,0 +1,61 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, voltage_sampler, output
from esphome.const import (
CONF_SENSOR,
CONF_OUTPUT,
DEVICE_CLASS_PM25,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
)
DEPENDENCIES = ["output"]
AUTO_LOAD = ["voltage_sampler"]
CODEOWNERS = ["@zry98"]
CONF_ADC_VOLTAGE_OFFSET = "adc_voltage_offset"
CONF_ADC_VOLTAGE_MULTIPLIER = "adc_voltage_multiplier"
gp2y1010au0f_ns = cg.esphome_ns.namespace("gp2y1010au0f")
GP2Y1010AU0FSensor = gp2y1010au0f_ns.class_(
"GP2Y1010AU0FSensor", sensor.Sensor, cg.PollingComponent
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
GP2Y1010AU0FSensor,
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_CHEMICAL_WEAPON,
)
.extend(
{
cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler),
cv.Optional(CONF_ADC_VOLTAGE_OFFSET, default=0.0): cv.float_,
cv.Optional(CONF_ADC_VOLTAGE_MULTIPLIER, default=1.0): cv.float_,
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
}
)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
# the ADC sensor to read voltage from
adc_sensor = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_adc_source(adc_sensor))
cg.add(
var.set_voltage_refs(
config[CONF_ADC_VOLTAGE_OFFSET], config[CONF_ADC_VOLTAGE_MULTIPLIER]
)
)
# the binary output to control the module's internal IR LED
led_output = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_led_output(led_output))

View file

@ -0,0 +1,38 @@
#pragma once
#include <array>
#include <cstdint>
#include "esphome/core/hal.h"
namespace esphome {
namespace gpio_expander {
/// @brief A class to cache the read state of a GPIO expander.
template<typename T, T N> class CachedGpioExpander {
public:
bool digital_read(T pin) {
if (!this->read_cache_invalidated_[pin]) {
this->read_cache_invalidated_[pin] = true;
return this->digital_read_cache(pin);
}
return this->digital_read_hw(pin);
}
void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); }
protected:
virtual bool digital_read_hw(T pin) = 0;
virtual bool digital_read_cache(T pin) = 0;
virtual void digital_write_hw(T pin, bool value) = 0;
void reset_pin_cache_() {
for (T i = 0; i < N; i++) {
this->read_cache_invalidated_[i] = false;
}
}
std::array<bool, N> read_cache_invalidated_{};
};
} // namespace gpio_expander
} // namespace esphome

View file

@ -114,7 +114,6 @@ SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
}
@ -240,7 +239,9 @@ CONFIG_SCHEMA = cv.All(
): cv.ensure_list(
cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True)
),
cv.Optional(CONF_BEEPER, default=True): cv.boolean,
cv.Optional(CONF_BEEPER): cv.invalid(
f"The {CONF_BEEPER} option is deprecated, use beeper_on/beeper_off actions or beeper switch for a haier platform instead"
),
cv.Optional(
CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
@ -254,7 +255,7 @@ CONFIG_SCHEMA = cv.All(
): cv.int_range(min=PROTOCOL_STATUS_MESSAGE_HEADER_SIZE),
cv.Optional(
CONF_SUPPORTED_PRESETS,
default=["BOOST", "ECO", "SLEEP"], # No AWAY by default
default=["BOOST", "SLEEP"], # No AWAY by default
): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
),

View file

@ -52,8 +52,6 @@ bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::stead
HaierClimateBase::HaierClimateBase()
: haier_protocol_(*this),
protocol_phase_(ProtocolPhases::SENDING_INIT_1),
display_status_(true),
health_mode_(false),
force_send_control_(false),
forced_request_status_(false),
reset_protocol_request_(false),
@ -127,21 +125,34 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() {
}
#endif
bool HaierClimateBase::get_display_state() const { return this->display_status_; }
void HaierClimateBase::set_display_state(bool state) {
if (this->display_status_ != state) {
this->display_status_ = state;
this->force_send_control_ = true;
void HaierClimateBase::save_settings() {
HaierBaseSettings settings{this->get_health_mode(), this->get_display_state()};
if (!this->base_rtc_.save(&settings)) {
ESP_LOGW(TAG, "Failed to save settings");
}
}
bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
bool HaierClimateBase::get_display_state() const {
return (this->display_status_ == SwitchState::ON) || (this->display_status_ == SwitchState::PENDING_ON);
}
void HaierClimateBase::set_display_state(bool state) {
if (state != this->get_display_state()) {
this->display_status_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
this->force_send_control_ = true;
this->save_settings();
}
}
bool HaierClimateBase::get_health_mode() const {
return (this->health_mode_ == SwitchState::ON) || (this->health_mode_ == SwitchState::PENDING_ON);
}
void HaierClimateBase::set_health_mode(bool state) {
if (this->health_mode_ != state) {
this->health_mode_ = state;
if (state != this->get_health_mode()) {
this->health_mode_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
this->force_send_control_ = true;
this->save_settings();
}
}
@ -287,6 +298,14 @@ void HaierClimateBase::loop() {
}
this->process_phase(now);
this->haier_protocol_.loop();
#ifdef USE_SWITCH
if ((this->display_switch_ != nullptr) && (this->display_switch_->state != this->get_display_state())) {
this->display_switch_->publish_state(this->get_display_state());
}
if ((this->health_mode_switch_ != nullptr) && (this->health_mode_switch_->state != this->get_health_mode())) {
this->health_mode_switch_->publish_state(this->get_health_mode());
}
#endif // USE_SWITCH
}
void HaierClimateBase::process_protocol_reset() {
@ -329,6 +348,26 @@ bool HaierClimateBase::prepare_pending_action() {
ClimateTraits HaierClimateBase::traits() { return traits_; }
void HaierClimateBase::initialization() {
constexpr uint32_t restore_settings_version = 0xA77D21EF;
this->base_rtc_ =
global_preferences->make_preference<HaierBaseSettings>(this->get_object_id_hash() ^ restore_settings_version);
HaierBaseSettings recovered;
if (!this->base_rtc_.load(&recovered)) {
recovered = {false, true};
}
this->display_status_ = recovered.display_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
this->health_mode_ = recovered.health_mode ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
#ifdef USE_SWITCH
if (this->display_switch_ != nullptr) {
this->display_switch_->publish_state(this->get_display_state());
}
if (this->health_mode_switch_ != nullptr) {
this->health_mode_switch_->publish_state(this->get_health_mode());
}
#endif
}
void HaierClimateBase::control(const ClimateCall &call) {
ESP_LOGD("Control", "Control call");
if (!this->valid_connection()) {
@ -353,6 +392,22 @@ void HaierClimateBase::control(const ClimateCall &call) {
}
}
#ifdef USE_SWITCH
void HaierClimateBase::set_display_switch(switch_::Switch *sw) {
this->display_switch_ = sw;
if ((this->display_switch_ != nullptr) && (this->valid_connection())) {
this->display_switch_->publish_state(this->get_display_state());
}
}
void HaierClimateBase::set_health_mode_switch(switch_::Switch *sw) {
this->health_mode_switch_ = sw;
if ((this->health_mode_switch_ != nullptr) && (this->valid_connection())) {
this->health_mode_switch_->publish_state(this->get_health_mode());
}
}
#endif
void HaierClimateBase::HvacSettings::reset() {
this->valid = false;
this->mode.reset();

View file

@ -8,6 +8,10 @@
// HaierProtocol
#include <protocol/haier_protocol.h>
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
namespace esphome {
namespace haier {
@ -20,10 +24,24 @@ enum class ActionRequest : uint8_t {
START_STERI_CLEAN = 5, // only hOn
};
struct HaierBaseSettings {
bool health_mode;
bool display_state;
};
class HaierClimateBase : public esphome::Component,
public esphome::climate::Climate,
public esphome::uart::UARTDevice,
public haier_protocol::ProtocolStream {
#ifdef USE_SWITCH
public:
void set_display_switch(switch_::Switch *sw);
void set_health_mode_switch(switch_::Switch *sw);
protected:
switch_::Switch *display_switch_{nullptr};
switch_::Switch *health_mode_switch_{nullptr};
#endif
public:
HaierClimateBase();
HaierClimateBase(const HaierClimateBase &) = delete;
@ -82,7 +100,8 @@ class HaierClimateBase : public esphome::Component,
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming)
virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; // NOLINT(readability-identifier-naming)
virtual void initialization(){};
virtual void save_settings();
virtual void initialization();
virtual bool prepare_pending_action();
virtual void process_protocol_reset();
esphome::climate::ClimateTraits traits() override;
@ -127,13 +146,19 @@ class HaierClimateBase : public esphome::Component,
ActionRequest action;
esphome::optional<haier_protocol::HaierMessage> message;
};
enum class SwitchState {
OFF = 0b00,
ON = 0b01,
PENDING_OFF = 0b10,
PENDING_ON = 0b11,
};
haier_protocol::ProtocolHandler haier_protocol_;
ProtocolPhases protocol_phase_;
esphome::optional<PendingAction> action_request_;
uint8_t fan_mode_speed_;
uint8_t other_modes_fan_speed_;
bool display_status_;
bool health_mode_;
SwitchState display_status_{SwitchState::ON};
SwitchState health_mode_{SwitchState::OFF};
bool force_send_control_;
bool forced_request_status_;
bool reset_protocol_request_;
@ -148,6 +173,7 @@ class HaierClimateBase : public esphome::Component,
std::chrono::steady_clock::time_point last_status_request_; // To request AC status
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
CallbackManager<void(const char *, size_t)> status_message_callback_{};
ESPPreferenceObject base_rtc_;
};
class StatusMessageTrigger : public Trigger<const char *, size_t> {

View file

@ -31,9 +31,32 @@ HonClimate::HonClimate()
HonClimate::~HonClimate() {}
void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
void HonClimate::set_beeper_state(bool state) {
if (state != this->settings_.beeper_state) {
this->settings_.beeper_state = state;
#ifdef USE_SWITCH
this->beeper_switch_->publish_state(state);
#endif
this->hon_rtc_.save(&this->settings_);
}
}
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
bool HonClimate::get_beeper_state() const { return this->settings_.beeper_state; }
void HonClimate::set_quiet_mode_state(bool state) {
if (state != this->get_quiet_mode_state()) {
this->quiet_mode_state_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
this->settings_.quiet_mode_state = state;
#ifdef USE_SWITCH
this->quiet_mode_switch_->publish_state(state);
#endif
this->hon_rtc_.save(&this->settings_);
}
}
bool HonClimate::get_quiet_mode_state() const {
return (this->quiet_mode_state_ == SwitchState::ON) || (this->quiet_mode_state_ == SwitchState::PENDING_ON);
}
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
return this->current_vertical_swing_;
@ -474,16 +497,19 @@ haier_protocol::HaierMessage HonClimate::get_power_message(bool state) {
}
void HonClimate::initialization() {
constexpr uint32_t restore_settings_version = 0xE834D8DCUL;
this->rtc_ = global_preferences->make_preference<HonSettings>(this->get_object_id_hash() ^ restore_settings_version);
HaierClimateBase::initialization();
constexpr uint32_t restore_settings_version = 0x57EB59DDUL;
this->hon_rtc_ =
global_preferences->make_preference<HonSettings>(this->get_object_id_hash() ^ restore_settings_version);
HonSettings recovered;
if (this->rtc_.load(&recovered)) {
if (this->hon_rtc_.load(&recovered)) {
this->settings_ = recovered;
} else {
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER};
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false};
}
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
}
haier_protocol::HaierMessage HonClimate::get_control_message() {
@ -519,8 +545,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN;
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
// Disabling boost and eco mode for Fan only
out_data->quiet_mode = 0;
// Disabling boost for Fan only
out_data->fast_mode = 0;
break;
case CLIMATE_MODE_COOL:
@ -582,47 +607,34 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
}
if (out_data->ac_power == 0) {
// If AC is off - no presets allowed
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_BOOST:
out_data->quiet_mode = 0;
// Boost is not supported in Fan only mode
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_AWAY:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
// 10 degrees allowed only in heat mode
out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
break;
case CLIMATE_PRESET_SLEEP:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 1;
out_data->ten_degree = 0;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
@ -638,10 +650,23 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
this->pending_horizontal_direction_.reset();
}
out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0;
{
// Quiet mode
if ((out_data->ac_power == 0) || (out_data->ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN)) {
// If AC is off or in fan only mode - no quiet mode allowed
out_data->quiet_mode = 0;
} else {
out_data->quiet_mode = this->get_quiet_mode_state() ? 1 : 0;
}
// Clean quiet mode state pending flag
this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01);
}
out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0;
control_out_buffer[4] = 0; // This byte should be cleared before setting values
out_data->display_status = this->display_status_ ? 1 : 0;
out_data->health_mode = this->health_mode_ ? 1 : 0;
out_data->display_status = this->get_display_state() ? 1 : 0;
this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01);
out_data->health_mode = this->get_health_mode() ? 1 : 0;
this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01);
return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, this->real_control_packet_size_);
@ -765,6 +790,22 @@ void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::stri
}
#endif // USE_TEXT_SENSOR
#ifdef USE_SWITCH
void HonClimate::set_beeper_switch(switch_::Switch *sw) {
this->beeper_switch_ = sw;
if (this->beeper_switch_ != nullptr) {
this->beeper_switch_->publish_state(this->get_beeper_state());
}
}
void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) {
this->quiet_mode_switch_ = sw;
if (this->quiet_mode_switch_ != nullptr) {
this->quiet_mode_switch_->publish_state(this->settings_.quiet_mode_state);
}
}
#endif // USE_SWITCH
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
@ -827,9 +868,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
{
// Extra modes/presets
optional<ClimatePreset> old_preset = this->preset;
if (packet.control.quiet_mode != 0) {
this->preset = CLIMATE_PRESET_ECO;
} else if (packet.control.fast_mode != 0) {
if (packet.control.fast_mode != 0) {
this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.sleep_mode != 0) {
this->preset = CLIMATE_PRESET_SLEEP;
@ -883,28 +922,26 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
}
{
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status != 0;
if (disp_status != this->display_status_) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->force_send_control_ = true;
} else {
this->display_status_ = disp_status;
}
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status != 0;
if (disp_status != this->get_display_state()) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->force_send_control_ = true;
} else if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF;
}
}
}
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
// Health mode
if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
bool old_health_mode = this->get_health_mode();
this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF;
should_publish = should_publish || (old_health_mode != this->get_health_mode());
}
{
CleaningState new_cleaning;
@ -958,17 +995,36 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
}
should_publish = should_publish || (old_mode != this->mode);
}
{
// Quiet mode, should be after climate mode
if ((this->mode != CLIMATE_MODE_FAN_ONLY) && (this->mode != CLIMATE_MODE_OFF) &&
((((uint8_t) this->quiet_mode_state_) & 0b10) == 0)) {
// In proper mode and not in pending state
bool new_quiet_mode = packet.control.quiet_mode != 0;
if (new_quiet_mode != this->get_quiet_mode_state()) {
this->quiet_mode_state_ = new_quiet_mode ? SwitchState::ON : SwitchState::OFF;
this->settings_.quiet_mode_state = new_quiet_mode;
this->hon_rtc_.save(&this->settings_);
}
}
}
{
// Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode;
if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) {
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes();
bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end();
bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end();
if (horizontal_swing_supported &&
(packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
if (vertical_swing_supported &&
(packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO)) {
this->swing_mode = CLIMATE_SWING_BOTH;
} else {
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
}
} else {
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
if (vertical_swing_supported &&
(packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO)) {
this->swing_mode = CLIMATE_SWING_VERTICAL;
} else {
this->swing_mode = CLIMATE_SWING_OFF;
@ -985,7 +1041,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
if (save_settings) {
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
this->rtc_.save(&this->settings_);
this->hon_rtc_.save(&this->settings_);
}
should_publish = should_publish || (old_swing_mode != this->swing_mode);
}
@ -1017,7 +1073,7 @@ void HonClimate::fill_control_messages_queue_() {
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::BEEPER_STATUS,
this->beeper_status_ ? ZERO_BUF : ONE_BUF, 2));
this->get_beeper_state() ? ZERO_BUF : ONE_BUF, 2));
}
// Health mode
{
@ -1025,13 +1081,16 @@ void HonClimate::fill_control_messages_queue_() {
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::HEALTH_MODE,
this->health_mode_ ? ONE_BUF : ZERO_BUF, 2));
this->get_health_mode() ? ONE_BUF : ZERO_BUF, 2));
this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01);
}
// Climate mode
ClimateMode climate_mode = this->mode;
bool new_power = this->mode != CLIMATE_MODE_OFF;
uint8_t fan_mode_buf[] = {0x00, 0xFF};
uint8_t quiet_mode_buf[] = {0x00, 0xFF};
if (climate_control.mode.has_value()) {
climate_mode = climate_control.mode.value();
uint8_t buffer[2] = {0x00, 0x00};
switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF:
@ -1076,8 +1135,6 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_; // Auto doesn't work in fan only mode
// Disabling eco mode for Fan only
quiet_mode_buf[1] = 0;
break;
case CLIMATE_MODE_COOL:
new_power = true;
@ -1108,30 +1165,20 @@ void HonClimate::fill_control_messages_queue_() {
uint8_t away_mode_buf[] = {0x00, 0xFF};
if (!new_power) {
// If AC is off - no presets allowed
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_BOOST:
quiet_mode_buf[1] = 0x00;
// Boost is not supported in Fan only mode
fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_AWAY:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00;
break;
@ -1140,8 +1187,18 @@ void HonClimate::fill_control_messages_queue_() {
break;
}
}
{
// Quiet mode
if (new_power && (climate_mode != CLIMATE_MODE_FAN_ONLY) && this->get_quiet_mode_state()) {
quiet_mode_buf[1] = 0x01;
} else {
quiet_mode_buf[1] = 0x00;
}
// Clean quiet mode state pending flag
this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01);
}
auto presets = this->traits_.get_supported_presets();
if ((quiet_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_ECO) != presets.end()))) {
if (quiet_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +

View file

@ -10,6 +10,9 @@
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include "esphome/core/automation.h"
#include "haier_base.h"
#include "hon_packet.h"
@ -28,6 +31,8 @@ enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE
struct HonSettings {
hon_protocol::VerticalSwingMode last_vertiacal_swing;
hon_protocol::HorizontalSwingMode last_horizontal_swing;
bool beeper_state;
bool quiet_mode_state;
};
class HonClimate : public HaierClimateBase {
@ -86,6 +91,15 @@ class HonClimate : public HaierClimateBase {
protected:
void update_sub_text_sensor_(SubTextSensorType type, const std::string &value);
text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr};
#endif
#ifdef USE_SWITCH
public:
void set_beeper_switch(switch_::Switch *sw);
void set_quiet_mode_switch(switch_::Switch *sw);
protected:
switch_::Switch *beeper_switch_{nullptr};
switch_::Switch *quiet_mode_switch_{nullptr};
#endif
public:
HonClimate();
@ -95,6 +109,8 @@ class HonClimate : public HaierClimateBase {
void dump_config() override;
void set_beeper_state(bool state);
bool get_beeper_state() const;
void set_quiet_mode_state(bool state);
bool get_quiet_mode_state() const;
esphome::optional<hon_protocol::VerticalSwingMode> get_vertical_airflow() const;
void set_vertical_airflow(hon_protocol::VerticalSwingMode direction);
esphome::optional<hon_protocol::HorizontalSwingMode> get_horizontal_airflow() const;
@ -153,7 +169,6 @@ class HonClimate : public HaierClimateBase {
bool functions_[5];
};
bool beeper_status_;
CleaningState cleaning_status_;
bool got_valid_outdoor_temp_;
esphome::optional<hon_protocol::VerticalSwingMode> pending_vertical_direction_{};
@ -175,7 +190,8 @@ class HonClimate : public HaierClimateBase {
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
HonSettings settings_;
ESPPreferenceObject rtc_;
ESPPreferenceObject hon_rtc_;
SwitchState quiet_mode_state_{SwitchState::OFF};
};
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {

View file

@ -376,8 +376,10 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
}
}
}
out_data->display_status = this->display_status_ ? 0 : 1;
out_data->health_mode = this->health_mode_ ? 1 : 0;
out_data->display_status = this->get_display_state() ? 0 : 1;
this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01);
out_data->health_mode = this->get_health_mode() ? 1 : 0;
this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01);
return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer,
sizeof(smartair2_protocol::HaierPacketControl));
}
@ -446,28 +448,26 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
}
{
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status == 0;
if (disp_status != this->display_status_) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->force_send_control_ = true;
} else {
this->display_status_ = disp_status;
}
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status == 0;
if (disp_status != this->get_display_state()) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->force_send_control_ = true;
} else if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF;
}
}
}
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
// Health mode
if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
bool old_health_mode = this->get_health_mode();
this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF;
should_publish = should_publish || (old_health_mode != this->get_health_mode());
}
{
// Climate mode

View file

@ -0,0 +1,91 @@
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.components import switch
from esphome.const import (
CONF_BEEPER,
CONF_DISPLAY,
ENTITY_CATEGORY_CONFIG,
)
from ..climate import (
CONF_HAIER_ID,
CONF_PROTOCOL,
HaierClimateBase,
haier_ns,
PROTOCOL_HON,
)
CODEOWNERS = ["@paveldn"]
BeeperSwitch = haier_ns.class_("BeeperSwitch", switch.Switch)
HealthModeSwitch = haier_ns.class_("HealthModeSwitch", switch.Switch)
DisplaySwitch = haier_ns.class_("DisplaySwitch", switch.Switch)
QuietModeSwitch = haier_ns.class_("QuietModeSwitch", switch.Switch)
# Haier switches
CONF_HEALTH_MODE = "health_mode"
CONF_QUIET_MODE = "quiet_mode"
# Additional icons
ICON_LEAF = "mdi:leaf"
ICON_LED_ON = "mdi:led-on"
ICON_VOLUME_HIGH = "mdi:volume-high"
ICON_VOLUME_OFF = "mdi:volume-off"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HaierClimateBase),
cv.Optional(CONF_DISPLAY): switch.switch_schema(
DisplaySwitch,
icon=ICON_LED_ON,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
cv.Optional(CONF_HEALTH_MODE): switch.switch_schema(
HealthModeSwitch,
icon=ICON_LEAF,
default_restore_mode="DISABLED",
),
# Beeper switch is only supported for HonClimate
cv.Optional(CONF_BEEPER): switch.switch_schema(
BeeperSwitch,
icon=ICON_VOLUME_HIGH,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
# Quiet mode is only supported for HonClimate
cv.Optional(CONF_QUIET_MODE): switch.switch_schema(
QuietModeSwitch,
icon=ICON_VOLUME_OFF,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
}
)
def _final_validate(config):
full_config = fv.full_config.get()
for switch_type in [CONF_BEEPER, CONF_QUIET_MODE]:
# Check switches that are only supported for HonClimate
if config.get(switch_type):
climate_path = full_config.get_path_for_id(config[CONF_HAIER_ID])[:-1]
climate_conf = full_config.get_config_for_path(climate_path)
protocol_type = climate_conf.get(CONF_PROTOCOL)
if protocol_type.casefold() != PROTOCOL_HON.casefold():
raise cv.Invalid(
f"{switch_type} switch is only supported for hon climate"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
parent = await cg.get_variable(config[CONF_HAIER_ID])
for switch_type in [CONF_DISPLAY, CONF_HEALTH_MODE, CONF_BEEPER, CONF_QUIET_MODE]:
if conf := config.get(switch_type):
sw_var = await switch.new_switch(conf)
await cg.register_parented(sw_var, parent)
cg.add(getattr(parent, f"set_{switch_type}_switch")(sw_var))

View file

@ -0,0 +1,14 @@
#include "beeper.h"
namespace esphome {
namespace haier {
void BeeperSwitch::write_state(bool state) {
if (this->parent_->get_beeper_state() != state) {
this->parent_->set_beeper_state(state);
}
this->publish_state(state);
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../hon_climate.h"
namespace esphome {
namespace haier {
class BeeperSwitch : public switch_::Switch, public Parented<HonClimate> {
public:
BeeperSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,14 @@
#include "display.h"
namespace esphome {
namespace haier {
void DisplaySwitch::write_state(bool state) {
if (this->parent_->get_display_state() != state) {
this->parent_->set_display_state(state);
}
this->publish_state(state);
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../haier_base.h"
namespace esphome {
namespace haier {
class DisplaySwitch : public switch_::Switch, public Parented<HaierClimateBase> {
public:
DisplaySwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,14 @@
#include "health_mode.h"
namespace esphome {
namespace haier {
void HealthModeSwitch::write_state(bool state) {
if (this->parent_->get_health_mode() != state) {
this->parent_->set_health_mode(state);
}
this->publish_state(state);
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../haier_base.h"
namespace esphome {
namespace haier {
class HealthModeSwitch : public switch_::Switch, public Parented<HaierClimateBase> {
public:
HealthModeSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,14 @@
#include "quiet_mode.h"
namespace esphome {
namespace haier {
void QuietModeSwitch::write_state(bool state) {
if (this->parent_->get_quiet_mode_state() != state) {
this->parent_->set_quiet_mode_state(state);
}
this->publish_state(state);
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../hon_climate.h"
namespace esphome {
namespace haier {
class QuietModeSwitch : public switch_::Switch, public Parented<HonClimate> {
public:
QuietModeSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace haier
} // namespace esphome

View file

@ -25,6 +25,7 @@ I2SAudioSpeaker = i2s_audio_ns.class_(
CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
INTERNAL_DAC_OPTIONS = {
@ -33,6 +34,20 @@ INTERNAL_DAC_OPTIONS = {
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}
i2s_comm_format_t = cg.global_ns.enum("i2s_comm_format_t")
I2C_COMM_FMT_OPTIONS = {
"stand_i2s": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_I2S,
"stand_msb": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MSB,
"stand_pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_SHORT,
"stand_pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_LONG,
"stand_max": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MAX,
"i2s_msb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_MSB,
"i2s_lsb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_LSB,
"pcm": i2s_comm_format_t.I2S_COMM_FORMAT_PCM,
"pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_SHORT,
"pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG,
}
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
@ -77,6 +92,9 @@ CONFIG_SCHEMA = cv.All(
cv.Required(
CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.enum(
I2C_COMM_FMT_OPTIONS, lower=True
),
}
),
},
@ -96,4 +114,5 @@ async def to_code(config):
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT]))
cg.add(var.set_timeout(config[CONF_TIMEOUT]))

View file

@ -83,7 +83,7 @@ void I2SAudioSpeaker::player_task(void *params) {
.sample_rate = this_speaker->sample_rate_,
.bits_per_sample = this_speaker->bits_per_sample_,
.channel_format = this_speaker->channel_,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.communication_format = this_speaker->i2s_comm_fmt_,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = 256,

View file

@ -49,6 +49,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
#if SOC_I2S_SUPPORTS_DAC
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
#endif
void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; }
void start() override;
void stop() override;
@ -76,6 +77,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
#if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
#endif
i2s_comm_format_t i2s_comm_fmt_;
};
} // namespace i2s_audio

View file

@ -53,6 +53,8 @@ MODELS = {
"inkplate_10": InkplateModel.INKPLATE_10,
"inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS,
"inkplate_6_v2": InkplateModel.INKPLATE_6_V2,
"inkplate_5": InkplateModel.INKPLATE_5,
"inkplate_5_v2": InkplateModel.INKPLATE_5_V2,
}
CONFIG_SCHEMA = cv.All(

View file

@ -15,6 +15,8 @@ enum InkplateModel : uint8_t {
INKPLATE_10 = 1,
INKPLATE_6_PLUS = 2,
INKPLATE_6_V2 = 3,
INKPLATE_5 = 4,
INKPLATE_5_V2 = 5,
};
class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice {
@ -29,7 +31,7 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice {
const uint8_t pixelMaskLUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80};
const uint8_t pixelMaskGLUT[2] = {0x0F, 0xF0};
const uint8_t waveform3BitAll[4][8][9] = {// INKPLATE_6
const uint8_t waveform3BitAll[6][8][9] = {// INKPLATE_6
{{0, 1, 1, 0, 0, 1, 1, 0, 0},
{0, 1, 2, 1, 1, 2, 1, 0, 0},
{1, 1, 1, 2, 2, 1, 0, 0, 0},
@ -64,7 +66,25 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice {
{1, 1, 1, 1, 2, 2, 1, 0, 0},
{0, 1, 1, 1, 2, 2, 1, 0, 0},
{0, 0, 0, 0, 1, 1, 2, 0, 0},
{0, 0, 0, 0, 0, 1, 2, 0, 0}}};
{0, 0, 0, 0, 0, 1, 2, 0, 0}},
// INKPLATE_5
{{0, 0, 1, 1, 0, 1, 1, 1, 0},
{0, 1, 1, 1, 1, 2, 0, 1, 0},
{1, 2, 2, 0, 2, 1, 1, 1, 0},
{1, 1, 1, 2, 0, 1, 1, 2, 0},
{0, 1, 1, 1, 2, 0, 1, 2, 0},
{0, 0, 0, 1, 1, 2, 1, 2, 0},
{1, 1, 1, 2, 0, 2, 1, 2, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0}},
// INKPLATE_5_V2
{{0, 0, 1, 1, 2, 1, 1, 1, 0},
{1, 1, 2, 2, 1, 2, 1, 1, 0},
{0, 1, 2, 2, 1, 1, 2, 1, 0},
{0, 0, 1, 1, 1, 1, 1, 2, 0},
{1, 2, 1, 2, 1, 1, 1, 2, 0},
{0, 1, 1, 1, 2, 0, 1, 2, 0},
{1, 1, 1, 2, 2, 2, 1, 2, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0}}};
void set_greyscale(bool greyscale) {
this->greyscale_ = greyscale;
@ -146,6 +166,10 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice {
return 800;
} else if (this->model_ == INKPLATE_10) {
return 1200;
} else if (this->model_ == INKPLATE_5) {
return 960;
} else if (this->model_ == INKPLATE_5_V2) {
return 1280;
} else if (this->model_ == INKPLATE_6_PLUS) {
return 1024;
}
@ -155,6 +179,10 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice {
int get_height_internal() override {
if (this->model_ == INKPLATE_6 || this->model_ == INKPLATE_6_V2) {
return 600;
} else if (this->model_ == INKPLATE_5) {
return 540;
} else if (this->model_ == INKPLATE_5_V2) {
return 720;
} else if (this->model_ == INKPLATE_10) {
return 825;
} else if (this->model_ == INKPLATE_6_PLUS) {

View file

@ -22,9 +22,10 @@ from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid
from .automation import disp_update, focused_widgets, update_to_code
from .defines import add_define
from .defines import CONF_WIDGETS, add_define
from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code
from .gradient import GRADIENT_SCHEMA, gradients_to_code
from .hello_world import get_hello_world
from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent
from .schemas import (
@ -32,7 +33,7 @@ from .schemas import (
FLEX_OBJ_SCHEMA,
GRID_CELL_SCHEMA,
LAYOUT_SCHEMAS,
STYLE_SCHEMA,
STATE_SCHEMA,
WIDGET_TYPES,
any_widget_schema,
container_schema,
@ -292,6 +293,13 @@ def display_schema(config):
return value or [cv.use_id(Display)(config)]
def add_hello_world(config):
if CONF_WIDGETS not in config and CONF_PAGES not in config:
LOGGER.info("No pages or widgets configured, creating default hello_world page")
config[CONF_WIDGETS] = cv.ensure_list(WIDGET_SCHEMA)(get_hello_world())
return config
FINAL_VALIDATE_SCHEMA = final_validation
CONFIG_SCHEMA = (
@ -313,7 +321,7 @@ CONFIG_SCHEMA = (
),
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
.extend(STYLE_SCHEMA)
.extend(STATE_SCHEMA)
.extend(
{
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
@ -349,4 +357,5 @@ CONFIG_SCHEMA = (
}
)
.extend(DISP_BG_SCHEMA)
).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))
.add_extra(add_hello_world)
)

View file

@ -0,0 +1,64 @@
from io import StringIO
from esphome.yaml_util import parse_yaml
CONFIG = """
- obj:
radius: 0
pad_all: 12
bg_color: 0xFFFFFF
height: 100%
width: 100%
widgets:
- spinner:
id: hello_world_spinner_
align: center
indicator:
arc_color: tomato
height: 100
width: 100
spin_time: 2s
arc_length: 60deg
- label:
id: hello_world_label_
text: "Hello World!"
align: center
on_click:
lvgl.spinner.update:
id: hello_world_spinner_
arc_color: springgreen
- checkbox:
pad_all: 8
text: Checkbox
align: top_right
on_click:
lvgl.label.update:
id: hello_world_label_
text: "Checked!"
- button:
pad_all: 8
checkable: true
align: top_left
text_font: montserrat_20
on_click:
lvgl.label.update:
id: hello_world_label_
text: "Clicked!"
widgets:
- label:
text: "Button"
- slider:
width: 80%
align: bottom_mid
on_value:
lvgl.label.update:
id: hello_world_label_
text:
format: "%.0f%%"
args: [x]
"""
def get_hello_world():
with StringIO(CONFIG) as fp:
return parse_yaml("hello_world", fp)

View file

@ -49,17 +49,172 @@ def opacity_validator(value):
opacity = LValidator(opacity_validator, uint32, retmapper=literal)
COLOR_NAMES = {
"aliceblue": 0xF0F8FF,
"antiquewhite": 0xFAEBD7,
"aqua": 0x00FFFF,
"aquamarine": 0x7FFFD4,
"azure": 0xF0FFFF,
"beige": 0xF5F5DC,
"bisque": 0xFFE4C4,
"black": 0x000000,
"blanchedalmond": 0xFFEBCD,
"blue": 0x0000FF,
"blueviolet": 0x8A2BE2,
"brown": 0xA52A2A,
"burlywood": 0xDEB887,
"cadetblue": 0x5F9EA0,
"chartreuse": 0x7FFF00,
"chocolate": 0xD2691E,
"coral": 0xFF7F50,
"cornflowerblue": 0x6495ED,
"cornsilk": 0xFFF8DC,
"crimson": 0xDC143C,
"cyan": 0x00FFFF,
"darkblue": 0x00008B,
"darkcyan": 0x008B8B,
"darkgoldenrod": 0xB8860B,
"darkgray": 0xA9A9A9,
"darkgreen": 0x006400,
"darkgrey": 0xA9A9A9,
"darkkhaki": 0xBDB76B,
"darkmagenta": 0x8B008B,
"darkolivegreen": 0x556B2F,
"darkorange": 0xFF8C00,
"darkorchid": 0x9932CC,
"darkred": 0x8B0000,
"darksalmon": 0xE9967A,
"darkseagreen": 0x8FBC8F,
"darkslateblue": 0x483D8B,
"darkslategray": 0x2F4F4F,
"darkslategrey": 0x2F4F4F,
"darkturquoise": 0x00CED1,
"darkviolet": 0x9400D3,
"deeppink": 0xFF1493,
"deepskyblue": 0x00BFFF,
"dimgray": 0x696969,
"dimgrey": 0x696969,
"dodgerblue": 0x1E90FF,
"firebrick": 0xB22222,
"floralwhite": 0xFFFAF0,
"forestgreen": 0x228B22,
"fuchsia": 0xFF00FF,
"gainsboro": 0xDCDCDC,
"ghostwhite": 0xF8F8FF,
"goldenrod": 0xDAA520,
"gold": 0xFFD700,
"gray": 0x808080,
"green": 0x008000,
"greenyellow": 0xADFF2F,
"grey": 0x808080,
"honeydew": 0xF0FFF0,
"hotpink": 0xFF69B4,
"indianred": 0xCD5C5C,
"indigo": 0x4B0082,
"ivory": 0xFFFFF0,
"khaki": 0xF0E68C,
"lavenderblush": 0xFFF0F5,
"lavender": 0xE6E6FA,
"lawngreen": 0x7CFC00,
"lemonchiffon": 0xFFFACD,
"lightblue": 0xADD8E6,
"lightcoral": 0xF08080,
"lightcyan": 0xE0FFFF,
"lightgoldenrodyellow": 0xFAFAD2,
"lightgray": 0xD3D3D3,
"lightgreen": 0x90EE90,
"lightgrey": 0xD3D3D3,
"lightpink": 0xFFB6C1,
"lightsalmon": 0xFFA07A,
"lightseagreen": 0x20B2AA,
"lightskyblue": 0x87CEFA,
"lightslategray": 0x778899,
"lightslategrey": 0x778899,
"lightsteelblue": 0xB0C4DE,
"lightyellow": 0xFFFFE0,
"lime": 0x00FF00,
"limegreen": 0x32CD32,
"linen": 0xFAF0E6,
"magenta": 0xFF00FF,
"maroon": 0x800000,
"mediumaquamarine": 0x66CDAA,
"mediumblue": 0x0000CD,
"mediumorchid": 0xBA55D3,
"mediumpurple": 0x9370DB,
"mediumseagreen": 0x3CB371,
"mediumslateblue": 0x7B68EE,
"mediumspringgreen": 0x00FA9A,
"mediumturquoise": 0x48D1CC,
"mediumvioletred": 0xC71585,
"midnightblue": 0x191970,
"mintcream": 0xF5FFFA,
"mistyrose": 0xFFE4E1,
"moccasin": 0xFFE4B5,
"navajowhite": 0xFFDEAD,
"navy": 0x000080,
"oldlace": 0xFDF5E6,
"olive": 0x808000,
"olivedrab": 0x6B8E23,
"orange": 0xFFA500,
"orangered": 0xFF4500,
"orchid": 0xDA70D6,
"palegoldenrod": 0xEEE8AA,
"palegreen": 0x98FB98,
"paleturquoise": 0xAFEEEE,
"palevioletred": 0xDB7093,
"papayawhip": 0xFFEFD5,
"peachpuff": 0xFFDAB9,
"peru": 0xCD853F,
"pink": 0xFFC0CB,
"plum": 0xDDA0DD,
"powderblue": 0xB0E0E6,
"purple": 0x800080,
"rebeccapurple": 0x663399,
"red": 0xFF0000,
"rosybrown": 0xBC8F8F,
"royalblue": 0x4169E1,
"saddlebrown": 0x8B4513,
"salmon": 0xFA8072,
"sandybrown": 0xF4A460,
"seagreen": 0x2E8B57,
"seashell": 0xFFF5EE,
"sienna": 0xA0522D,
"silver": 0xC0C0C0,
"skyblue": 0x87CEEB,
"slateblue": 0x6A5ACD,
"slategray": 0x708090,
"slategrey": 0x708090,
"snow": 0xFFFAFA,
"springgreen": 0x00FF7F,
"steelblue": 0x4682B4,
"tan": 0xD2B48C,
"teal": 0x008080,
"thistle": 0xD8BFD8,
"tomato": 0xFF6347,
"turquoise": 0x40E0D0,
"violet": 0xEE82EE,
"wheat": 0xF5DEB3,
"white": 0xFFFFFF,
"whitesmoke": 0xF5F5F5,
"yellow": 0xFFFF00,
"yellowgreen": 0x9ACD32,
}
@schema_extractor("one_of")
def color(value):
if value == SCHEMA_EXTRACT:
return ["hex color value", "color ID"]
return cv.Any(cv.int_, cv.use_id(ColorStruct))(value)
return cv.Any(cv.int_, cv.one_of(*COLOR_NAMES, lower=True), cv.use_id(ColorStruct))(
value
)
def color_retmapper(value):
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
if isinstance(value, str) and value in COLOR_NAMES:
value = COLOR_NAMES[value]
if isinstance(value, int):
return literal(
f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})"

View file

@ -37,6 +37,7 @@ struct MediaPlayerSupportedFormat {
uint32_t sample_rate;
uint32_t num_channels;
MediaPlayerFormatPurpose purpose;
uint32_t sample_bytes;
};
class MediaPlayer;

View file

@ -419,6 +419,13 @@ async def to_code(config):
repo="https://github.com/espressif/esp-tflite-micro",
ref="v1.3.1",
)
# add esp-nn dependency for tflite-micro to work around https://github.com/espressif/esp-nn/issues/17
# ...remove after switching to IDF 5.1.4+
esp32.add_idf_component(
name="esp-nn",
repo="https://github.com/espressif/esp-nn",
ref="v1.1.0",
)
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")

View file

@ -1,27 +1,29 @@
import binascii
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
import esphome.codegen as cg
from esphome.components import modbus
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_NAME,
CONF_LAMBDA,
CONF_NAME,
CONF_OFFSET,
CONF_TRIGGER_ID,
)
from esphome.cpp_helpers import logging
from .const import (
CONF_ALLOW_DUPLICATE_COMMANDS,
CONF_BITMASK,
CONF_BYTE_OFFSET,
CONF_COMMAND_THROTTLE,
CONF_OFFLINE_SKIP_UPDATES,
CONF_CUSTOM_COMMAND,
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_MAX_CMD_RETRIES,
CONF_MODBUS_CONTROLLER_ID,
CONF_OFFLINE_SKIP_UPDATES,
CONF_ON_COMMAND_SENT,
CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE,

View file

@ -1,16 +1,16 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ADDRESS, CONF_ID
from .. import (
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
validate_modbus_register,
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
MODBUS_REGISTER_TYPE,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,

View file

@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
@ -12,14 +12,13 @@ from esphome.const import (
from .. import (
MODBUS_WRITE_REGISTER_TYPE,
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
SENSOR_VALUE_TYPE,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
)
from ..const import (
CONF_BITMASK,
CONF_CUSTOM_COMMAND,

View file

@ -1,20 +1,15 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import output
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_MULTIPLY,
)
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY
from .. import (
modbus_controller_ns,
modbus_calc_properties,
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
SENSOR_VALUE_TYPE,
modbus_calc_properties,
modbus_controller_ns,
)
from ..const import (
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_TYPE,
@ -65,6 +60,7 @@ CONFIG_SCHEMA = cv.typed_schema(
async def to_code(config):
byte_offset, reg_count = modbus_calc_properties(config)
# Binary Output
write_template = None
if config[CONF_REGISTER_TYPE] == "coil":
var = cg.new_Pvariable(
config[CONF_ID],
@ -72,7 +68,7 @@ async def to_code(config):
byte_offset,
)
if CONF_WRITE_LAMBDA in config:
template_ = await cg.process_lambda(
write_template = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusBinaryOutput.operator("ptr"), "item"),
@ -92,7 +88,7 @@ async def to_code(config):
)
cg.add(var.set_write_multiply(config[CONF_MULTIPLY]))
if CONF_WRITE_LAMBDA in config:
template_ = await cg.process_lambda(
write_template = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusFloatOutput.operator("ptr"), "item"),
@ -105,5 +101,5 @@ async def to_code(config):
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
cg.add(var.set_parent(parent))
if CONF_WRITE_LAMBDA in config:
cg.add(var.set_write_template(template_))
if write_template:
cg.add(var.set_write_template(write_template))

View file

@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
from .. import (

View file

@ -1,17 +1,17 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ADDRESS, CONF_ID
from esphome.const import CONF_ID, CONF_ADDRESS
from .. import (
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
validate_modbus_register,
ModbusItemBaseSchema,
SensorItem,
MODBUS_REGISTER_TYPE,
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,

View file

@ -1,17 +1,16 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ADDRESS, CONF_ID
from esphome.const import CONF_ID, CONF_ADDRESS
from .. import (
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
validate_modbus_register,
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
MODBUS_REGISTER_TYPE,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,

View file

@ -1,26 +1,25 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ADDRESS, CONF_ID
from .. import (
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
validate_modbus_register,
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
MODBUS_REGISTER_TYPE,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_RAW_ENCODE,
CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE,
CONF_RESPONSE_SIZE,
CONF_SKIP_UPDATES,
CONF_RAW_ENCODE,
CONF_REGISTER_TYPE,
)
DEPENDENCIES = ["modbus_controller"]

View file

@ -11,6 +11,7 @@ from esphome.const import (
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_CERTIFICATE_AUTHORITY,
CONF_CLEAN_SESSION,
CONF_CLIENT_CERTIFICATE,
CONF_CLIENT_CERTIFICATE_KEY,
CONF_CLIENT_ID,
@ -209,6 +210,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_PORT, default=1883): cv.port,
cv.Optional(CONF_USERNAME, default=""): cv.string,
cv.Optional(CONF_PASSWORD, default=""): cv.string,
cv.Optional(CONF_CLEAN_SESSION, default=False): cv.boolean,
cv.Optional(CONF_CLIENT_ID): cv.string,
cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32_idf=False): cv.All(
cv.boolean, cv.only_with_esp_idf
@ -325,6 +327,7 @@ async def to_code(config):
cg.add(var.set_broker_port(config[CONF_PORT]))
cg.add(var.set_username(config[CONF_USERNAME]))
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_clean_session(config[CONF_CLEAN_SESSION]))
if CONF_CLIENT_ID in config:
cg.add(var.set_client_id(config[CONF_CLIENT_ID]))

View file

@ -147,6 +147,7 @@ void MQTTClientComponent::dump_config() {
this->ip_.str().c_str());
ESP_LOGCONFIG(TAG, " Username: " LOG_SECRET("'%s'"), this->credentials_.username.c_str());
ESP_LOGCONFIG(TAG, " Client ID: " LOG_SECRET("'%s'"), this->credentials_.client_id.c_str());
ESP_LOGCONFIG(TAG, " Clean Session: %s", YESNO(this->credentials_.clean_session));
if (this->is_discovery_ip_enabled()) {
ESP_LOGCONFIG(TAG, " Discovery IP enabled");
}
@ -246,6 +247,7 @@ void MQTTClientComponent::start_connect_() {
this->mqtt_backend_.disconnect();
this->mqtt_backend_.set_client_id(this->credentials_.client_id.c_str());
this->mqtt_backend_.set_clean_session(this->credentials_.clean_session);
const char *username = nullptr;
if (!this->credentials_.username.empty())
username = this->credentials_.username.c_str();

View file

@ -51,6 +51,7 @@ struct MQTTCredentials {
std::string username;
std::string password;
std::string client_id; ///< The client ID. Will automatically be truncated to 23 characters.
bool clean_session; ///< Whether the session will be cleaned or remembered between connects.
};
/// Simple data struct for Home Assistant component availability.
@ -254,6 +255,7 @@ class MQTTClientComponent : public Component {
void set_username(const std::string &username) { this->credentials_.username = username; }
void set_password(const std::string &password) { this->credentials_.password = password; }
void set_client_id(const std::string &client_id) { this->credentials_.client_id = client_id; }
void set_clean_session(const bool &clean_session) { this->credentials_.clean_session = clean_session; }
void set_on_connect(mqtt_on_connect_callback_t &&callback);
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);

View file

@ -27,6 +27,7 @@ CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
CONF_FONT_ID = "font_id"
CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake"
def NextionName(value):

View file

@ -23,6 +23,7 @@ from .base_component import (
CONF_START_UP_PAGE,
CONF_AUTO_WAKE_ON_TOUCH,
CONF_EXIT_REPARSE_ON_START,
CONF_SKIP_CONNECTION_HANDSHAKE,
)
CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
@ -72,6 +73,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
cv.Optional(CONF_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean,
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean,
}
)
.extend(cv.polling_component_schema("5s"))
@ -118,6 +120,8 @@ async def to_code(config):
cg.add(var.set_exit_reparse_on_start_internal(config[CONF_EXIT_REPARSE_ON_START]))
cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE]))
await display.register_display(var, config)
for conf in config.get(CONF_ON_SETUP, []):

View file

@ -43,6 +43,16 @@ bool Nextion::check_connect_() {
if (this->get_is_connected_())
return true;
// Check if the handshake should be skipped for the Nextion connection
if (this->skip_connection_handshake_) {
// Log the connection status without handshake
ESP_LOGW(TAG, "Nextion display set as connected without performing handshake");
// Set the connection status to true
this->is_connected_ = true;
// Return true indicating the connection is set
return true;
}
if (this->comok_sent_ == 0) {
this->reset_(false);
@ -126,10 +136,14 @@ void Nextion::reset_(bool reset_nextion) {
void Nextion::dump_config() {
ESP_LOGCONFIG(TAG, "Nextion:");
ESP_LOGCONFIG(TAG, " Device Model: %s", this->device_model_.c_str());
ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str());
ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str());
ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str());
if (this->skip_connection_handshake_) {
ESP_LOGCONFIG(TAG, " Skip handshake: %s", YESNO(this->skip_connection_handshake_));
} else {
ESP_LOGCONFIG(TAG, " Device Model: %s", this->device_model_.c_str());
ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str());
ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str());
ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str());
}
ESP_LOGCONFIG(TAG, " Wake On Touch: %s", YESNO(this->auto_wake_on_touch_));
ESP_LOGCONFIG(TAG, " Exit reparse: %s", YESNO(this->exit_reparse_on_start_));
@ -262,6 +276,7 @@ void Nextion::loop() {
this->goto_page(this->start_up_page_);
}
// This could probably be removed from the loop area, as those are redundant.
this->set_auto_wake_on_touch(this->auto_wake_on_touch_);
this->set_exit_reparse_on_start(this->exit_reparse_on_start_);

View file

@ -926,6 +926,21 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
void set_exit_reparse_on_start(bool exit_reparse);
/**
* Sets whether the Nextion display should skip the connection handshake process.
* @param skip_handshake True or false. When skip_connection_handshake is true,
* the connection will be established without performing the handshake.
* This can be useful when using Nextion Simulator.
*
* Example:
* ```cpp
* it.set_skip_connection_handshake(true);
* ```
*
* When set to true, the display will be marked as connected without performing a handshake.
*/
void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; }
/**
* Sets Nextion mode between sleep and awake
* @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode.
@ -1221,6 +1236,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
int16_t start_up_page_ = -1;
bool auto_wake_on_touch_ = true;
bool exit_reparse_on_start_ = false;
bool skip_connection_handshake_ = false;
/**
* Manually send a raw command to the display and don't wait for an acknowledgement packet.

View file

@ -0,0 +1,57 @@
from typing import Any
from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
CODEOWNERS = ["@olegtarasov"]
MULTI_CONF = True
CONF_IN_PIN = "in_pin"
CONF_OUT_PIN = "out_pin"
CONF_CH_ENABLE = "ch_enable"
CONF_DHW_ENABLE = "dhw_enable"
CONF_COOLING_ENABLE = "cooling_enable"
CONF_OTC_ACTIVE = "otc_active"
CONF_CH2_ACTIVE = "ch2_active"
CONF_SYNC_MODE = "sync_mode"
opentherm_ns = cg.esphome_ns.namespace("opentherm")
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(OpenthermHub),
cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_CH_ENABLE, True): cv.boolean,
cv.Optional(CONF_DHW_ENABLE, True): cv.boolean,
cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean,
cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean,
cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean,
cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
)
async def to_code(config: dict[str, Any]) -> None:
# Create the hub, passing the two callbacks defined below
# Since the hub is used in the callbacks, we need to define it first
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Set pins
in_pin = await cg.gpio_pin_expression(config[CONF_IN_PIN])
cg.add(var.set_in_pin(in_pin))
out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN])
cg.add(var.set_out_pin(out_pin))
non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN}
for key, value in config.items():
if key not in non_sensors:
cg.add(getattr(var, f"set_{key}")(value))

View file

@ -0,0 +1,277 @@
#include "hub.h"
#include "esphome/core/helpers.h"
#include <string>
namespace esphome {
namespace opentherm {
static const char *const TAG = "opentherm";
OpenthermData OpenthermHub::build_request_(MessageId request_id) {
OpenthermData data;
data.type = 0;
data.id = 0;
data.valueHB = 0;
data.valueLB = 0;
// First, handle the status request. This requires special logic, because we
// wouldn't want to inadvertently disable domestic hot water, for example.
// It is also included in the macro-generated code below, but that will
// never be executed, because we short-circuit it here.
if (request_id == MessageId::STATUS) {
bool const ch_enabled = this->ch_enable;
bool dhw_enabled = this->dhw_enable;
bool cooling_enabled = this->cooling_enable;
bool otc_enabled = this->otc_active;
bool ch2_enabled = this->ch2_active;
data.type = MessageType::READ_DATA;
data.id = MessageId::STATUS;
data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4);
// Disable incomplete switch statement warnings, because the cases in each
// switch are generated based on the configured sensors and inputs.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
// TODO: This is a placeholder for an auto-generated switch statement which builds request structure based on
// which sensors are enabled in config.
#pragma GCC diagnostic pop
return data;
}
return OpenthermData();
}
OpenthermHub::OpenthermHub() : Component() {}
void OpenthermHub::process_response(OpenthermData &data) {
ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id,
this->opentherm_->message_id_to_str((MessageId) data.id));
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str());
}
void OpenthermHub::setup() {
ESP_LOGD(TAG, "Setting up OpenTherm component");
this->opentherm_ = make_unique<OpenTherm>(this->in_pin_, this->out_pin_);
if (!this->opentherm_->initialize()) {
ESP_LOGE(TAG, "Failed to initialize OpenTherm protocol. See previous log messages for details.");
this->mark_failed();
return;
}
// Ensure that there is at least one request, as we are required to
// communicate at least once every second. Sending the status request is
// good practice anyway.
this->add_repeating_message(MessageId::STATUS);
this->current_message_iterator_ = this->initial_messages_.begin();
}
void OpenthermHub::on_shutdown() { this->opentherm_->stop(); }
void OpenthermHub::loop() {
if (this->sync_mode_) {
this->sync_loop_();
return;
}
auto cur_time = millis();
auto const cur_mode = this->opentherm_->get_mode();
switch (cur_mode) {
case OperationMode::WRITE:
case OperationMode::READ:
case OperationMode::LISTEN:
if (!this->check_timings_(cur_time)) {
break;
}
this->last_mode_ = cur_mode;
break;
case OperationMode::ERROR_PROTOCOL:
if (this->last_mode_ == OperationMode::WRITE) {
this->handle_protocol_write_error_();
} else if (this->last_mode_ == OperationMode::READ) {
this->handle_protocol_read_error_();
}
this->stop_opentherm_();
break;
case OperationMode::ERROR_TIMEOUT:
this->handle_timeout_error_();
this->stop_opentherm_();
break;
case OperationMode::IDLE:
if (this->should_skip_loop_(cur_time)) {
break;
}
this->start_conversation_();
break;
case OperationMode::SENT:
// Message sent, now listen for the response.
this->opentherm_->listen();
break;
case OperationMode::RECEIVED:
this->read_response_();
break;
}
}
void OpenthermHub::sync_loop_() {
if (!this->opentherm_->is_idle()) {
ESP_LOGE(TAG, "OpenTherm is not idle at the start of the loop");
return;
}
auto cur_time = millis();
this->check_timings_(cur_time);
if (this->should_skip_loop_(cur_time)) {
return;
}
this->start_conversation_();
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
ESP_LOGE(TAG, "Hub timeout triggered during send");
this->stop_opentherm_();
return;
}
if (this->opentherm_->is_error()) {
this->handle_protocol_write_error_();
this->stop_opentherm_();
return;
} else if (!this->opentherm_->is_sent()) {
ESP_LOGW(TAG, "Unexpected state after sending request: %s",
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
this->stop_opentherm_();
return;
}
// Listen for the response
this->opentherm_->listen();
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
ESP_LOGE(TAG, "Hub timeout triggered during receive");
this->stop_opentherm_();
return;
}
if (this->opentherm_->is_timeout()) {
this->handle_timeout_error_();
this->stop_opentherm_();
return;
} else if (this->opentherm_->is_protocol_error()) {
this->handle_protocol_read_error_();
this->stop_opentherm_();
return;
} else if (!this->opentherm_->has_message()) {
ESP_LOGW(TAG, "Unexpected state after receiving response: %s",
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
this->stop_opentherm_();
return;
}
this->read_response_();
}
bool OpenthermHub::check_timings_(uint32_t cur_time) {
if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) {
ESP_LOGW(TAG,
"%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other "
"components that might slow the loop down.",
(int) (cur_time - this->last_conversation_start_));
this->stop_opentherm_();
return false;
}
return true;
}
bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const {
if (this->last_conversation_end_ > 0 && (cur_time - this->last_conversation_end_) < 100) {
ESP_LOGV(TAG, "Less than 100 ms elapsed since last convo, skipping this iteration");
return true;
}
return false;
}
void OpenthermHub::start_conversation_() {
if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) {
this->sending_initial_ = false;
this->current_message_iterator_ = this->repeating_messages_.begin();
} else if (this->current_message_iterator_ == this->repeating_messages_.end()) {
this->current_message_iterator_ = this->repeating_messages_.begin();
}
auto request = this->build_request_(*this->current_message_iterator_);
ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
this->opentherm_->message_id_to_str((MessageId) request.id));
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(request).c_str());
// Send the request
this->last_conversation_start_ = millis();
this->opentherm_->send(request);
}
void OpenthermHub::read_response_() {
OpenthermData response;
if (!this->opentherm_->get_message(response)) {
ESP_LOGW(TAG, "Couldn't get the response, but flags indicated success. This is a bug.");
this->stop_opentherm_();
return;
}
this->stop_opentherm_();
this->process_response(response);
this->current_message_iterator_++;
}
void OpenthermHub::stop_opentherm_() {
this->opentherm_->stop();
this->last_conversation_end_ = millis();
}
void OpenthermHub::handle_protocol_write_error_() {
ESP_LOGW(TAG, "Error while sending request: %s",
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
ESP_LOGW(TAG, "%s", this->opentherm_->debug_data(this->last_request_).c_str());
}
void OpenthermHub::handle_protocol_read_error_() {
OpenThermError error;
this->opentherm_->get_protocol_error(error);
ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", this->opentherm_->debug_error(error).c_str());
}
void OpenthermHub::handle_timeout_error_() {
ESP_LOGW(TAG, "Receive response timed out at a protocol level");
this->stop_opentherm_();
}
#define ID(x) x
#define SHOW2(x) #x
#define SHOW(x) SHOW2(x)
void OpenthermHub::dump_config() {
ESP_LOGCONFIG(TAG, "OpenTherm:");
LOG_PIN(" In: ", this->in_pin_);
LOG_PIN(" Out: ", this->out_pin_);
ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_);
ESP_LOGCONFIG(TAG, " Initial requests:");
for (auto type : this->initial_messages_) {
ESP_LOGCONFIG(TAG, " - %d", type);
}
ESP_LOGCONFIG(TAG, " Repeating requests:");
for (auto type : this->repeating_messages_) {
ESP_LOGCONFIG(TAG, " - %d", type);
}
}
} // namespace opentherm
} // namespace esphome

View file

@ -0,0 +1,110 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "opentherm.h"
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <functional>
namespace esphome {
namespace opentherm {
// OpenTherm component for ESPHome
class OpenthermHub : public Component {
protected:
// Communication pins for the OpenTherm interface
InternalGPIOPin *in_pin_, *out_pin_;
// The OpenTherm interface
std::unique_ptr<OpenTherm> opentherm_;
// The set of initial messages to send on starting communication with the boiler
std::unordered_set<MessageId> initial_messages_;
// and the repeating messages which are sent repeatedly to update various sensors
// and boiler parameters (like the setpoint).
std::unordered_set<MessageId> repeating_messages_;
// Indicates if we are still working on the initial requests or not
bool sending_initial_ = true;
// Index for the current request in one of the _requests sets.
std::unordered_set<MessageId>::const_iterator current_message_iterator_;
uint32_t last_conversation_start_ = 0;
uint32_t last_conversation_end_ = 0;
OperationMode last_mode_ = IDLE;
OpenthermData last_request_;
// Synchronous communication mode prevents other components from disabling interrupts while
// we are talking to the boiler. Enable if you experience random intermittent invalid response errors.
// Very likely to happen while using Dallas temperature sensors.
bool sync_mode_ = false;
// Create OpenTherm messages based on the message id
OpenthermData build_request_(MessageId request_id);
void handle_protocol_write_error_();
void handle_protocol_read_error_();
void handle_timeout_error_();
void stop_opentherm_();
void start_conversation_();
void read_response_();
bool check_timings_(uint32_t cur_time);
bool should_skip_loop_(uint32_t cur_time) const;
void sync_loop_();
template<typename F> bool spin_wait_(uint32_t timeout, F func) {
auto start_time = millis();
while (func()) {
yield();
auto cur_time = millis();
if (cur_time - start_time >= timeout) {
return false;
}
}
return true;
}
public:
// Constructor with references to the global interrupt handlers
OpenthermHub();
// Handle responses from the OpenTherm interface
void process_response(OpenthermData &data);
// Setters for the input and output OpenTherm interface pins
void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; }
void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; }
// Add a request to the set of initial requests
void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); }
// Add a request to the set of repeating requests. Note that a large number of repeating
// requests will slow down communication with the boiler. Each request may take up to 1 second,
// so with all sensors enabled, it may take about half a minute before a change in setpoint
// will be processed.
void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); }
// There are five status variables, which can either be set as a simple variable,
// or using a switch. ch_enable and dhw_enable default to true, the others to false.
bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false;
// Setters for the status variables
void set_ch_enable(bool value) { this->ch_enable = value; }
void set_dhw_enable(bool value) { this->dhw_enable = value; }
void set_cooling_enable(bool value) { this->cooling_enable = value; }
void set_otc_active(bool value) { this->otc_active = value; }
void set_ch2_active(bool value) { this->ch2_active = value; }
void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void setup() override;
void on_shutdown() override;
void loop() override;
void dump_config() override;
};
} // namespace opentherm
} // namespace esphome

View file

@ -0,0 +1,568 @@
/*
* OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but
* heavily modified to comply with ESPHome coding standards and provide better logging.
* Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
* Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project.
*/
#include "opentherm.h"
#include "esphome/core/helpers.h"
#if defined(ESP32) || defined(USE_ESP_IDF)
#include "driver/timer.h"
#include "esp_err.h"
#endif
#ifdef ESP8266
#include "Arduino.h"
#endif
#include <string>
#include <sstream>
#include <bitset>
namespace esphome {
namespace opentherm {
using std::string;
using std::bitset;
using std::stringstream;
using std::to_string;
static const char *const TAG = "opentherm";
#ifdef ESP8266
OpenTherm *OpenTherm::instance_ = nullptr;
#endif
OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout)
: in_pin_(in_pin),
out_pin_(out_pin),
#if defined(ESP32) || defined(USE_ESP_IDF)
timer_group_(TIMER_GROUP_0),
timer_idx_(TIMER_0),
#endif
mode_(OperationMode::IDLE),
error_type_(ProtocolErrorType::NO_ERROR),
capture_(0),
clock_(0),
data_(0),
bit_pos_(0),
timeout_counter_(-1),
device_timeout_(device_timeout) {
this->isr_in_pin_ = in_pin->to_isr();
this->isr_out_pin_ = out_pin->to_isr();
}
bool OpenTherm::initialize() {
#ifdef ESP8266
OpenTherm::instance_ = this;
#endif
this->in_pin_->pin_mode(gpio::FLAG_INPUT);
this->out_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->out_pin_->digital_write(true);
#if defined(ESP32) || defined(USE_ESP_IDF)
return this->init_esp32_timer_();
#else
return true;
#endif
}
void OpenTherm::listen() {
this->stop_timer_();
this->timeout_counter_ = this->device_timeout_ * 5; // timer_ ticks at 5 ticks/ms
this->mode_ = OperationMode::LISTEN;
this->data_ = 0;
this->bit_pos_ = 0;
this->start_read_timer_();
}
void OpenTherm::send(OpenthermData &data) {
this->stop_timer_();
this->data_ = data.type;
this->data_ = (this->data_ << 12) | data.id;
this->data_ = (this->data_ << 8) | data.valueHB;
this->data_ = (this->data_ << 8) | data.valueLB;
if (!check_parity_(this->data_)) {
this->data_ = this->data_ | 0x80000000;
}
this->clock_ = 1; // clock starts at HIGH
this->bit_pos_ = 33; // count down (33 == start bit, 32-1 data, 0 == stop bit)
this->mode_ = OperationMode::WRITE;
this->start_write_timer_();
}
bool OpenTherm::get_message(OpenthermData &data) {
if (this->mode_ == OperationMode::RECEIVED) {
data.type = (this->data_ >> 28) & 0x7;
data.id = (this->data_ >> 16) & 0xFF;
data.valueHB = (this->data_ >> 8) & 0xFF;
data.valueLB = this->data_ & 0xFF;
return true;
}
return false;
}
bool OpenTherm::get_protocol_error(OpenThermError &error) {
if (this->mode_ != OperationMode::ERROR_PROTOCOL) {
return false;
}
error.error_type = this->error_type_;
error.bit_pos = this->bit_pos_;
error.capture = this->capture_;
error.clock = this->clock_;
error.data = this->data_;
return true;
}
void OpenTherm::stop() {
this->stop_timer_();
this->mode_ = OperationMode::IDLE;
}
void IRAM_ATTR OpenTherm::read_() {
this->data_ = 0;
this->bit_pos_ = 0;
this->mode_ = OperationMode::READ;
this->capture_ = 1; // reset counter and add as if read start bit
this->clock_ = 1; // clock is high at the start of comm
this->start_read_timer_(); // get us into 1/4 of manchester code. 5 timer ticks constitute 1 ms, which is 1 bit
// period in OpenTherm.
}
bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) {
if (arg->mode_ == OperationMode::LISTEN) {
if (arg->timeout_counter_ == 0) {
arg->mode_ = OperationMode::ERROR_TIMEOUT;
arg->stop_timer_();
return false;
}
bool const value = arg->isr_in_pin_.digital_read();
if (value) { // incoming data (rising signal)
arg->read_();
}
if (arg->timeout_counter_ > 0) {
arg->timeout_counter_--;
}
} else if (arg->mode_ == OperationMode::READ) {
bool const value = arg->isr_in_pin_.digital_read();
uint8_t const last = (arg->capture_ & 1);
if (value != last) {
// transition of signal from last sampling
if (arg->clock_ == 1 && arg->capture_ > 0xF) {
// no transition in the middle of the bit
arg->mode_ = OperationMode::ERROR_PROTOCOL;
arg->error_type_ = ProtocolErrorType::NO_TRANSITION;
arg->stop_timer_();
return false;
} else if (arg->clock_ == 1 || arg->capture_ > 0xF) {
// transition in the middle of the bit OR no transition between two bit, both are valid data points
if (arg->bit_pos_ == BitPositions::STOP_BIT) {
// expecting stop bit
auto stop_bit_error = arg->verify_stop_bit_(last);
if (stop_bit_error == ProtocolErrorType::NO_ERROR) {
arg->mode_ = OperationMode::RECEIVED;
arg->stop_timer_();
return false;
} else {
// end of data not verified, invalid data
arg->mode_ = OperationMode::ERROR_PROTOCOL;
arg->error_type_ = stop_bit_error;
arg->stop_timer_();
return false;
}
} else {
// normal data point at clock high
arg->bit_read_(last);
arg->clock_ = 0;
}
} else {
// clock low, not a data point, switch clock
arg->clock_ = 1;
}
arg->capture_ = 1; // reset counter
} else if (arg->capture_ > 0xFF) {
// no change for too long, invalid mancheter encoding
arg->mode_ = OperationMode::ERROR_PROTOCOL;
arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG;
arg->stop_timer_();
return false;
}
arg->capture_ = (arg->capture_ << 1) | value;
} else if (arg->mode_ == OperationMode::WRITE) {
// write data to pin
if (arg->bit_pos_ == 33 || arg->bit_pos_ == 0) { // start bit
arg->write_bit_(1, arg->clock_);
} else { // data bits
arg->write_bit_(read_bit(arg->data_, arg->bit_pos_ - 1), arg->clock_);
}
if (arg->clock_ == 0) {
if (arg->bit_pos_ <= 0) { // check termination
arg->mode_ = OperationMode::SENT; // all data written
arg->stop_timer_();
}
arg->bit_pos_--;
arg->clock_ = 1;
} else {
arg->clock_ = 0;
}
}
return false;
}
#ifdef ESP8266
void IRAM_ATTR OpenTherm::esp8266_timer_isr() { OpenTherm::timer_isr(OpenTherm::instance_); }
#endif
void IRAM_ATTR OpenTherm::bit_read_(uint8_t value) {
this->data_ = (this->data_ << 1) | value;
this->bit_pos_++;
}
ProtocolErrorType OpenTherm::verify_stop_bit_(uint8_t value) {
if (value) { // stop bit detected
return check_parity_(this->data_) ? ProtocolErrorType::NO_ERROR : ProtocolErrorType::PARITY_ERROR;
} else { // no stop bit detected, error
return ProtocolErrorType::INVALID_STOP_BIT;
}
}
void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) {
if (clock == 1) { // left part of manchester encoding
this->isr_out_pin_.digital_write(!high); // low means logical 1 to protocol
} else { // right part of manchester encoding
this->isr_out_pin_.digital_write(high); // high means logical 0 to protocol
}
}
#if defined(ESP32) || defined(USE_ESP_IDF)
bool OpenTherm::init_esp32_timer_() {
// Search for a free timer. Maybe unstable, we'll see.
int cur_timer = 0;
timer_group_t timer_group = TIMER_GROUP_0;
timer_idx_t timer_idx = TIMER_0;
bool timer_found = false;
for (; cur_timer < SOC_TIMER_GROUP_TOTAL_TIMERS; cur_timer++) {
timer_config_t temp_config;
timer_group = cur_timer < 2 ? TIMER_GROUP_0 : TIMER_GROUP_1;
timer_idx = cur_timer < 2 ? (timer_idx_t) cur_timer : (timer_idx_t) (cur_timer - 2);
auto err = timer_get_config(timer_group, timer_idx, &temp_config);
if (err == ESP_ERR_INVALID_ARG) {
// Error means timer was not initialized (or other things, but we are careful with our args)
timer_found = true;
break;
}
ESP_LOGD(TAG, "Timer %d:%d seems to be occupied, will try another", timer_group, timer_idx);
}
if (!timer_found) {
ESP_LOGE(TAG, "No free timer was found! OpenTherm cannot function without a timer.");
return false;
}
ESP_LOGD(TAG, "Found free timer %d:%d", timer_group, timer_idx);
this->timer_group_ = timer_group;
this->timer_idx_ = timer_idx;
timer_config_t const config = {
.alarm_en = TIMER_ALARM_EN,
.counter_en = TIMER_PAUSE,
.intr_type = TIMER_INTR_LEVEL,
.counter_dir = TIMER_COUNT_UP,
.auto_reload = TIMER_AUTORELOAD_EN,
#if ESP_IDF_VERSION_MAJOR >= 5
.clk_src = TIMER_SRC_CLK_DEFAULT,
#endif
.divider = 80,
};
esp_err_t result;
result = timer_init(this->timer_group_, this->timer_idx_, &config);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to init timer. Error: %s", error);
return false;
}
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to set counter value. Error: %s", error);
return false;
}
result = timer_isr_callback_add(this->timer_group_, this->timer_idx_, reinterpret_cast<bool (*)(void *)>(timer_isr),
this, 0);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to register timer interrupt. Error: %s", error);
return false;
}
return true;
}
void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) {
esp_err_t result;
result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error);
return;
}
result = timer_start(this->timer_group_, this->timer_idx_);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error);
return;
}
}
// 5 kHz timer_
void IRAM_ATTR OpenTherm::start_read_timer_() {
InterruptLock const lock;
this->start_esp32_timer_(200);
}
// 2 kHz timer_
void IRAM_ATTR OpenTherm::start_write_timer_() {
InterruptLock const lock;
this->start_esp32_timer_(500);
}
void IRAM_ATTR OpenTherm::stop_timer_() {
InterruptLock const lock;
esp_err_t result;
result = timer_pause(this->timer_group_, this->timer_idx_);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error);
return;
}
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error);
return;
}
}
#endif // END ESP32
#ifdef ESP8266
// 5 kHz timer_
void OpenTherm::start_read_timer_() {
InterruptLock const lock;
timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max)
timer1_write(1000); // 5kHz
}
// 2 kHz timer_
void OpenTherm::start_write_timer_() {
InterruptLock const lock;
timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max)
timer1_write(2500); // 2kHz
}
void OpenTherm::stop_timer_() {
InterruptLock const lock;
timer1_disable();
timer1_detachInterrupt();
}
#endif // END ESP8266
// https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd
bool OpenTherm::check_parity_(uint32_t val) {
val ^= val >> 16;
val ^= val >> 8;
val ^= val >> 4;
val ^= val >> 2;
val ^= val >> 1;
return (~val) & 1;
}
#define TO_STRING_MEMBER(name) \
case name: \
return #name;
const char *OpenTherm::operation_mode_to_str(OperationMode mode) {
switch (mode) {
TO_STRING_MEMBER(IDLE)
TO_STRING_MEMBER(LISTEN)
TO_STRING_MEMBER(READ)
TO_STRING_MEMBER(RECEIVED)
TO_STRING_MEMBER(WRITE)
TO_STRING_MEMBER(SENT)
TO_STRING_MEMBER(ERROR_PROTOCOL)
TO_STRING_MEMBER(ERROR_TIMEOUT)
default:
return "<INVALID>";
}
}
const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) {
switch (error_type) {
TO_STRING_MEMBER(NO_ERROR)
TO_STRING_MEMBER(NO_TRANSITION)
TO_STRING_MEMBER(INVALID_STOP_BIT)
TO_STRING_MEMBER(PARITY_ERROR)
TO_STRING_MEMBER(NO_CHANGE_TOO_LONG)
default:
return "<INVALID>";
}
}
const char *OpenTherm::message_type_to_str(MessageType message_type) {
switch (message_type) {
TO_STRING_MEMBER(READ_DATA)
TO_STRING_MEMBER(READ_ACK)
TO_STRING_MEMBER(WRITE_DATA)
TO_STRING_MEMBER(WRITE_ACK)
TO_STRING_MEMBER(INVALID_DATA)
TO_STRING_MEMBER(DATA_INVALID)
TO_STRING_MEMBER(UNKNOWN_DATAID)
default:
return "<INVALID>";
}
}
const char *OpenTherm::message_id_to_str(MessageId id) {
switch (id) {
TO_STRING_MEMBER(STATUS)
TO_STRING_MEMBER(CH_SETPOINT)
TO_STRING_MEMBER(CONTROLLER_CONFIG)
TO_STRING_MEMBER(DEVICE_CONFIG)
TO_STRING_MEMBER(COMMAND_CODE)
TO_STRING_MEMBER(FAULT_FLAGS)
TO_STRING_MEMBER(REMOTE)
TO_STRING_MEMBER(COOLING_CONTROL)
TO_STRING_MEMBER(CH2_SETPOINT)
TO_STRING_MEMBER(CH_SETPOINT_OVERRIDE)
TO_STRING_MEMBER(TSP_COUNT)
TO_STRING_MEMBER(TSP_COMMAND)
TO_STRING_MEMBER(FHB_SIZE)
TO_STRING_MEMBER(FHB_COMMAND)
TO_STRING_MEMBER(MAX_MODULATION_LEVEL)
TO_STRING_MEMBER(MAX_BOILER_CAPACITY)
TO_STRING_MEMBER(ROOM_SETPOINT)
TO_STRING_MEMBER(MODULATION_LEVEL)
TO_STRING_MEMBER(CH_WATER_PRESSURE)
TO_STRING_MEMBER(DHW_FLOW_RATE)
TO_STRING_MEMBER(DAY_TIME)
TO_STRING_MEMBER(DATE)
TO_STRING_MEMBER(YEAR)
TO_STRING_MEMBER(ROOM_SETPOINT_CH2)
TO_STRING_MEMBER(ROOM_TEMP)
TO_STRING_MEMBER(FEED_TEMP)
TO_STRING_MEMBER(DHW_TEMP)
TO_STRING_MEMBER(OUTSIDE_TEMP)
TO_STRING_MEMBER(RETURN_WATER_TEMP)
TO_STRING_MEMBER(SOLAR_STORE_TEMP)
TO_STRING_MEMBER(SOLAR_COLLECT_TEMP)
TO_STRING_MEMBER(FEED_TEMP_CH2)
TO_STRING_MEMBER(DHW2_TEMP)
TO_STRING_MEMBER(EXHAUST_TEMP)
TO_STRING_MEMBER(FAN_SPEED)
TO_STRING_MEMBER(FLAME_CURRENT)
TO_STRING_MEMBER(DHW_BOUNDS)
TO_STRING_MEMBER(CH_BOUNDS)
TO_STRING_MEMBER(OTC_CURVE_BOUNDS)
TO_STRING_MEMBER(DHW_SETPOINT)
TO_STRING_MEMBER(MAX_CH_SETPOINT)
TO_STRING_MEMBER(OTC_CURVE_RATIO)
TO_STRING_MEMBER(HVAC_STATUS)
TO_STRING_MEMBER(REL_VENT_SETPOINT)
TO_STRING_MEMBER(DEVICE_VENT)
TO_STRING_MEMBER(REL_VENTILATION)
TO_STRING_MEMBER(REL_HUMID_EXHAUST)
TO_STRING_MEMBER(SUPPLY_INLET_TEMP)
TO_STRING_MEMBER(SUPPLY_OUTLET_TEMP)
TO_STRING_MEMBER(EXHAUST_INLET_TEMP)
TO_STRING_MEMBER(EXHAUST_OUTLET_TEMP)
TO_STRING_MEMBER(NOM_REL_VENTILATION)
TO_STRING_MEMBER(OVERRIDE_FUNC)
TO_STRING_MEMBER(OEM_DIAGNOSTIC)
TO_STRING_MEMBER(BURNER_STARTS)
TO_STRING_MEMBER(CH_PUMP_STARTS)
TO_STRING_MEMBER(DHW_PUMP_STARTS)
TO_STRING_MEMBER(DHW_BURNER_STARTS)
TO_STRING_MEMBER(BURNER_HOURS)
TO_STRING_MEMBER(CH_PUMP_HOURS)
TO_STRING_MEMBER(DHW_PUMP_HOURS)
TO_STRING_MEMBER(DHW_BURNER_HOURS)
TO_STRING_MEMBER(OT_VERSION_CONTROLLER)
TO_STRING_MEMBER(OT_VERSION_DEVICE)
TO_STRING_MEMBER(VERSION_CONTROLLER)
TO_STRING_MEMBER(VERSION_DEVICE)
default:
return "<INVALID>";
}
}
string OpenTherm::debug_data(OpenthermData &data) {
stringstream result;
result << bitset<8>(data.type) << " " << bitset<8>(data.id) << " " << bitset<8>(data.valueHB) << " "
<< bitset<8>(data.valueLB) << "\n";
result << "type: " << this->message_type_to_str((MessageType) data.type) << "; ";
result << "id: " << to_string(data.id) << "; ";
result << "HB: " << to_string(data.valueHB) << "; ";
result << "LB: " << to_string(data.valueLB) << "; ";
result << "uint_16: " << to_string(data.u16()) << "; ";
result << "float: " << to_string(data.f88());
return result.str();
}
std::string OpenTherm::debug_error(OpenThermError &error) {
stringstream result;
result << "type: " << this->protocol_error_to_to_str(error.error_type) << "; ";
result << "data: ";
result << format_hex(error.data);
result << "; clock: " << to_string(clock_);
result << "; capture: " << bitset<32>(error.capture);
result << "; bit_pos: " << to_string(error.bit_pos);
return result.str();
}
float OpenthermData::f88() { return ((float) this->s16()) / 256.0; }
void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); }
uint16_t OpenthermData::u16() {
uint16_t const value = this->valueHB;
return (value << 8) | this->valueLB;
}
void OpenthermData::u16(uint16_t value) {
this->valueLB = value & 0xFF;
this->valueHB = (value >> 8) & 0xFF;
}
int16_t OpenthermData::s16() {
int16_t const value = this->valueHB;
return (value << 8) | this->valueLB;
}
void OpenthermData::s16(int16_t value) {
this->valueLB = value & 0xFF;
this->valueHB = (value >> 8) & 0xFF;
}
} // namespace opentherm
} // namespace esphome

View file

@ -0,0 +1,347 @@
/*
* OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but
* heavily modified to comply with ESPHome coding standards and provide better logging.
* Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
* Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project.
*/
#pragma once
#include <string>
#include <sstream>
#include <iomanip>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#if defined(ESP32) || defined(USE_ESP_IDF)
#include "driver/timer.h"
#endif
namespace esphome {
namespace opentherm {
// TODO: Account for immutable semantics change in hub.cpp when doing later installments of OpenTherm PR
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
return bit_value ? setBit(value, bit) : clearBit(value, bit);
}
enum OperationMode {
IDLE = 0, // no operation
LISTEN = 1, // waiting for transmission to start
READ = 2, // reading 32-bit data frame
RECEIVED = 3, // data frame received with valid start and stop bit
WRITE = 4, // writing data with timer_
SENT = 5, // all data written to output
ERROR_PROTOCOL = 8, // manchester protocol data transfer error
ERROR_TIMEOUT = 9 // read timeout
};
enum ProtocolErrorType {
NO_ERROR = 0, // No error
NO_TRANSITION = 1, // No transition in the middle of the bit
INVALID_STOP_BIT = 2, // Stop bit wasn't present when expected
PARITY_ERROR = 3, // Parity check didn't pass
NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks
};
enum MessageType {
READ_DATA = 0,
READ_ACK = 4,
WRITE_DATA = 1,
WRITE_ACK = 5,
INVALID_DATA = 2,
DATA_INVALID = 6,
UNKNOWN_DATAID = 7
};
enum MessageId {
STATUS = 0,
CH_SETPOINT = 1,
CONTROLLER_CONFIG = 2,
DEVICE_CONFIG = 3,
COMMAND_CODE = 4,
FAULT_FLAGS = 5,
REMOTE = 6,
COOLING_CONTROL = 7,
CH2_SETPOINT = 8,
CH_SETPOINT_OVERRIDE = 9,
TSP_COUNT = 10,
TSP_COMMAND = 11,
FHB_SIZE = 12,
FHB_COMMAND = 13,
MAX_MODULATION_LEVEL = 14,
MAX_BOILER_CAPACITY = 15, // u8_hb - u8_lb gives min modulation level
ROOM_SETPOINT = 16,
MODULATION_LEVEL = 17,
CH_WATER_PRESSURE = 18,
DHW_FLOW_RATE = 19,
DAY_TIME = 20,
DATE = 21,
YEAR = 22,
ROOM_SETPOINT_CH2 = 23,
ROOM_TEMP = 24,
FEED_TEMP = 25,
DHW_TEMP = 26,
OUTSIDE_TEMP = 27,
RETURN_WATER_TEMP = 28,
SOLAR_STORE_TEMP = 29,
SOLAR_COLLECT_TEMP = 30,
FEED_TEMP_CH2 = 31,
DHW2_TEMP = 32,
EXHAUST_TEMP = 33,
FAN_SPEED = 35,
FLAME_CURRENT = 36,
DHW_BOUNDS = 48,
CH_BOUNDS = 49,
OTC_CURVE_BOUNDS = 50,
DHW_SETPOINT = 56,
MAX_CH_SETPOINT = 57,
OTC_CURVE_RATIO = 58,
// HVAC Specific Message IDs
HVAC_STATUS = 70,
REL_VENT_SETPOINT = 71,
DEVICE_VENT = 74,
REL_VENTILATION = 77,
REL_HUMID_EXHAUST = 78,
SUPPLY_INLET_TEMP = 80,
SUPPLY_OUTLET_TEMP = 81,
EXHAUST_INLET_TEMP = 82,
EXHAUST_OUTLET_TEMP = 83,
NOM_REL_VENTILATION = 87,
OVERRIDE_FUNC = 100,
OEM_DIAGNOSTIC = 115,
BURNER_STARTS = 116,
CH_PUMP_STARTS = 117,
DHW_PUMP_STARTS = 118,
DHW_BURNER_STARTS = 119,
BURNER_HOURS = 120,
CH_PUMP_HOURS = 121,
DHW_PUMP_HOURS = 122,
DHW_BURNER_HOURS = 123,
OT_VERSION_CONTROLLER = 124,
OT_VERSION_DEVICE = 125,
VERSION_CONTROLLER = 126,
VERSION_DEVICE = 127
};
enum BitPositions { STOP_BIT = 33 };
/**
* Structure to hold Opentherm data packet content.
* Use f88(), u16() or s16() functions to get appropriate value of data packet accoridng to id of message.
*/
struct OpenthermData {
uint8_t type;
uint8_t id;
uint8_t valueHB;
uint8_t valueLB;
OpenthermData() : type(0), id(0), valueHB(0), valueLB(0) {}
/**
* @return float representation of data packet value
*/
float f88();
/**
* @param float number to set as value of this data packet
*/
void f88(float value);
/**
* @return unsigned 16b integer representation of data packet value
*/
uint16_t u16();
/**
* @param unsigned 16b integer number to set as value of this data packet
*/
void u16(uint16_t value);
/**
* @return signed 16b integer representation of data packet value
*/
int16_t s16();
/**
* @param signed 16b integer number to set as value of this data packet
*/
void s16(int16_t value);
};
struct OpenThermError {
ProtocolErrorType error_type;
uint32_t capture;
uint8_t clock;
uint32_t data;
uint8_t bit_pos;
};
/**
* Opentherm static class that supports either listening or sending Opentherm data packets in the same time
*/
class OpenTherm {
public:
OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout = 800);
/**
* Setup pins.
*/
bool initialize();
/**
* Start listening for Opentherm data packet comming from line connected to given pin.
* If data packet is received then has_message() function returns true and data packet can be retrieved by calling
* get_message() function. If timeout > 0 then this function waits for incomming data package for timeout millis and
* if no data packet is recevived, error state is indicated by is_error() function. If either data packet is received
* or timeout is reached listening is stopped.
*/
void listen();
/**
* Use this function to check whether listen() function already captured a valid data packet.
*
* @return true if data packet has been captured from line by listen() function.
*/
bool has_message() { return mode_ == OperationMode::RECEIVED; }
/**
* Use this to retrive data packed captured by listen() function. Data packet is ready when has_message() function
* returns true. This function can be called multiple times until stop() is called.
*
* @param data reference to data structure to which fill the data packet data.
* @return true if packet was ready and was filled into data structure passed, false otherwise.
*/
bool get_message(OpenthermData &data);
/**
* Immediately send out Opentherm data packet to line connected on given pin.
* Completed data transfer is indicated by is_sent() function.
* Error state is indicated by is_error() function.
*
* @param data Opentherm data packet.
*/
void send(OpenthermData &data);
/**
* Stops listening for data packet or sending out data packet and resets internal state of this class.
* Stops all timers and unattaches all interrupts.
*/
void stop();
/**
* Get protocol error details in case a protocol error occured.
* @param error reference to data structure to which fill the error details
* @return true if protocol error occured during last conversation, false otherwise.
*/
bool get_protocol_error(OpenThermError &error);
/**
* Use this function to check whether send() function already finished sending data packed to line.
*
* @return true if data packet has been sent, false otherwise.
*/
bool is_sent() { return mode_ == OperationMode::SENT; }
/**
* Indicates whether listinig or sending is not in progress.
* That also means that no timers are running and no interrupts are attached.
*
* @return true if listening nor sending is in progress.
*/
bool is_idle() { return mode_ == OperationMode::IDLE; }
/**
* Indicates whether last listen() or send() operation ends up with an error. Includes both timeout and
* protocol errors.
*
* @return true if last listen() or send() operation ends up with an error.
*/
bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; }
/**
* Indicates whether last listen() or send() operation ends up with a *timeout* error
* @return true if last listen() or send() operation ends up with a *timeout* error.
*/
bool is_timeout() { return mode_ == OperationMode::ERROR_TIMEOUT; }
/**
* Indicates whether last listen() or send() operation ends up with a *protocol* error
* @return true if last listen() or send() operation ends up with a *protocol* error.
*/
bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; }
bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; }
OperationMode get_mode() { return mode_; }
std::string debug_data(OpenthermData &data);
std::string debug_error(OpenThermError &error);
const char *protocol_error_to_to_str(ProtocolErrorType error_type);
const char *message_type_to_str(MessageType message_type);
const char *operation_mode_to_str(OperationMode mode);
const char *message_id_to_str(MessageId id);
static bool timer_isr(OpenTherm *arg);
#ifdef ESP8266
static void esp8266_timer_isr();
#endif
private:
InternalGPIOPin *in_pin_;
InternalGPIOPin *out_pin_;
ISRInternalGPIOPin isr_in_pin_;
ISRInternalGPIOPin isr_out_pin_;
#if defined(ESP32) || defined(USE_ESP_IDF)
timer_group_t timer_group_;
timer_idx_t timer_idx_;
#endif
OperationMode mode_;
ProtocolErrorType error_type_;
uint32_t capture_;
uint8_t clock_;
uint32_t data_;
uint8_t bit_pos_;
int32_t timeout_counter_; // <0 no timeout
int32_t device_timeout_;
#if defined(ESP32) || defined(USE_ESP_IDF)
bool init_esp32_timer_();
void start_esp32_timer_(uint64_t alarm_value);
#endif
void stop_timer_();
void read_(); // data detected start reading
void start_read_timer_(); // reading timer_ to sample at 1/5 of manchester code bit length (at 5kHz)
void start_write_timer_(); // writing timer_ to send manchester code (at 2kHz)
bool check_parity_(uint32_t val);
void bit_read_(uint8_t value);
ProtocolErrorType verify_stop_bit_(uint8_t value);
void write_bit_(uint8_t high, uint8_t clock);
#ifdef ESP8266
// ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm
static OpenTherm *instance_;
#endif
};
} // namespace opentherm
} // namespace esphome

View file

@ -1,10 +1,14 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import esp32_rmt, remote_base
import esphome.config_validation as cv
from esphome import pins
from esphome.components import remote_base, esp32_rmt
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_ID, CONF_PIN, CONF_RMT_CHANNEL
AUTO_LOAD = ["remote_base"]
CONF_ON_TRANSMIT = "on_transmit"
CONF_ON_COMPLETE = "on_complete"
remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter")
RemoteTransmitterComponent = remote_transmitter_ns.class_(
"RemoteTransmitterComponent", remote_base.RemoteTransmitterBase, cg.Component
@ -19,6 +23,8 @@ CONFIG_SCHEMA = cv.Schema(
cv.percentage_int, cv.Range(min=1, max=100)
),
cv.Optional(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=True),
cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
}
).extend(cv.COMPONENT_SCHEMA)
@ -32,3 +38,13 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT]))
if on_transmit_config := config.get(CONF_ON_TRANSMIT):
await automation.build_automation(
var.get_transmit_trigger(), [], on_transmit_config
)
if on_complete_config := config.get(CONF_ON_COMPLETE):
await automation.build_automation(
var.get_complete_trigger(), [], on_complete_config
)

View file

@ -33,6 +33,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; }
Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; };
Trigger<> *get_complete_trigger() const { return this->complete_trigger_; };
protected:
void send_internal(uint32_t send_times, uint32_t send_wait) override;
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
@ -57,6 +60,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
bool inverted_{false};
#endif
uint8_t carrier_duty_percent_;
Trigger<> *transmit_trigger_{new Trigger<>()};
Trigger<> *complete_trigger_{new Trigger<>()};
};
} // namespace remote_transmitter

View file

@ -124,6 +124,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
ESP_LOGE(TAG, "Empty data");
return;
}
this->transmit_trigger_->trigger();
for (uint32_t i = 0; i < send_times; i++) {
esp_err_t error = rmt_write_items(this->channel_, this->rmt_temp_.data(), this->rmt_temp_.size(), true);
if (error != ESP_OK) {
@ -135,6 +136,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (i + 1 < send_times)
delayMicroseconds(send_wait);
}
this->complete_trigger_->trigger();
}
} // namespace remote_transmitter

View file

@ -76,6 +76,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
uint32_t on_time, off_time;
this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
this->target_time_ = 0;
this->transmit_trigger_->trigger();
for (uint32_t i = 0; i < send_times; i++) {
for (int32_t item : this->temp_.get_data()) {
if (item > 0) {
@ -93,6 +94,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (i + 1 < send_times)
this->target_time_ += send_wait;
}
this->complete_trigger_->trigger();
}
} // namespace remote_transmitter

View file

@ -78,6 +78,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
uint32_t on_time, off_time;
this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
this->target_time_ = 0;
this->transmit_trigger_->trigger();
for (uint32_t i = 0; i < send_times; i++) {
InterruptLock lock;
for (int32_t item : this->temp_.get_data()) {
@ -96,6 +97,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (i + 1 < send_times)
this->target_time_ += send_wait;
}
this->complete_trigger_->trigger();
}
} // namespace remote_transmitter

View file

@ -8,8 +8,14 @@ namespace st7701s {
void ST7701S::setup() {
esph_log_config(TAG, "Setting up ST7701S");
this->spi_setup();
this->write_init_sequence_();
esp_lcd_rgb_panel_config_t config{};
config.flags.fb_in_psram = 1;
#if ESP_IDF_VERSION_MAJOR >= 5
config.bounce_buffer_size_px = this->width_ * 10;
config.num_fbs = 1;
#endif // ESP_IDF_VERSION_MAJOR
config.timings.h_res = this->width_;
config.timings.v_res = this->height_;
config.timings.hsync_pulse_width = this->hsync_pulse_width_;
@ -21,7 +27,6 @@ void ST7701S::setup() {
config.timings.flags.pclk_active_neg = this->pclk_inverted_;
config.timings.pclk_hz = this->pclk_frequency_;
config.clk_src = LCD_CLK_SRC_PLL160M;
config.sram_trans_align = 64;
config.psram_trans_align = 64;
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++) {
@ -34,15 +39,21 @@ void ST7701S::setup() {
config.de_gpio_num = this->de_pin_->get_pin();
config.pclk_gpio_num = this->pclk_pin_->get_pin();
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
if (err != ESP_OK) {
esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
}
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
this->write_init_sequence_();
esph_log_config(TAG, "ST7701S setup complete");
}
void ST7701S::loop() {
#if ESP_IDF_VERSION_MAJOR >= 5
if (this->handle_ != nullptr)
esp_lcd_rgb_panel_restart(this->handle_);
#endif // ESP_IDF_VERSION_MAJOR
}
void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (w <= 0 || h <= 0)
@ -160,10 +171,12 @@ void ST7701S::write_init_sequence_() {
this->write_data_(val);
ESP_LOGD(TAG, "write MADCTL %X", val);
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
this->set_timeout(120, [this] {
this->write_command_(SLEEP_OUT);
this->write_command_(DISPLAY_ON);
});
// can't avoid this inline delay due to the need to complete setup before anything else tries to draw.
delay(120); // NOLINT
this->write_command_(SLEEP_OUT);
this->write_command_(DISPLAY_ON);
this->spi_teardown(); // SPI not needed after this
delay(10);
}
void ST7701S::dump_config() {

View file

@ -33,6 +33,8 @@ class ST7701S : public display::Display,
public:
void update() override { this->do_update_(); }
void setup() override;
void complete_setup_();
void loop() override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;

View file

@ -0,0 +1,72 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
)
CODEOWNERS = ["@mobrembski"]
AUTO_LOAD = ["gpio_expander"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
tca9555_ns = cg.esphome_ns.namespace("tca9555")
TCA9555Component = tca9555_ns.class_("TCA9555Component", cg.Component, i2c.I2CDevice)
TCA9555GPIOPin = tca9555_ns.class_("TCA9555GPIOPin", cg.GPIOPin)
CONF_TCA9555 = "tca9555"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(TCA9555Component),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x21))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
def validate_mode(value):
if not (value[CONF_INPUT] or value[CONF_OUTPUT]):
raise cv.Invalid("Mode must be either input or output")
if value[CONF_INPUT] and value[CONF_OUTPUT]:
raise cv.Invalid("Mode must be either input or output")
return value
TCA9555_PIN_SCHEMA = pins.gpio_base_schema(
TCA9555GPIOPin,
cv.int_range(min=0, max=15),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
invertable=True,
).extend(
{
cv.Required(CONF_TCA9555): cv.use_id(TCA9555Component),
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_TCA9555, TCA9555_PIN_SCHEMA)
async def tca9555_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_TCA9555])
cg.add(var.set_pin(config[CONF_NUMBER]))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View file

@ -0,0 +1,140 @@
#include "tca9555.h"
#include "esphome/core/log.h"
static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00;
static const uint8_t TCA9555_INPUT_PORT_REGISTER_1 = 0x01;
static const uint8_t TCA9555_OUTPUT_PORT_REGISTER_0 = 0x02;
static const uint8_t TCA9555_OUTPUT_PORT_REGISTER_1 = 0x03;
static const uint8_t TCA9555_POLARITY_REGISTER_0 = 0x04;
static const uint8_t TCA9555_POLARITY_REGISTER_1 = 0x05;
static const uint8_t TCA9555_CONFIGURATION_PORT_0 = 0x06;
static const uint8_t TCA9555_CONFIGURATION_PORT_1 = 0x07;
namespace esphome {
namespace tca9555 {
static const char *const TAG = "tca9555";
void TCA9555Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up TCA9555...");
if (!this->read_gpio_modes_()) {
this->mark_failed();
return;
}
if (!this->read_gpio_outputs_()) {
this->mark_failed();
return;
}
}
void TCA9555Component::dump_config() {
ESP_LOGCONFIG(TAG, "TCA9555:");
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with TCA9555 failed!");
}
}
void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
// Set mode mask bit
this->mode_mask_ |= 1 << pin;
} else if (flags == gpio::FLAG_OUTPUT) {
// Clear mode mask bit
this->mode_mask_ &= ~(1 << pin);
}
// Write GPIO to enable input mode
this->write_gpio_modes_();
}
void TCA9555Component::loop() { this->reset_pin_cache_(); }
bool TCA9555Component::read_gpio_outputs_() {
if (this->is_failed())
return false;
uint8_t data[2];
if (!this->read_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) {
this->status_set_warning("Failed to read output register");
return false;
}
this->output_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0);
this->status_clear_warning();
return true;
}
bool TCA9555Component::read_gpio_modes_() {
if (this->is_failed())
return false;
uint8_t data[2];
bool success = this->read_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2);
if (!success) {
this->status_set_warning("Failed to read mode register");
return false;
}
this->mode_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0);
this->status_clear_warning();
return true;
}
bool TCA9555Component::digital_read_hw(uint8_t pin) {
if (this->is_failed())
return false;
bool success;
uint8_t data[2];
success = this->read_bytes(TCA9555_INPUT_PORT_REGISTER_0, data, 2);
this->input_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0);
if (!success) {
this->status_set_warning("Failed to read input register");
return false;
}
this->status_clear_warning();
return true;
}
void TCA9555Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
uint8_t data[2];
data[0] = this->output_mask_;
data[1] = this->output_mask_ >> 8;
if (!this->write_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) {
this->status_set_warning("Failed to write output register");
return;
}
this->status_clear_warning();
}
bool TCA9555Component::write_gpio_modes_() {
if (this->is_failed())
return false;
uint8_t data[2];
data[0] = this->mode_mask_;
data[1] = this->mode_mask_ >> 8;
if (!this->write_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2)) {
this->status_set_warning("Failed to write mode register");
return false;
}
this->status_clear_warning();
return true;
}
bool TCA9555Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
float TCA9555Component::get_setup_priority() const { return setup_priority::IO; }
void TCA9555GPIOPin::setup() { this->pin_mode(this->flags_); }
void TCA9555GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool TCA9555GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void TCA9555GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
std::string TCA9555GPIOPin::dump_summary() const { return str_sprintf("%u via TCA9555", this->pin_); }
} // namespace tca9555
} // namespace esphome

View file

@ -0,0 +1,64 @@
#pragma once
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace tca9555 {
class TCA9555Component : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 16> {
public:
TCA9555Component() = default;
/// Check i2c availability and setup masks
void setup() override;
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
void loop() override;
protected:
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
/// Mask for the pin mode - 1 means output, 0 means input
uint16_t mode_mask_{0x00};
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint16_t output_mask_{0x00};
/// The state read in digital_read_hw - 1 means HIGH, 0 means LOW
uint16_t input_mask_{0x00};
bool read_gpio_modes_();
bool write_gpio_modes_();
bool read_gpio_outputs_();
};
/// Helper class to expose a TCA9555 pin as an internal input GPIO pin.
class TCA9555GPIOPin : public GPIOPin, public Parented<TCA9555Component> {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
std::string dump_summary() const override;
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
protected:
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace tca9555
} // namespace esphome

View file

@ -1,6 +1,7 @@
#include "tcs34725.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <algorithm>
namespace esphome {
namespace tcs34725 {
@ -211,7 +212,7 @@ void TCS34725Component::update() {
if (raw_c == 0) {
channel_c = channel_r = channel_g = channel_b = 0.0f;
} else {
float max_count = this->integration_time_ * 1024.0f / 2.4;
float max_count = this->integration_time_ <= 153.6f ? this->integration_time_ * 1024.0f / 2.4f : 65535.0f;
float sum = raw_c;
channel_r = raw_r / sum * 100.0f;
channel_g = raw_g / sum * 100.0f;
@ -254,7 +255,8 @@ void TCS34725Component::update() {
// change integration time an gain to achieve maximum resolution an dynamic range
// calculate optimal integration time to achieve 70% satuaration
float integration_time_ideal;
integration_time_ideal = 60 / ((float) raw_c / 655.35) * this->integration_time_;
integration_time_ideal = 60 / ((float) std::max((uint16_t) 1, raw_c) / 655.35f) * this->integration_time_;
uint8_t gain_reg_val_new = this->gain_reg_;
// increase gain if less than 20% of white channel used and high integration time

View file

@ -77,6 +77,18 @@ struct Timer {
}
};
struct WakeWord {
std::string id;
std::string wake_word;
std::vector<std::string> trained_languages;
};
struct Configuration {
std::vector<WakeWord> available_wake_words;
std::vector<std::string> active_wake_words;
uint32_t max_active_wake_words;
};
class VoiceAssistant : public Component {
public:
void setup() override;
@ -133,6 +145,8 @@ class VoiceAssistant : public Component {
void on_audio(const api::VoiceAssistantAudio &msg);
void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg);
void on_announce(const api::VoiceAssistantAnnounceRequest &msg);
void on_set_configuration(const std::vector<std::string> &active_wake_words){};
const Configuration &get_configuration() { return this->config_; };
bool is_running() const { return this->state_ != State::IDLE; }
void set_continuous(bool continuous) { this->continuous_ = continuous; }
@ -279,6 +293,8 @@ class VoiceAssistant : public Component {
AudioMode audio_mode_{AUDIO_MODE_UDP};
bool udp_socket_running_{false};
bool start_udp_socket_();
Configuration config_{};
};
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<VoiceAssistant> {

View file

@ -1012,6 +1012,16 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
call.set_mode(mode.c_str());
}
if (request->hasParam("fan_mode")) {
auto mode = request->getParam("fan_mode")->value();
call.set_fan_mode(mode.c_str());
}
if (request->hasParam("swing_mode")) {
auto mode = request->getParam("swing_mode")->value();
call.set_swing_mode(mode.c_str());
}
if (request->hasParam("target_temperature_high")) {
auto target_temperature_high = parse_number<float>(request->getParam("target_temperature_high")->value().c_str());
if (target_temperature_high.has_value())

View file

@ -130,11 +130,16 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
}
void WiFiComponent::wifi_pre_setup_() {
#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC
uint8_t mac[6];
#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC
get_mac_address_raw(mac);
set_mac_address(mac);
ESP_LOGV(TAG, "Use EFuse MAC without checking CRC: %s", get_mac_address_pretty().c_str());
#else
if (has_custom_mac_address()) {
get_mac_address_raw(mac);
set_mac_address(mac);
}
#endif
esp_err_t err = esp_netif_init();
if (err != ERR_OK) {

View file

@ -120,6 +120,7 @@ CONF_CHANNELS = "channels"
CONF_CHARACTERISTIC_UUID = "characteristic_uuid"
CONF_CHECK = "check"
CONF_CHIPSET = "chipset"
CONF_CLEAN_SESSION = "clean_session"
CONF_CLEAR_IMPEDANCE = "clear_impedance"
CONF_CLIENT_CERTIFICATE = "client_certificate"
CONF_CLIENT_CERTIFICATE_KEY = "client_certificate_key"

View file

@ -100,9 +100,6 @@ def valid_include(value):
def valid_project_name(value: str):
if value.count(".") != 1:
raise cv.Invalid("project name needs to have a namespace")
value = value.replace(" ", "_")
return value

View file

@ -29,6 +29,7 @@
#define USE_DATETIME_TIME
#define USE_DEEP_SLEEP
#define USE_DISPLAY
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT
#define USE_FAN
#define USE_GRAPH
@ -45,10 +46,10 @@
#define USE_LVGL_BUTTONMATRIX
#define USE_LVGL_FONT
#define USE_LVGL_IMAGE
#define USE_LVGL_KEYBOARD
#define USE_LVGL_KEY_LISTENER
#define USE_LVGL_TOUCHSCREEN
#define USE_LVGL_KEYBOARD
#define USE_LVGL_ROTARY_ENCODER
#define USE_LVGL_TOUCHSCREEN
#define USE_MDNS
#define USE_MEDIA_PLAYER
#define USE_MQTT

View file

@ -671,9 +671,17 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame
// match the CRC that goes along with it. For those devices, this
// work-around reads and uses the MAC address as-is from EFuse,
// without doing the CRC check.
esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
if (has_custom_mac_address()) {
esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48);
} else {
esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
}
#else
esp_efuse_mac_get_default(mac);
if (has_custom_mac_address()) {
esp_efuse_mac_get_custom(mac);
} else {
esp_efuse_mac_get_default(mac);
}
#endif
#elif defined(USE_ESP8266)
wifi_get_macaddr(STATION_IF, mac);
@ -685,20 +693,53 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame
// this should be an error, but that messes with CI checks. #error No mac address method defined
#endif
}
std::string get_mac_address() {
uint8_t mac[6];
get_mac_address_raw(mac);
return str_snprintf("%02x%02x%02x%02x%02x%02x", 12, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
std::string get_mac_address_pretty() {
uint8_t mac[6];
get_mac_address_raw(mac);
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
#ifdef USE_ESP32
void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
#endif
bool has_custom_mac_address() {
#ifdef USE_ESP32
uint8_t mac[6];
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED) || defined(USE_ESP32_IGNORE_EFUSE_MAC_CRC)
return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
#else
return (esp_efuse_mac_get_custom(mac) == ESP_OK) && mac_address_is_valid(mac);
#endif
#else
return false;
#endif
}
bool mac_address_is_valid(const uint8_t *mac) {
bool is_all_zeros = true;
bool is_all_ones = true;
for (uint8_t i = 0; i < 6; i++) {
if (mac[i] != 0) {
is_all_zeros = false;
}
}
for (uint8_t i = 0; i < 6; i++) {
if (mac[i] != 0xFF) {
is_all_ones = false;
}
}
return !(is_all_zeros || is_all_ones);
}
void delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability
uint32_t start = micros();

View file

@ -635,6 +635,14 @@ std::string get_mac_address_pretty();
void set_mac_address(uint8_t *mac);
#endif
/// Check if a custom MAC address is set (ESP32 & variants)
/// @return True if a custom MAC address is set (ESP32 & variants), else false
bool has_custom_mac_address();
/// Check if the MAC address is not all zeros or all ones
/// @return True if MAC is valid, else false
bool mac_address_is_valid(const uint8_t *mac);
/// Delay for the given amount of microseconds, possibly yielding to other processes during the wait.
void delay_microseconds_safe(uint32_t us);

Some files were not shown because too many files have changed in this diff Show more