1
0
mirror of https://github.com/esphome/esphome.git synced 2025-04-10 12:50:30 +01:00

Merge remote-tracking branch 'origin/dev' into nrf52

This commit is contained in:
Tomasz Duda 2024-05-26 19:48:03 +02:00
commit 26c15dfb26
82 changed files with 5565 additions and 4535 deletions

View File

@ -152,6 +152,10 @@ esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte
esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
@ -306,7 +310,7 @@ esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core
esphome/components/sdm_meter/* @jesserockz @polyfaces

View File

@ -1,19 +1,16 @@
from esphome.cpp_generator import RawExpression
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
from esphome.const import (
CONF_ID,
CONF_NUM_ATTEMPTS,
CONF_OTA,
CONF_PASSWORD,
CONF_PORT,
CONF_REBOOT_TIMEOUT,
CONF_SAFE_MODE,
CONF_VERSION,
KEY_PAST_SAFE_MODE,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core import coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
@ -28,7 +25,6 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
cv.SplitDefault(
CONF_PORT,
@ -39,10 +35,15 @@ CONFIG_SCHEMA = (
rtl87xx=8892,
): cv.port,
cv.Optional(CONF_PASSWORD): cv.string,
cv.Optional(
CONF_REBOOT_TIMEOUT, default="5min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
),
cv.Optional(CONF_REBOOT_TIMEOUT): cv.invalid(
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
),
cv.Optional(CONF_SAFE_MODE): cv.invalid(
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
),
}
)
.extend(BASE_OTA_SCHEMA)
@ -50,10 +51,8 @@ CONFIG_SCHEMA = (
)
@coroutine_with_priority(50.0)
@coroutine_with_priority(52.0)
async def to_code(config):
CORE.data[CONF_OTA] = {}
var = cg.new_Pvariable(config[CONF_ID])
await ota_to_code(var, config)
cg.add(var.set_port(config[CONF_PORT]))
@ -63,10 +62,3 @@ async def to_code(config):
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
await cg.register_component(var, config)
if config[CONF_SAFE_MODE]:
condition = var.should_enter_safe_mode(
config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT]
)
cg.add(RawExpression(f"if ({condition}) return"))
CORE.data[CONF_OTA][KEY_PAST_SAFE_MODE] = True

View File

@ -78,23 +78,9 @@ void ESPHomeOTAComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Password configured");
}
#endif
if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 &&
this->safe_mode_rtc_value_ != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) {
ESP_LOGW(TAG, "Last reset occurred too quickly; safe mode will be invoked in %" PRIu32 " restarts",
this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_);
}
}
void ESPHomeOTAComponent::loop() {
this->handle_();
if (this->has_safe_mode_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) {
this->has_safe_mode_ = false;
// successful boot, reset counter
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->clean_rtc();
}
}
void ESPHomeOTAComponent::loop() { this->handle_(); }
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
@ -423,86 +409,4 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
void ESPHomeOTAComponent::set_safe_mode_pending(const bool &pending) {
if (!this->has_safe_mode_)
return;
uint32_t current_rtc = this->read_rtc_();
if (pending && current_rtc != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) {
ESP_LOGI(TAG, "Device will enter safe mode on next boot");
this->write_rtc_(ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC);
}
if (!pending && current_rtc == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) {
ESP_LOGI(TAG, "Safe mode pending has been cleared");
this->clean_rtc();
}
}
bool ESPHomeOTAComponent::get_safe_mode_pending() {
return this->has_safe_mode_ && this->read_rtc_() == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC;
}
bool ESPHomeOTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
this->has_safe_mode_ = true;
this->safe_mode_start_time_ = millis();
this->safe_mode_enable_time_ = enable_time;
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
this->safe_mode_rtc_value_ = this->read_rtc_();
bool is_manual_safe_mode = this->safe_mode_rtc_value_ == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC;
if (is_manual_safe_mode) {
ESP_LOGI(TAG, "Safe mode has been entered manually");
} else {
ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts", this->safe_mode_rtc_value_);
}
if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) {
this->clean_rtc();
if (!is_manual_safe_mode) {
ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode");
}
this->status_set_error();
this->set_timeout(enable_time, []() {
ESP_LOGE(TAG, "No OTA attempt made, restarting");
App.reboot();
});
// Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised.
delay(300); // NOLINT
App.setup();
ESP_LOGI(TAG, "Waiting for OTA attempt");
return true;
} else {
// increment counter
this->write_rtc_(this->safe_mode_rtc_value_ + 1);
return false;
}
}
void ESPHomeOTAComponent::write_rtc_(uint32_t val) {
this->rtc_.save(&val);
global_preferences->sync();
}
uint32_t ESPHomeOTAComponent::read_rtc_() {
uint32_t val;
if (!this->rtc_.load(&val))
return 0;
return val;
}
void ESPHomeOTAComponent::clean_rtc() { this->write_rtc_(0); }
void ESPHomeOTAComponent::on_safe_shutdown() {
if (this->has_safe_mode_ && this->read_rtc_() != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC)
this->clean_rtc();
}
} // namespace esphome

View File

@ -15,17 +15,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
void set_auth_password(const std::string &password) { password_ = password; }
#endif // USE_OTA_PASSWORD
/// Manually set the port OTA should listen on.
/// Manually set the port OTA should listen on
void set_port(uint16_t port);
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time);
/// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending);
bool get_safe_mode_pending();
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
@ -33,14 +25,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
uint16_t get_port() const;
void clean_rtc();
void on_safe_shutdown() override;
protected:
void write_rtc_(uint32_t val);
uint32_t read_rtc_();
void handle_();
bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len);
@ -53,16 +38,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
std::unique_ptr<socket::Socket> server_;
std::unique_ptr<socket::Socket> client_;
bool has_safe_mode_{false}; ///< stores whether safe mode can be enabled
uint32_t safe_mode_start_time_; ///< stores when safe mode was enabled
uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should be on for
uint32_t safe_mode_rtc_value_;
uint8_t safe_mode_num_attempts_;
ESPPreferenceObject rtc_;
static const uint32_t ENTER_SAFE_MODE_MAGIC =
0x5afe5afe; ///< a magic number to indicate that safe mode should be entered on next boot
};
} // namespace esphome

View File

@ -46,7 +46,7 @@ template<typename... Ts> class BeeperOffAction : public Action<Ts...> {
template<typename... Ts> class VerticalAirflowAction : public Action<Ts...> {
public:
VerticalAirflowAction(HonClimate *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(AirflowVerticalDirection, direction)
TEMPLATABLE_VALUE(hon_protocol::VerticalSwingMode, direction)
void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); }
protected:
@ -56,7 +56,7 @@ template<typename... Ts> class VerticalAirflowAction : public Action<Ts...> {
template<typename... Ts> class HorizontalAirflowAction : public Action<Ts...> {
public:
HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction)
TEMPLATABLE_VALUE(hon_protocol::HorizontalSwingMode, direction)
void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); }
protected:

View File

@ -11,6 +11,7 @@ from ..climate import (
HonClimate,
)
CODEOWNERS = ["@paveldn"]
BinarySensorTypeEnum = HonClimate.enum("SubBinarySensorType", True)
# Haier sensors

View File

@ -0,0 +1,41 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import button
from ..climate import (
CONF_HAIER_ID,
HonClimate,
haier_ns,
)
CODEOWNERS = ["@paveldn"]
SelfCleaningButton = haier_ns.class_("SelfCleaningButton", button.Button)
SteriCleaningButton = haier_ns.class_("SteriCleaningButton", button.Button)
# Haier buttons
CONF_SELF_CLEANING = "self_cleaning"
CONF_STERI_CLEANING = "steri_cleaning"
# Additional icons
ICON_SPRAY_BOTTLE = "mdi:spray-bottle"
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_SELF_CLEANING): button.button_schema(
SelfCleaningButton,
icon=ICON_SPRAY_BOTTLE,
),
cv.Optional(CONF_STERI_CLEANING): button.button_schema(
SteriCleaningButton,
icon=ICON_SPRAY_BOTTLE,
),
}
)
async def to_code(config):
for button_type in [CONF_SELF_CLEANING, CONF_STERI_CLEANING]:
if conf := config.get(button_type):
btn = await button.new_button(conf)
await cg.register_parented(btn, config[CONF_HAIER_ID])

View File

@ -0,0 +1,9 @@
#include "self_cleaning.h"
namespace esphome {
namespace haier {
void SelfCleaningButton::press_action() { this->parent_->start_self_cleaning(); }
} // namespace haier
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hon_climate.h"
namespace esphome {
namespace haier {
class SelfCleaningButton : public button::Button, public Parented<HonClimate> {
public:
SelfCleaningButton() = default;
protected:
void press_action() override;
};
} // namespace haier
} // namespace esphome

View File

@ -0,0 +1,9 @@
#include "steri_cleaning.h"
namespace esphome {
namespace haier {
void SteriCleaningButton::press_action() { this->parent_->start_steri_cleaning(); }
} // namespace haier
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hon_climate.h"
namespace esphome {
namespace haier {
class SteriCleaningButton : public button::Button, public Parented<HonClimate> {
public:
SteriCleaningButton() = default;
protected:
void press_action() override;
};
} // namespace haier
} // namespace esphome

View File

@ -55,6 +55,7 @@ PROTOCOL_HON = "HON"
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
haier_ns = cg.esphome_ns.namespace("haier")
hon_protocol_ns = haier_ns.namespace("hon_protocol")
HaierClimateBase = haier_ns.class_(
"HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component
)
@ -63,7 +64,7 @@ Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase)
CONF_HAIER_ID = "haier_id"
AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection", True)
AirflowVerticalDirection = hon_protocol_ns.enum("VerticalSwingMode", True)
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
"HEALTH_UP": AirflowVerticalDirection.HEALTH_UP,
"MAX_UP": AirflowVerticalDirection.MAX_UP,
@ -73,7 +74,7 @@ AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
"HEALTH_DOWN": AirflowVerticalDirection.HEALTH_DOWN,
}
AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection", True)
AirflowHorizontalDirection = hon_protocol_ns.enum("HorizontalSwingMode", True)
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
"MAX_LEFT": AirflowHorizontalDirection.MAX_LEFT,
"LEFT": AirflowHorizontalDirection.LEFT,
@ -483,4 +484,4 @@ async def to_code(config):
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
)
# https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.25")
cg.add_library("pavlodn/HaierProtocol", "0.9.28")

View File

@ -234,6 +234,7 @@ void HaierClimateBase::setup() {
this->haier_protocol_.set_default_timeout_handler(
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
this->set_handlers();
this->initialization();
}
void HaierClimateBase::dump_config() {
@ -326,7 +327,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; }
void HaierClimateBase::control(const ClimateCall &call) {
ESP_LOGD("Control", "Control call");
if (this->protocol_phase_ < ProtocolPhases::IDLE) {
if (!this->valid_connection()) {
ESP_LOGW(TAG, "Can't send control packet, first poll answer not received");
return; // cancel the control, we cant do it without a poll answer.
}

View File

@ -44,7 +44,7 @@ class HaierClimateBase : public esphome::Component,
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets);
bool valid_connection() { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
size_t read_array(uint8_t *data, size_t len) noexcept override {
return esphome::uart::UARTDevice::read_array(data, len) ? len : 0;
@ -80,6 +80,7 @@ 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;
virtual haier_protocol::HaierMessage get_power_message(bool state) = 0;
virtual void initialization(){};
virtual bool prepare_pending_action();
virtual void process_protocol_reset();
esphome::climate::ClimateTraits traits() override;

View File

@ -19,38 +19,6 @@ constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
switch (direction) {
case AirflowVerticalDirection::HEALTH_UP:
return hon_protocol::VerticalSwingMode::HEALTH_UP;
case AirflowVerticalDirection::MAX_UP:
return hon_protocol::VerticalSwingMode::MAX_UP;
case AirflowVerticalDirection::UP:
return hon_protocol::VerticalSwingMode::UP;
case AirflowVerticalDirection::DOWN:
return hon_protocol::VerticalSwingMode::DOWN;
case AirflowVerticalDirection::HEALTH_DOWN:
return hon_protocol::VerticalSwingMode::HEALTH_DOWN;
default:
return hon_protocol::VerticalSwingMode::CENTER;
}
}
hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) {
switch (direction) {
case AirflowHorizontalDirection::MAX_LEFT:
return hon_protocol::HorizontalSwingMode::MAX_LEFT;
case AirflowHorizontalDirection::LEFT:
return hon_protocol::HorizontalSwingMode::LEFT;
case AirflowHorizontalDirection::RIGHT:
return hon_protocol::HorizontalSwingMode::RIGHT;
case AirflowHorizontalDirection::MAX_RIGHT:
return hon_protocol::HorizontalSwingMode::MAX_RIGHT;
default:
return hon_protocol::HorizontalSwingMode::CENTER;
}
}
HonClimate::HonClimate()
: cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
@ -66,17 +34,21 @@ void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; };
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
return this->current_vertical_swing_;
};
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
this->vertical_direction_ = direction;
void HonClimate::set_vertical_airflow(hon_protocol::VerticalSwingMode direction) {
this->pending_vertical_direction_ = direction;
this->force_send_control_ = true;
}
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
esphome::optional<hon_protocol::HorizontalSwingMode> HonClimate::get_horizontal_airflow() const {
return this->current_horizontal_swing_;
}
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
this->horizontal_direction_ = direction;
void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction) {
this->pending_horizontal_direction_ = direction;
this->force_send_control_ = true;
}
@ -148,6 +120,11 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haie
this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp);
strncpy(tmp, answr->device_name, 8);
this->hvac_hardware_info_.value().device_name_ = std::string(tmp);
#ifdef USE_TEXT_SENSOR
this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, this->hvac_hardware_info_.value().device_name_);
this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION,
this->hvac_hardware_info_.value().protocol_version_);
#endif
this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
this->hvac_hardware_info_.value().functions_[1] =
(answr->functions[1] & 0x02) != 0; // controller-device mode support
@ -488,6 +465,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);
HonSettings recovered;
if (this->rtc_.load(&recovered)) {
this->settings_ = recovered;
} else {
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER};
}
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
}
haier_protocol::HaierMessage HonClimate::get_control_message() {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
@ -560,16 +550,16 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
if (climate_control.swing_mode.has_value()) {
switch (climate_control.swing_mode.value()) {
case CLIMATE_SWING_OFF:
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
out_data->horizontal_swing_mode = (uint8_t) this->settings_.last_horizontal_swing;
out_data->vertical_swing_mode = (uint8_t) this->settings_.last_vertiacal_swing;
break;
case CLIMATE_SWING_VERTICAL:
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
out_data->horizontal_swing_mode = (uint8_t) this->settings_.last_horizontal_swing;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
break;
case CLIMATE_SWING_HORIZONTAL:
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
out_data->vertical_swing_mode = (uint8_t) this->settings_.last_vertiacal_swing;
break;
case CLIMATE_SWING_BOTH:
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
@ -631,11 +621,14 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
break;
}
}
} else {
if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO)
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
}
if (this->pending_vertical_direction_.has_value()) {
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
this->pending_vertical_direction_.reset();
}
if (this->pending_horizontal_direction_.has_value()) {
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;
control_out_buffer[4] = 0; // This byte should be cleared before setting values
@ -737,6 +730,33 @@ void HonClimate::update_sub_binary_sensor_(SubBinarySensorType type, uint8_t val
}
#endif // USE_BINARY_SENSOR
#ifdef USE_TEXT_SENSOR
void HonClimate::set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens) {
this->sub_text_sensors_[(size_t) type] = sens;
switch (type) {
case SubTextSensorType::APPLIANCE_NAME:
if (this->hvac_hardware_info_.has_value())
sens->publish_state(this->hvac_hardware_info_.value().device_name_);
break;
case SubTextSensorType::PROTOCOL_VERSION:
if (this->hvac_hardware_info_.has_value())
sens->publish_state(this->hvac_hardware_info_.value().protocol_version_);
break;
case SubTextSensorType::CLEANING_STATUS:
sens->publish_state(this->get_cleaning_status_text());
break;
default:
break;
}
}
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::string &value) {
size_t index = (size_t) type;
if (this->sub_text_sensors_[index] != nullptr)
this->sub_text_sensors_[index]->publish_state(value);
}
#endif // USE_TEXT_SENSOR
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
size_t expected_size = 2 + sizeof(hon_protocol::HaierPacketControl) + sizeof(hon_protocol::HaierPacketSensors) +
this->extra_control_packet_bytes_;
@ -896,6 +916,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()});
}
this->cleaning_status_ = new_cleaning;
#ifdef USE_TEXT_SENSOR
this->update_sub_text_sensor_(SubTextSensorType::CLEANING_STATUS, this->get_cleaning_status_text());
#endif // USE_TEXT_SENSOR
}
}
{
@ -941,6 +964,19 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
this->swing_mode = CLIMATE_SWING_OFF;
}
}
// Saving last known non auto mode for vertical and horizontal swing
this->current_vertical_swing_ = (hon_protocol::VerticalSwingMode) packet.control.vertical_swing_mode;
this->current_horizontal_swing_ = (hon_protocol::HorizontalSwingMode) packet.control.horizontal_swing_mode;
bool save_settings = ((this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO) &&
(this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO_SPECIAL) &&
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing)) ||
((this->current_horizontal_swing_.value() != hon_protocol::HorizontalSwingMode::AUTO) &&
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing));
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_);
}
should_publish = should_publish || (old_swing_mode != this->swing_mode);
}
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();

View File

@ -7,29 +7,16 @@
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include "esphome/core/automation.h"
#include "haier_base.h"
#include "hon_packet.h"
namespace esphome {
namespace haier {
enum class AirflowVerticalDirection : uint8_t {
HEALTH_UP = 0,
MAX_UP = 1,
UP = 2,
CENTER = 3,
DOWN = 4,
HEALTH_DOWN = 5,
};
enum class AirflowHorizontalDirection : uint8_t {
MAX_LEFT = 0,
LEFT = 1,
CENTER = 2,
RIGHT = 3,
MAX_RIGHT = 4,
};
enum class CleaningState : uint8_t {
NO_CLEANING = 0,
SELF_CLEAN = 1,
@ -38,6 +25,11 @@ enum class CleaningState : uint8_t {
enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER };
struct HonSettings {
hon_protocol::VerticalSwingMode last_vertiacal_swing;
hon_protocol::HorizontalSwingMode last_horizontal_swing;
};
class HonClimate : public HaierClimateBase {
#ifdef USE_SENSOR
public:
@ -80,6 +72,20 @@ class HonClimate : public HaierClimateBase {
protected:
void update_sub_binary_sensor_(SubBinarySensorType type, uint8_t value);
binary_sensor::BinarySensor *sub_binary_sensors_[(size_t) SubBinarySensorType::SUB_BINARY_SENSOR_TYPE_COUNT]{nullptr};
#endif
#ifdef USE_TEXT_SENSOR
public:
enum class SubTextSensorType {
CLEANING_STATUS = 0,
PROTOCOL_VERSION,
APPLIANCE_NAME,
SUB_TEXT_SENSOR_TYPE_COUNT,
};
void set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens);
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
public:
HonClimate();
@ -89,10 +95,10 @@ class HonClimate : public HaierClimateBase {
void dump_config() override;
void set_beeper_state(bool state);
bool get_beeper_state() const;
AirflowVerticalDirection get_vertical_airflow() const;
void set_vertical_airflow(AirflowVerticalDirection direction);
AirflowHorizontalDirection get_horizontal_airflow() const;
void set_horizontal_airflow(AirflowHorizontalDirection direction);
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;
void set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction);
std::string get_cleaning_status_text() const;
CleaningState get_cleaning_status() const;
void start_self_cleaning();
@ -108,6 +114,7 @@ class HonClimate : public HaierClimateBase {
void process_phase(std::chrono::steady_clock::time_point now) override;
haier_protocol::HaierMessage get_control_message() override;
haier_protocol::HaierMessage get_power_message(bool state) override;
void initialization() override;
bool prepare_pending_action() override;
void process_protocol_reset() override;
bool should_get_big_data_();
@ -147,9 +154,9 @@ class HonClimate : public HaierClimateBase {
bool beeper_status_;
CleaningState cleaning_status_;
bool got_valid_outdoor_temp_;
AirflowVerticalDirection vertical_direction_;
AirflowHorizontalDirection horizontal_direction_;
esphome::optional<HardwareInfo> hvac_hardware_info_;
esphome::optional<hon_protocol::VerticalSwingMode> pending_vertical_direction_{};
esphome::optional<hon_protocol::HorizontalSwingMode> pending_horizontal_direction_{};
esphome::optional<HardwareInfo> hvac_hardware_info_{};
uint8_t active_alarms_[8];
int extra_control_packet_bytes_;
HonControlMethod control_method_;
@ -159,6 +166,10 @@ class HonClimate : public HaierClimateBase {
float active_alarm_count_{NAN};
std::chrono::steady_clock::time_point last_alarm_request_;
int big_data_sensors_{0};
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
HonSettings settings_;
ESPPreferenceObject rtc_;
};
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {

View File

@ -13,7 +13,10 @@ enum class VerticalSwingMode : uint8_t {
UP = 0x04,
CENTER = 0x06,
DOWN = 0x08,
AUTO = 0x0C
MAX_DOWN = 0x0A,
AUTO = 0x0C,
// Auto for special modes
AUTO_SPECIAL = 0x0E
};
enum class HorizontalSwingMode : uint8_t {

View File

@ -31,6 +31,7 @@ from ..climate import (
HonClimate,
)
CODEOWNERS = ["@paveldn"]
SensorTypeEnum = HonClimate.enum("SubSensorType", True)
# Haier sensors

View File

@ -0,0 +1,54 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import (
ENTITY_CATEGORY_DIAGNOSTIC,
ENTITY_CATEGORY_NONE,
)
from ..climate import (
CONF_HAIER_ID,
HonClimate,
)
CODEOWNERS = ["@paveldn"]
TextSensorTypeEnum = HonClimate.enum("SubTextSensorType", True)
# Haier text sensors
CONF_CLEANING_STATUS = "cleaning_status"
CONF_PROTOCOL_VERSION = "protocol_version"
CONF_APPLIANCE_NAME = "appliance_name"
# Additional icons
ICON_SPRAY_BOTTLE = "mdi:spray-bottle"
ICON_TEXT_BOX = "mdi:text-box-outline"
TEXT_SENSOR_TYPES = {
CONF_CLEANING_STATUS: text_sensor.text_sensor_schema(
icon=ICON_SPRAY_BOTTLE,
entity_category=ENTITY_CATEGORY_NONE,
),
CONF_PROTOCOL_VERSION: text_sensor.text_sensor_schema(
icon=ICON_TEXT_BOX,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_APPLIANCE_NAME: text_sensor.text_sensor_schema(
icon=ICON_TEXT_BOX,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate),
}
).extend({cv.Optional(type): schema for type, schema in TEXT_SENSOR_TYPES.items()})
async def to_code(config):
paren = await cg.get_variable(config[CONF_HAIER_ID])
for type, _ in TEXT_SENSOR_TYPES.items():
if conf := config.get(type):
sens = await text_sensor.new_text_sensor(conf)
text_sensor_type = getattr(TextSensorTypeEnum, type.upper())
cg.add(paren.set_sub_text_sensor(text_sensor_type, sens))

View File

@ -52,12 +52,12 @@ float ledc_min_frequency_for_bit_depth(uint8_t bit_depth, bool low_frequency) {
}
optional<uint8_t> ledc_bit_depth_for_frequency(float frequency) {
ESP_LOGD(TAG, "Calculating resolution bit-depth for frequency %f", frequency);
ESP_LOGV(TAG, "Calculating resolution bit-depth for frequency %f", frequency);
for (int i = MAX_RES_BITS; i >= 1; i--) {
const float min_frequency = ledc_min_frequency_for_bit_depth(i, (frequency < 100));
const float max_frequency = ledc_max_frequency_for_bit_depth(i);
if (min_frequency <= frequency && frequency <= max_frequency) {
ESP_LOGD(TAG, "Resolution calculated as %d", i);
ESP_LOGV(TAG, "Resolution calculated as %d", i);
return i;
}
}

View File

@ -1,5 +1,9 @@
from __future__ import annotations
from typing import Literal
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.cpp_helpers import gpio_pin_expression
from esphome.components import uart
from esphome.const import (
@ -17,13 +21,21 @@ Modbus = modbus_ns.class_("Modbus", cg.Component, uart.UARTDevice)
ModbusDevice = modbus_ns.class_("ModbusDevice")
MULTI_CONF = True
CONF_ROLE = "role"
CONF_MODBUS_ID = "modbus_id"
CONF_SEND_WAIT_TIME = "send_wait_time"
ModbusRole = modbus_ns.enum("ModbusRole")
MODBUS_ROLES = {
"client": ModbusRole.CLIENT,
"server": ModbusRole.SERVER,
}
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Modbus),
cv.Optional(CONF_ROLE, default="client"): cv.enum(MODBUS_ROLES),
cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema,
cv.Optional(
CONF_SEND_WAIT_TIME, default="250ms"
@ -43,6 +55,7 @@ async def to_code(config):
await uart.register_uart_device(var, config)
cg.add(var.set_role(config[CONF_ROLE]))
if CONF_FLOW_CONTROL_PIN in config:
pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN])
cg.add(var.set_flow_control_pin(pin))
@ -62,6 +75,28 @@ def modbus_device_schema(default_address):
return cv.Schema(schema)
def final_validate_modbus_device(
name: str, *, role: Literal["server", "client"] | None = None
):
def validate_role(value):
assert role in MODBUS_ROLES
if value != role:
raise cv.Invalid(f"Component {name} requires role to be {role}")
return value
def validate_hub(hub_config):
hub_schema = {}
if role is not None:
hub_schema[cv.Required(CONF_ROLE)] = validate_role
return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config)
return cv.Schema(
{cv.Required(CONF_MODBUS_ID): fv.id_declaration_match_schema(validate_hub)},
extra=cv.ALLOW_EXTRA,
)
async def register_modbus_device(var, config):
parent = await cg.get_variable(config[CONF_MODBUS_ID])
cg.add(var.set_parent(parent))

View File

@ -77,7 +77,13 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
} else {
// the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
// data starts at 2 and length is 4 for read registers commands
if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) {
data_offset = 2;
data_len = 4;
}
// the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
data_offset = 2;
data_len = 4;
@ -123,6 +129,9 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
// Ignore modbus exception not related to a pending command
ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response");
}
} else if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) {
device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8),
uint16_t(data[3]) | (uint16_t(data[2]) << 8));
} else {
device->on_modbus_data(data);
}
@ -164,16 +173,18 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
std::vector<uint8_t> data;
data.push_back(address);
data.push_back(function_code);
data.push_back(start_address >> 8);
data.push_back(start_address >> 0);
if (function_code != 0x5 && function_code != 0x6) {
data.push_back(number_of_entities >> 8);
data.push_back(number_of_entities >> 0);
if (this->role == ModbusRole::CLIENT) {
data.push_back(start_address >> 8);
data.push_back(start_address >> 0);
if (function_code != 0x5 && function_code != 0x6) {
data.push_back(number_of_entities >> 8);
data.push_back(number_of_entities >> 0);
}
}
if (payload != nullptr) {
if (function_code == 0xF || function_code == 0x10) { // Write multiple
data.push_back(payload_len); // Byte count is required for write
if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) { // Write multiple
data.push_back(payload_len); // Byte count is required for write
} else {
payload_len = 2; // Write single register or coil
}

View File

@ -8,6 +8,11 @@
namespace esphome {
namespace modbus {
enum ModbusRole {
CLIENT,
SERVER,
};
class ModbusDevice;
class Modbus : public uart::UARTDevice, public Component {
@ -27,11 +32,14 @@ class Modbus : public uart::UARTDevice, public Component {
void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
uint8_t payload_len = 0, const uint8_t *payload = nullptr);
void send_raw(const std::vector<uint8_t> &payload);
void set_role(ModbusRole role) { this->role = role; }
void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
uint8_t waiting_for_response{0};
void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; }
void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; }
ModbusRole role;
protected:
GPIOPin *flow_control_pin_{nullptr};
@ -50,6 +58,7 @@ class ModbusDevice {
void set_address(uint8_t address) { address_ = address; }
virtual void on_modbus_data(const std::vector<uint8_t> &data) = 0;
virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){};
void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
const uint8_t *payload = nullptr) {
this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload);

View File

@ -23,6 +23,8 @@ CODEOWNERS = ["@martgras"]
AUTO_LOAD = ["modbus"]
CONF_READ_LAMBDA = "read_lambda"
CONF_SERVER_REGISTERS = "server_registers"
MULTI_CONF = True
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
@ -31,6 +33,7 @@ ModbusController = modbus_controller_ns.class_(
)
SensorItem = modbus_controller_ns.struct("SensorItem")
ServerRegister = modbus_controller_ns.struct("ServerRegister")
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode")
@ -94,10 +97,18 @@ TYPE_REGISTER_MAP = {
"FP32_R": 2,
}
MULTI_CONF = True
_LOGGER = logging.getLogger(__name__)
ModbusServerRegisterSchema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ServerRegister),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@ -106,6 +117,9 @@ CONFIG_SCHEMA = cv.All(
CONF_COMMAND_THROTTLE, default="0ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
cv.Optional(
CONF_SERVER_REGISTERS,
): cv.ensure_list(ModbusServerRegisterSchema),
}
)
.extend(cv.polling_component_schema("60s"))
@ -154,6 +168,17 @@ def validate_modbus_register(config):
return config
def _final_validate(config):
if CONF_SERVER_REGISTERS in config:
return modbus.final_validate_modbus_device("modbus_controller", role="server")(
config
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
def modbus_calc_properties(config):
byte_offset = 0
reg_count = 0
@ -183,7 +208,7 @@ def modbus_calc_properties(config):
async def add_modbus_base_properties(
var, config, sensor_type, lamdba_param_type=cg.float_, lamdba_return_type=float
var, config, sensor_type, lambda_param_type=cg.float_, lambda_return_type=float
):
if CONF_CUSTOM_COMMAND in config:
cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND]))
@ -196,13 +221,13 @@ async def add_modbus_base_properties(
config[CONF_LAMBDA],
[
(sensor_type.operator("ptr"), "item"),
(lamdba_param_type, "x"),
(lambda_param_type, "x"),
(
cg.std_vector.template(cg.uint8).operator("const").operator("ref"),
"data",
),
],
return_type=cg.optional.template(lamdba_return_type),
return_type=cg.optional.template(lambda_return_type),
)
cg.add(var.set_template(template_))
@ -211,6 +236,23 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
if CONF_SERVER_REGISTERS in config:
for server_register in config[CONF_SERVER_REGISTERS]:
cg.add(
var.add_server_register(
cg.new_Pvariable(
server_register[CONF_ID],
server_register[CONF_ADDRESS],
server_register[CONF_VALUE_TYPE],
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
await cg.process_lambda(
server_register[CONF_READ_LAMBDA],
[],
return_type=cg.float_,
),
)
)
)
await register_modbus_device(var, config)

View File

@ -7,10 +7,7 @@ namespace modbus_controller {
static const char *const TAG = "modbus_controller";
void ModbusController::setup() {
// Modbus::setup();
this->create_register_ranges_();
}
void ModbusController::setup() { this->create_register_ranges_(); }
/*
To work with the existing modbus class and avoid polling for responses a command queue is used.
@ -102,6 +99,51 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_
}
}
void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
ESP_LOGD(TAG,
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
std::vector<uint16_t> sixteen_bit_response;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
float value = server_register->read_lambda();
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.",
server_register->address, static_cast<uint8_t>(server_register->value_type),
server_register->register_count, value);
number_to_payload(sixteen_bit_response, value, server_register->value_type);
current_address += server_register->register_count;
found = true;
break;
}
}
if (!found) {
ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address);
std::vector<uint8_t> error_response;
error_response.push_back(this->address_);
error_response.push_back(0x81);
error_response.push_back(0x02);
this->send_raw(error_response);
return;
}
}
std::vector<uint8_t> response;
for (auto v : sixteen_bit_response) {
auto decoded_value = decode_value(v);
response.push_back(decoded_value[0]);
response.push_back(decoded_value[1]);
}
this->send(function_code, start_address, number_of_registers, response.size(), response.data());
}
SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
auto reg_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) {
return (r.start_address == start_address && r.register_type == register_type);
@ -190,7 +232,7 @@ void ModbusController::update() {
// walk through the sensors and determine the register ranges to read
size_t ModbusController::create_register_ranges_() {
register_ranges_.clear();
if (sensorset_.empty()) {
if (this->parent_->role == modbus::ModbusRole::CLIENT && sensorset_.empty()) {
ESP_LOGW(TAG, "No sensors registered");
return 0;
}
@ -309,6 +351,11 @@ void ModbusController::dump_config() {
ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
it.start_address, it.register_count, it.skip_updates);
}
ESP_LOGCONFIG(TAG, "server registers");
for (auto &r : server_registers_) {
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address,
static_cast<uint8_t>(r->value_type), r->register_count);
}
#endif
}

View File

@ -8,6 +8,7 @@
#include <list>
#include <queue>
#include <set>
#include <utility>
#include <vector>
namespace esphome {
@ -251,6 +252,21 @@ class SensorItem {
bool force_new_range{false};
};
class ServerRegister {
public:
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count,
std::function<float()> read_lambda) {
this->address = address;
this->value_type = value_type;
this->register_count = register_count;
this->read_lambda = std::move(read_lambda);
}
uint16_t address;
SensorValueType value_type;
uint8_t register_count;
std::function<float()> read_lambda;
};
// ModbusController::create_register_ranges_ tries to optimize register range
// for this the sensors must be ordered by register_type, start_address and bitmask
class SensorItemsComparator {
@ -418,10 +434,14 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void queue_command(const ModbusCommandItem &command);
/// Registers a sensor with the controller. Called by esphomes code generator
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
/// Registers a server register with the controller. Called by esphomes code generator
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
/// called when a modbus response was parsed without errors
void on_modbus_data(const std::vector<uint8_t> &data) override;
/// called when a modbus error response was received
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
/// called when a modbus request (function code 3 or 4) was parsed without errors
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
/// default delegate called by process_modbus_data when a response for a write response has retrieved from the
@ -452,6 +472,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void dump_sensors_();
/// Collection of all sensors for this component
SensorSet sensorset_;
/// Collection of all server registers for this component
std::vector<ServerRegister *> server_registers_;
/// Continuous range of modbus registers
std::vector<RegisterRange> register_ranges_;
/// Hold the pending requests to be sent

View File

@ -1,19 +1,32 @@
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome import pins
from esphome.components import i2c
from esphome.const import CONF_ID
from esphome.const import (
CONF_BINARY_SENSOR,
CONF_CHANNEL,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
)
CONF_TOUCH_THRESHOLD = "touch_threshold"
CONF_RELEASE_THRESHOLD = "release_threshold"
CONF_TOUCH_DEBOUNCE = "touch_debounce"
CONF_RELEASE_DEBOUNCE = "release_debounce"
CONF_MAX_TOUCH_CHANNEL = "max_touch_channel"
CONF_MPR121 = "mpr121"
CONF_MPR121_ID = "mpr121_id"
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["binary_sensor"]
mpr121_ns = cg.esphome_ns.namespace("mpr121")
CONF_MPR121_ID = "mpr121_id"
MPR121Component = mpr121_ns.class_("MPR121Component", cg.Component, i2c.I2CDevice)
MPR121GPIOPin = mpr121_ns.class_("MPR121GPIOPin", cg.GPIOPin)
MULTI_CONF = True
CONFIG_SCHEMA = (
@ -28,6 +41,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_RELEASE_THRESHOLD, default=0x06): cv.int_range(
min=0x05, max=0x30
),
cv.Optional(CONF_MAX_TOUCH_CHANNEL): cv.int_range(min=3, max=11),
}
)
.extend(cv.COMPONENT_SCHEMA)
@ -35,11 +49,79 @@ CONFIG_SCHEMA = (
)
def _final_validate(config):
fconf = fv.full_config.get()
max_touch_channel = 3
if (binary_sensors := fconf.get(CONF_BINARY_SENSOR)) is not None:
for binary_sensor in binary_sensors:
if binary_sensor.get(CONF_MPR121_ID) == config[CONF_ID]:
max_touch_channel = max(max_touch_channel, binary_sensor[CONF_CHANNEL])
if max_touch_channel_in_config := config.get(CONF_MAX_TOUCH_CHANNEL):
if max_touch_channel != max_touch_channel_in_config:
raise cv.Invalid(
"Max touch channel must equal the highest binary sensor channel or be removed for auto calculation",
path=[CONF_MAX_TOUCH_CHANNEL],
)
path = fconf.get_path_for_id(config[CONF_ID])[:-1]
this_config = fconf.get_config_for_path(path)
this_config[CONF_MAX_TOUCH_CHANNEL] = max_touch_channel
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_touch_debounce(config[CONF_TOUCH_DEBOUNCE]))
cg.add(var.set_release_debounce(config[CONF_RELEASE_DEBOUNCE]))
cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD]))
cg.add(var.set_release_threshold(config[CONF_RELEASE_THRESHOLD]))
cg.add(var.set_max_touch_channel(config[CONF_MAX_TOUCH_CHANNEL]))
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
def validate_mode(value):
if bool(value[CONF_INPUT]) == bool(value[CONF_OUTPUT]):
raise cv.Invalid("Mode must be either input or output")
return value
# https://www.nxp.com/docs/en/data-sheet/MPR121.pdf, page 4
#
# Among the 12 electrode inputs, 8 inputs are designed as multifunctional pins. When these pins are
# not configured as electrodes, they may be used to drive LEDs or used for general purpose input or
# output.
MPR121_GPIO_PIN_SCHEMA = pins.gpio_base_schema(
MPR121GPIOPin,
cv.int_range(min=4, max=11),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
).extend(
{
cv.Required(CONF_MPR121): cv.use_id(MPR121Component),
}
)
def mpr121_pin_final_validate(pin_config, parent_config):
if pin_config[CONF_NUMBER] <= parent_config[CONF_MAX_TOUCH_CHANNEL]:
raise cv.Invalid(
"Pin number must be higher than the max touch channel of the MPR121 component",
)
@pins.PIN_SCHEMA_REGISTRY.register(
CONF_MPR121, MPR121_GPIO_PIN_SCHEMA, mpr121_pin_final_validate
)
async def mpr121_gpio_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_MPR121])
cg.add(var.set_parent(parent))
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import CONF_CHANNEL
from . import (
from .. import (
mpr121_ns,
MPR121Component,
CONF_MPR121_ID,
@ -11,9 +11,9 @@ from . import (
)
DEPENDENCIES = ["mpr121"]
MPR121Channel = mpr121_ns.class_("MPR121Channel", binary_sensor.BinarySensor)
MPR121BinarySensor = mpr121_ns.class_("MPR121BinarySensor", binary_sensor.BinarySensor)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(MPR121Channel).extend(
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(MPR121BinarySensor).extend(
{
cv.GenerateID(CONF_MPR121_ID): cv.use_id(MPR121Component),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=11),
@ -27,6 +27,7 @@ async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
hub = await cg.get_variable(config[CONF_MPR121_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.register_parented(var, hub)
if CONF_TOUCH_THRESHOLD in config:
cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD]))

View File

@ -0,0 +1,20 @@
#include "mpr121_binary_sensor.h"
namespace esphome {
namespace mpr121 {
void MPR121BinarySensor::setup() {
uint8_t touch_threshold = this->touch_threshold_.value_or(this->parent_->get_touch_threshold());
this->parent_->write_byte(MPR121_TOUCHTH_0 + 2 * this->channel_, touch_threshold);
uint8_t release_threshold = this->release_threshold_.value_or(this->parent_->get_release_threshold());
this->parent_->write_byte(MPR121_RELEASETH_0 + 2 * this->channel_, release_threshold);
}
void MPR121BinarySensor::process(uint16_t data) {
bool new_state = data & (1 << this->channel_);
this->publish_state(new_state);
}
} // namespace mpr121
} // namespace esphome

View File

@ -0,0 +1,26 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "../mpr121.h"
namespace esphome {
namespace mpr121 {
class MPR121BinarySensor : public binary_sensor::BinarySensor, public MPR121Channel, public Parented<MPR121Component> {
public:
void set_channel(uint8_t channel) { this->channel_ = channel; }
void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; };
void set_release_threshold(uint8_t release_threshold) { this->release_threshold_ = release_threshold; };
void setup() override;
void process(uint16_t data) override;
protected:
uint8_t channel_{0};
optional<uint8_t> touch_threshold_{};
optional<uint8_t> release_threshold_{};
};
} // namespace mpr121
} // namespace esphome

View File

@ -1,6 +1,9 @@
#include "mpr121.h"
#include "esphome/core/log.h"
#include <cstdint>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace mpr121 {
@ -20,10 +23,7 @@ void MPR121Component::setup() {
// set touch sensitivity for all 12 channels
for (auto *channel : this->channels_) {
this->write_byte(MPR121_TOUCHTH_0 + 2 * channel->channel_,
channel->touch_threshold_.value_or(this->touch_threshold_));
this->write_byte(MPR121_RELEASETH_0 + 2 * channel->channel_,
channel->release_threshold_.value_or(this->release_threshold_));
channel->setup();
}
this->write_byte(MPR121_MHDR, 0x01);
this->write_byte(MPR121_NHDR, 0x01);
@ -44,8 +44,15 @@ void MPR121Component::setup() {
this->write_byte(MPR121_CONFIG1, 0x10);
// 0.5uS encoding, 1ms period
this->write_byte(MPR121_CONFIG2, 0x20);
// start with first 5 bits of baseline tracking
this->write_byte(MPR121_ECR, 0x8F);
// Write the Electrode Configuration Register
// * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits.
// * The 2 bits below is "Proximity Enable" and are left at 0.
// * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled
// as a range, starting at 0 up to the highest channel index used.
this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1));
this->flush_gpio_();
}
void MPR121Component::set_touch_debounce(uint8_t debounce) {
@ -86,6 +93,72 @@ void MPR121Component::loop() {
for (auto *channel : this->channels_)
channel->process(val);
this->read_byte(MPR121_GPIODATA, &this->gpio_input_);
}
bool MPR121Component::digital_read(uint8_t ionum) { return (this->gpio_input_ & (1 << ionum)) != 0; }
void MPR121Component::digital_write(uint8_t ionum, bool value) {
if (value) {
this->gpio_output_ |= (1 << ionum);
} else {
this->gpio_output_ &= ~(1 << ionum);
}
this->flush_gpio_();
}
void MPR121Component::pin_mode(uint8_t ionum, gpio::Flags flags) {
this->gpio_enable_ |= (1 << ionum);
if (flags & gpio::FLAG_INPUT) {
this->gpio_direction_ &= ~(1 << ionum);
} else if (flags & gpio::FLAG_OUTPUT) {
this->gpio_direction_ |= 1 << ionum;
}
this->flush_gpio_();
}
bool MPR121Component::flush_gpio_() {
if (this->is_failed()) {
return false;
}
// TODO: The CTL registers can configure internal pullup/pulldown resistors.
this->write_byte(MPR121_GPIOCTL0, 0x00);
this->write_byte(MPR121_GPIOCTL1, 0x00);
this->write_byte(MPR121_GPIOEN, this->gpio_enable_);
this->write_byte(MPR121_GPIODIR, this->gpio_direction_);
if (!this->write_byte(MPR121_GPIODATA, this->gpio_output_)) {
this->status_set_warning();
return false;
}
this->status_clear_warning();
return true;
}
void MPR121GPIOPin::setup() { this->pin_mode(this->flags_); }
void MPR121GPIOPin::pin_mode(gpio::Flags flags) {
assert(this->pin_ >= 4);
this->parent_->pin_mode(this->pin_ - 4, flags);
}
bool MPR121GPIOPin::digital_read() {
assert(this->pin_ >= 4);
return this->parent_->digital_read(this->pin_ - 4) != this->inverted_;
}
void MPR121GPIOPin::digital_write(bool value) {
assert(this->pin_ >= 4);
this->parent_->digital_write(this->pin_ - 4, value != this->inverted_);
}
std::string MPR121GPIOPin::dump_summary() const {
char buffer[32];
snprintf(buffer, sizeof(buffer), "ELE%u on MPR121", this->pin_);
return buffer;
}
} // namespace mpr121

View File

@ -1,8 +1,10 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include <vector>
@ -39,6 +41,9 @@ enum {
MPR121_UPLIMIT = 0x7D,
MPR121_LOWLIMIT = 0x7E,
MPR121_TARGETLIMIT = 0x7F,
MPR121_GPIOCTL0 = 0x73,
MPR121_GPIOCTL1 = 0x74,
MPR121_GPIODATA = 0x75,
MPR121_GPIODIR = 0x76,
MPR121_GPIOEN = 0x77,
MPR121_GPIOSET = 0x78,
@ -47,19 +52,10 @@ enum {
MPR121_SOFTRESET = 0x80,
};
class MPR121Channel : public binary_sensor::BinarySensor {
friend class MPR121Component;
class MPR121Channel {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
void process(uint16_t data) { this->publish_state(static_cast<bool>(data & (1 << this->channel_))); }
void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; };
void set_release_threshold(uint8_t release_threshold) { this->release_threshold_ = release_threshold; };
protected:
uint8_t channel_{0};
optional<uint8_t> touch_threshold_{};
optional<uint8_t> release_threshold_{};
virtual void setup() = 0;
virtual void process(uint16_t data) = 0;
};
class MPR121Component : public Component, public i2c::I2CDevice {
@ -69,23 +65,63 @@ class MPR121Component : public Component, public i2c::I2CDevice {
void set_release_debounce(uint8_t debounce);
void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; };
void set_release_threshold(uint8_t release_threshold) { this->release_threshold_ = release_threshold; };
uint8_t get_touch_threshold() { return this->touch_threshold_; };
uint8_t get_release_threshold() { return this->release_threshold_; };
uint8_t get_touch_threshold() const { return this->touch_threshold_; };
uint8_t get_release_threshold() const { return this->release_threshold_; };
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
float get_setup_priority() const override { return setup_priority::IO; }
void loop() override;
void set_max_touch_channel(uint8_t max_touch_channel) { this->max_touch_channel_ = max_touch_channel; }
// GPIO helper functions.
bool digital_read(uint8_t ionum);
void digital_write(uint8_t ionum, bool value);
void pin_mode(uint8_t ionum, gpio::Flags flags);
protected:
std::vector<MPR121Channel *> channels_{};
uint8_t debounce_{0};
uint8_t touch_threshold_{};
uint8_t release_threshold_{};
uint8_t max_touch_channel_{3};
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
WRONG_CHIP_STATE,
} error_code_{NONE};
bool flush_gpio_();
/// The enable mask - zero means high Z, 1 means GPIO usage
uint8_t gpio_enable_{0x00};
/// Mask for the pin mode - 1 means output, 0 means input
uint8_t gpio_direction_{0x00};
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint8_t gpio_output_{0x00};
/// The mask to read as input state - 1 means HIGH, 0 means LOW
uint8_t gpio_input_{0x00};
};
/// Helper class to expose a MPR121 pin as an internal input GPIO pin.
class MPR121GPIOPin : public GPIOPin {
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_parent(MPR121Component *parent) { this->parent_ = parent; }
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:
MPR121Component *parent_;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace mpr121

View File

@ -952,6 +952,73 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
bool set_protocol_reparse_mode(bool active_mode);
// ======== Nextion Intelligent Series ========
/**
* Set the video id of a component.
* @param component The component name.
* @param vid_id The video ID.
*
* Example:
* ```cpp
* it.set_component_vid("textview", 1);
* ```
*
* This will change the video id of the component `textview`.
*
* Note: Requires Nextion Intelligent series display.
*/
void set_component_vid(const char *component, uint8_t vid_id);
/**
* Set the drag availability of a component.
* @param component The component name.
* @param drag False: Drag not available, True: Drag available.
*
* Example:
* ```cpp
* it.set_component_drag("textview", true);
* ```
*
* This will enable drag to the component `textview`.
*
* Note: Requires Nextion Intelligent series display.
*/
void set_component_drag(const char *component, bool drag);
/**
* Set the opaqueness (fading) of a component.
* @param component The component name.
* @param aph An integer between 0 and 127 related to the opaqueness/fading level.
*
* Example:
* ```cpp
* it.set_component_aph("textview", 64);
* ```
*
* This will set the opaqueness level of the component `textview` to 64.
*
* Note: Requires Nextion Intelligent series display.
*/
void set_component_aph(const char *component, uint8_t aph);
/**
* Set the position of a component.
* @param component The component name.
* @param x The new X (horizontal) coordinate for the component.
* @param y The new Y (vertical) coordinate for the component.
*
* Example:
* ```cpp
* it.set_component_aph("textview", 64, 35);
* ```
*
* This will move the component `textview` to the column 64 of row 35 of the display.
*
* Note: Requires Nextion Intelligent series display.
*/
void set_component_position(const char *component, uint32_t x, uint32_t y);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void register_touch_component(NextionComponentBase *obj) { this->touch_.push_back(obj); }

View File

@ -148,7 +148,25 @@ void Nextion::set_component_pic(const char *component, uint8_t pic_id) {
}
void Nextion::set_component_picc(const char *component, uint8_t pic_id) {
this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.picc=%" PRIu8, component, pic_id);
this->add_no_result_to_queue_with_printf_("set_component_picc", "%s.picc=%" PRIu8, component, pic_id);
}
// Set video
void Nextion::set_component_vid(const char *component, uint8_t vid_id) {
this->add_no_result_to_queue_with_printf_("set_component_vid", "%s.vid=%" PRIu8, component, vid_id);
}
void Nextion::set_component_drag(const char *component, bool drag) {
this->add_no_result_to_queue_with_printf_("set_component_drag", "%s.drag=%i", component, drag ? 1 : 0);
}
void Nextion::set_component_aph(const char *component, uint8_t aph) {
this->add_no_result_to_queue_with_printf_("set_component_aph", "%s.aph=%" PRIu8, component, aph);
}
void Nextion::set_component_position(const char *component, uint32_t x, uint32_t y) {
this->add_no_result_to_queue_with_printf_("set_component_position_x", "%s.x=%" PRIu32, component, x);
this->add_no_result_to_queue_with_printf_("set_component_position_y", "%s.y=%" PRIu32, component, y);
}
void Nextion::set_component_text_printf(const char *component, const char *format, ...) {

View File

@ -11,7 +11,7 @@ CODEOWNERS = ["@esphome/core"]
def AUTO_LOAD():
if CORE.is_nrf52:
return []
return ["md5"]
return ["md5", "safe_mode"]
IS_PLATFORM_COMPONENT = True
@ -82,7 +82,7 @@ BASE_OTA_SCHEMA = cv.Schema(
)
@coroutine_with_priority(51.0)
@coroutine_with_priority(54.0)
async def to_code(config):
cg.add_define("USE_OTA")

View File

@ -63,7 +63,13 @@ def validate_tolerance(value):
if "%" in str(value):
type_ = TYPE_PERCENTAGE
else:
type_ = TYPE_TIME
try:
cv.positive_time_period_microseconds(value)
type_ = TYPE_TIME
except cv.Invalid as exc:
raise cv.Invalid(
"Tolerance must be a percentage or time. Configurations made before 2024.5.0 treated the value as a percentage."
) from exc
return TOLERANCE_SCHEMA(
{

View File

@ -1,5 +1,70 @@
from esphome.cpp_generator import RawExpression
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_DISABLED,
CONF_ID,
CONF_NUM_ATTEMPTS,
CONF_REBOOT_TIMEOUT,
CONF_SAFE_MODE,
CONF_TRIGGER_ID,
KEY_PAST_SAFE_MODE,
)
from esphome.core import CORE, coroutine_with_priority
from esphome import automation
CODEOWNERS = ["@paulmonigatti", "@jsuanet"]
CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"]
CONF_ON_SAFE_MODE = "on_safe_mode"
safe_mode_ns = cg.esphome_ns.namespace("safe_mode")
SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component)
SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template())
def _remove_id_if_disabled(value):
value = value.copy()
if value[CONF_DISABLED]:
value.pop(CONF_ID)
return value
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SafeModeComponent),
cv.Optional(CONF_DISABLED, default=False): cv.boolean,
cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
cv.Optional(
CONF_REBOOT_TIMEOUT, default="5min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SafeModeTrigger),
}
),
}
).extend(cv.COMPONENT_SCHEMA),
_remove_id_if_disabled,
)
@coroutine_with_priority(50.0)
async def to_code(config):
if config[CONF_DISABLED]:
return
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for conf in config.get(CONF_ON_SAFE_MODE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
condition = var.should_enter_safe_mode(
config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT]
)
cg.add(RawExpression(f"if ({condition}) return"))
CORE.data[CONF_SAFE_MODE] = {}
CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True

View File

@ -0,0 +1,17 @@
#pragma once
#include "safe_mode.h"
#include "esphome/core/automation.h"
namespace esphome {
namespace safe_mode {
class SafeModeTrigger : public Trigger<> {
public:
explicit SafeModeTrigger(SafeModeComponent *parent) {
parent->add_on_safe_mode_callback([this, parent]() { trigger(); });
}
};
} // namespace safe_mode
} // namespace esphome

View File

@ -1,16 +1,15 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import button
from esphome.components.esphome.ota import ESPHomeOTAComponent
from esphome.const import (
CONF_ESPHOME,
CONF_SAFE_MODE,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ICON_RESTART_ALERT,
)
from .. import safe_mode_ns
from .. import safe_mode_ns, SafeModeComponent
DEPENDENCIES = ["ota.esphome"]
DEPENDENCIES = ["safe_mode"]
SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component)
@ -21,7 +20,7 @@ CONFIG_SCHEMA = (
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
)
.extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(ESPHomeOTAComponent)})
.extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
.extend(cv.COMPONENT_SCHEMA)
)
@ -30,5 +29,5 @@ async def to_code(config):
var = await button.new_button(config)
await cg.register_component(var, config)
ota = await cg.get_variable(config[CONF_ESPHOME])
cg.add(var.set_ota(ota))
safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE])
cg.add(var.set_safe_mode(safe_mode_component))

View File

@ -8,11 +8,13 @@ namespace safe_mode {
static const char *const TAG = "safe_mode.button";
void SafeModeButton::set_ota(esphome::ESPHomeOTAComponent *ota) { this->ota_ = ota; }
void SafeModeButton::set_safe_mode(SafeModeComponent *safe_mode_component) {
this->safe_mode_component_ = safe_mode_component;
}
void SafeModeButton::press_action() {
ESP_LOGI(TAG, "Restarting device in safe mode...");
this->ota_->set_safe_mode_pending(true);
this->safe_mode_component_->set_safe_mode_pending(true);
// Let MQTT settle a bit
delay(100); // NOLINT

View File

@ -1,7 +1,7 @@
#pragma once
#include "esphome/components/button/button.h"
#include "esphome/components/esphome/ota/ota_esphome.h"
#include "esphome/components/safe_mode/safe_mode.h"
#include "esphome/core/component.h"
namespace esphome {
@ -10,10 +10,10 @@ namespace safe_mode {
class SafeModeButton : public button::Button, public Component {
public:
void dump_config() override;
void set_ota(esphome::ESPHomeOTAComponent *ota);
void set_safe_mode(SafeModeComponent *safe_mode_component);
protected:
esphome::ESPHomeOTAComponent *ota_;
SafeModeComponent *safe_mode_component_;
void press_action() override;
};

View File

@ -0,0 +1,127 @@
#include "safe_mode.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include <cerrno>
#include <cinttypes>
#include <cstdio>
namespace esphome {
namespace safe_mode {
static const char *const TAG = "safe_mode";
void SafeModeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Safe Mode:");
ESP_LOGCONFIG(TAG, " Invoke after %u boot attempts", this->safe_mode_num_attempts_);
ESP_LOGCONFIG(TAG, " Remain in safe mode for %" PRIu32 " seconds",
this->safe_mode_enable_time_ / 1000); // because milliseconds
if (this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) {
auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_;
if (remaining_restarts) {
ESP_LOGW(TAG, "Last reset occurred too quickly; safe mode will be invoked in %" PRIu32 " restarts",
remaining_restarts);
} else {
ESP_LOGW(TAG, "SAFE MODE IS ACTIVE");
}
}
}
float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void SafeModeComponent::loop() {
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) {
// successful boot, reset counter
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->clean_rtc();
this->boot_successful_ = true;
}
}
void SafeModeComponent::set_safe_mode_pending(const bool &pending) {
uint32_t current_rtc = this->read_rtc_();
if (pending && current_rtc != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) {
ESP_LOGI(TAG, "Device will enter safe mode on next boot");
this->write_rtc_(SafeModeComponent::ENTER_SAFE_MODE_MAGIC);
}
if (!pending && current_rtc == SafeModeComponent::ENTER_SAFE_MODE_MAGIC) {
ESP_LOGI(TAG, "Safe mode pending has been cleared");
this->clean_rtc();
}
}
bool SafeModeComponent::get_safe_mode_pending() {
return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
}
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
this->safe_mode_start_time_ = millis();
this->safe_mode_enable_time_ = enable_time;
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
this->safe_mode_rtc_value_ = this->read_rtc_();
bool is_manual_safe_mode = this->safe_mode_rtc_value_ == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
if (is_manual_safe_mode) {
ESP_LOGI(TAG, "Safe mode invoked manually");
} else {
ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts", this->safe_mode_rtc_value_);
}
if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) {
this->clean_rtc();
if (!is_manual_safe_mode) {
ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode");
}
this->status_set_error();
this->set_timeout(enable_time, []() {
ESP_LOGW(TAG, "Safe mode enable time has elapsed -- restarting");
App.reboot();
});
// Delay here to allow power to stabilize before Wi-Fi/Ethernet is initialised
delay(300); // NOLINT
App.setup();
ESP_LOGW(TAG, "SAFE MODE IS ACTIVE");
this->safe_mode_callback_.call();
return true;
} else {
// increment counter
this->write_rtc_(this->safe_mode_rtc_value_ + 1);
return false;
}
}
void SafeModeComponent::write_rtc_(uint32_t val) {
this->rtc_.save(&val);
global_preferences->sync();
}
uint32_t SafeModeComponent::read_rtc_() {
uint32_t val;
if (!this->rtc_.load(&val))
return 0;
return val;
}
void SafeModeComponent::clean_rtc() { this->write_rtc_(0); }
void SafeModeComponent::on_safe_shutdown() {
if (this->read_rtc_() != SafeModeComponent::ENTER_SAFE_MODE_MAGIC)
this->clean_rtc();
}
} // namespace safe_mode
} // namespace esphome

View File

@ -0,0 +1,49 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace safe_mode {
/// SafeModeComponent provides a safe way to recover from repeated boot failures
class SafeModeComponent : public Component {
public:
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time);
/// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending);
bool get_safe_mode_pending();
void dump_config() override;
float get_setup_priority() const override;
void loop() override;
void clean_rtc();
void on_safe_shutdown() override;
void add_on_safe_mode_callback(std::function<void()> &&callback) {
this->safe_mode_callback_.add(std::move(callback));
}
protected:
void write_rtc_(uint32_t val);
uint32_t read_rtc_();
bool boot_successful_{false}; ///< set to true after boot is considered successful
uint32_t safe_mode_start_time_; ///< stores when safe mode was enabled
uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for
uint32_t safe_mode_rtc_value_;
uint8_t safe_mode_num_attempts_;
ESPPreferenceObject rtc_;
CallbackManager<void()> safe_mode_callback_{};
static const uint32_t ENTER_SAFE_MODE_MAGIC =
0x5afe5afe; ///< a magic number to indicate that safe mode should be entered on next boot
};
} // namespace safe_mode
} // namespace esphome

View File

@ -1,15 +1,14 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import switch
from esphome.components.esphome.ota import ESPHomeOTAComponent
from esphome.const import (
CONF_ESPHOME,
CONF_SAFE_MODE,
ENTITY_CATEGORY_CONFIG,
ICON_RESTART_ALERT,
)
from .. import safe_mode_ns
from .. import safe_mode_ns, SafeModeComponent
DEPENDENCIES = ["ota.esphome"]
DEPENDENCIES = ["safe_mode"]
SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component)
@ -20,7 +19,7 @@ CONFIG_SCHEMA = (
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
)
.extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(ESPHomeOTAComponent)})
.extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
.extend(cv.COMPONENT_SCHEMA)
)
@ -29,5 +28,5 @@ async def to_code(config):
var = await switch.new_switch(config)
await cg.register_component(var, config)
ota = await cg.get_variable(config[CONF_ESPHOME])
cg.add(var.set_ota(ota))
safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE])
cg.add(var.set_safe_mode(safe_mode_component))

View File

@ -6,9 +6,11 @@
namespace esphome {
namespace safe_mode {
static const char *const TAG = "safe_mode_switch";
static const char *const TAG = "safe_mode.switch";
void SafeModeSwitch::set_ota(esphome::ESPHomeOTAComponent *ota) { this->ota_ = ota; }
void SafeModeSwitch::set_safe_mode(SafeModeComponent *safe_mode_component) {
this->safe_mode_component_ = safe_mode_component;
}
void SafeModeSwitch::write_state(bool state) {
// Acknowledge
@ -16,13 +18,14 @@ void SafeModeSwitch::write_state(bool state) {
if (state) {
ESP_LOGI(TAG, "Restarting device in safe mode...");
this->ota_->set_safe_mode_pending(true);
this->safe_mode_component_->set_safe_mode_pending(true);
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
}
void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); }
} // namespace safe_mode

View File

@ -1,6 +1,6 @@
#pragma once
#include "esphome/components/esphome/ota/ota_esphome.h"
#include "esphome/components/safe_mode/safe_mode.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
@ -10,10 +10,10 @@ namespace safe_mode {
class SafeModeSwitch : public switch_::Switch, public Component {
public:
void dump_config() override;
void set_ota(esphome::ESPHomeOTAComponent *ota);
void set_safe_mode(SafeModeComponent *safe_mode_component);
protected:
esphome::ESPHomeOTAComponent *ota_;
SafeModeComponent *safe_mode_component_;
void write_state(bool state) override;
};

View File

@ -11,6 +11,7 @@ from esphome.const import (
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
CONF_OPEN_DRAIN,
)
CONF_KEYPAD = "keypad"
@ -79,6 +80,8 @@ def validate_mode(value):
raise cv.Invalid("Pulldown only available with input")
if value[CONF_PULLUP] and value[CONF_PULLDOWN]:
raise cv.Invalid("Can only have one of pullup or pulldown")
if value[CONF_OPEN_DRAIN] and not value[CONF_OUTPUT]:
raise cv.Invalid("Open drain available only with output")
return value
@ -94,6 +97,7 @@ SX1509_PIN_SCHEMA = cv.All(
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
},
validate_mode,
),

View File

@ -86,33 +86,63 @@ void SX1509Component::digital_write(uint8_t pin, bool bit_value) {
}
void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) {
ESP_LOGI(TAG, "Configuring pin %u with flags %x", pin, flags);
uint16_t temp_word = 0;
this->read_byte_16(REG_DIR_B, &this->ddr_mask_);
if (flags == gpio::FLAG_OUTPUT) {
if (flags & gpio::FLAG_OUTPUT) {
// Always disable input buffer
this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word);
temp_word |= (1 << pin);
this->write_byte_16(REG_INPUT_DISABLE_B, temp_word);
if (flags & gpio::FLAG_OPEN_DRAIN) {
// Pullup must be disabled for open drain mode
this->read_byte_16(REG_PULL_UP_B, &temp_word);
temp_word &= ~(1 << pin);
this->write_byte_16(REG_PULL_UP_B, temp_word);
this->read_byte_16(REG_OPEN_DRAIN_B, &temp_word);
temp_word |= (1 << pin);
this->write_byte_16(REG_OPEN_DRAIN_B, temp_word);
ESP_LOGD(TAG, "Open drain output mode set for %u", pin);
} else {
ESP_LOGD(TAG, "Output Mode for %u", pin);
}
// Set direction to output
this->ddr_mask_ &= ~(1 << pin);
this->write_byte_16(REG_DIR_B, this->ddr_mask_);
} else {
this->ddr_mask_ |= (1 << pin);
ESP_LOGD(TAG, "Input Mode for %u", pin);
uint16_t temp_pullup;
this->read_byte_16(REG_PULL_UP_B, &temp_pullup);
uint16_t temp_pulldown;
this->read_byte_16(REG_PULL_DOWN_B, &temp_pulldown);
// Always enable input buffer
this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word);
temp_word &= ~(1 << pin);
this->write_byte_16(REG_INPUT_DISABLE_B, temp_word);
// Pullup
this->read_byte_16(REG_PULL_UP_B, &temp_word);
if (flags & gpio::FLAG_PULLUP) {
temp_pullup |= (1 << pin);
temp_word |= (1 << pin);
} else {
temp_pullup &= ~(1 << pin);
temp_word &= ~(1 << pin);
}
this->write_byte_16(REG_PULL_UP_B, temp_word);
// Pulldown
this->read_byte_16(REG_PULL_DOWN_B, &temp_word);
if (flags & gpio::FLAG_PULLDOWN) {
temp_pulldown |= (1 << pin);
temp_word |= (1 << pin);
} else {
temp_pulldown &= ~(1 << pin);
temp_word &= ~(1 << pin);
}
this->write_byte_16(REG_PULL_DOWN_B, temp_word);
this->write_byte_16(REG_PULL_UP_B, temp_pullup);
this->write_byte_16(REG_PULL_DOWN_B, temp_pulldown);
// Set direction to input
this->ddr_mask_ |= (1 << pin);
this->write_byte_16(REG_DIR_B, this->ddr_mask_);
}
this->write_byte_16(REG_DIR_B, this->ddr_mask_);
}
void SX1509Component::setup_led_driver(uint8_t pin) {

View File

@ -71,6 +71,12 @@ void VoiceAssistant::setup() {
ESP_LOGCONFIG(TAG, "Setting up Voice Assistant...");
global_voice_assistant = this;
}
bool VoiceAssistant::allocate_buffers_() {
if (this->send_buffer_ != nullptr) {
return true; // Already allocated
}
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
@ -78,8 +84,7 @@ void VoiceAssistant::setup() {
this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE);
if (this->speaker_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate speaker buffer");
this->mark_failed();
return;
return false;
}
}
#endif
@ -88,8 +93,7 @@ void VoiceAssistant::setup() {
this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE);
if (this->input_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate input buffer");
this->mark_failed();
return;
return false;
}
#ifdef USE_ESP_ADF
@ -99,17 +103,71 @@ void VoiceAssistant::setup() {
this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t));
if (this->ring_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate ring buffer");
this->mark_failed();
return;
return false;
}
ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE);
if (send_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate send buffer");
this->mark_failed();
return;
return false;
}
return true;
}
void VoiceAssistant::clear_buffers_() {
if (this->send_buffer_ != nullptr) {
memset(this->send_buffer_, 0, SEND_BUFFER_SIZE);
}
if (this->input_buffer_ != nullptr) {
memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t));
}
if (this->ring_buffer_ != nullptr) {
this->ring_buffer_->reset();
}
#ifdef USE_SPEAKER
if (this->speaker_buffer_ != nullptr) {
memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE);
this->speaker_buffer_size_ = 0;
this->speaker_buffer_index_ = 0;
this->speaker_bytes_received_ = 0;
}
#endif
}
void VoiceAssistant::deallocate_buffers_() {
ExternalRAMAllocator<uint8_t> send_deallocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
send_deallocator.deallocate(this->send_buffer_, SEND_BUFFER_SIZE);
this->send_buffer_ = nullptr;
if (this->ring_buffer_ != nullptr) {
this->ring_buffer_.reset();
this->ring_buffer_ = nullptr;
}
#ifdef USE_ESP_ADF
if (this->vad_instance_ != nullptr) {
vad_destroy(this->vad_instance_);
this->vad_instance_ = nullptr;
}
#endif
ExternalRAMAllocator<int16_t> input_deallocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
input_deallocator.deallocate(this->input_buffer_, INPUT_BUFFER_SIZE);
this->input_buffer_ = nullptr;
#ifdef USE_SPEAKER
if (this->speaker_buffer_ != nullptr) {
ExternalRAMAllocator<uint8_t> speaker_deallocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE);
this->speaker_buffer_ = nullptr;
}
#endif
}
int VoiceAssistant::read_microphone_() {
@ -138,14 +196,13 @@ void VoiceAssistant::loop() {
}
this->continuous_ = false;
this->signal_stop_();
this->clear_buffers_();
return;
}
switch (this->state_) {
case State::IDLE: {
if (this->continuous_ && this->desired_state_ == State::IDLE) {
this->idle_trigger_->trigger();
this->ring_buffer_->reset();
#ifdef USE_ESP_ADF
if (this->use_wake_word_) {
this->set_state_(State::START_MICROPHONE, State::WAIT_FOR_VAD);
@ -161,8 +218,15 @@ void VoiceAssistant::loop() {
}
case State::START_MICROPHONE: {
ESP_LOGD(TAG, "Starting Microphone");
memset(this->send_buffer_, 0, SEND_BUFFER_SIZE);
memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t));
if (!this->allocate_buffers_()) {
this->status_set_error("Failed to allocate buffers");
return;
}
if (this->status_has_error()) {
this->status_clear_error();
}
this->clear_buffers_();
this->mic_->start();
this->high_freq_.start();
this->set_state_(State::STARTING_MICROPHONE);
@ -343,10 +407,9 @@ void VoiceAssistant::loop() {
this->speaker_->stop();
this->cancel_timeout("speaker-timeout");
this->cancel_timeout("playing");
this->speaker_buffer_size_ = 0;
this->speaker_buffer_index_ = 0;
this->speaker_bytes_received_ = 0;
memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE);
this->clear_buffers_();
this->wait_for_stream_end_ = false;
this->stream_ended_ = false;
@ -507,7 +570,6 @@ void VoiceAssistant::request_start(bool continuous, bool silence_detection) {
if (this->state_ == State::IDLE) {
this->continuous_ = continuous;
this->silence_detection_ = silence_detection;
this->ring_buffer_->reset();
#ifdef USE_ESP_ADF
if (this->use_wake_word_) {
this->set_state_(State::START_MICROPHONE, State::WAIT_FOR_VAD);

View File

@ -151,6 +151,10 @@ class VoiceAssistant : public Component {
void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; }
protected:
bool allocate_buffers_();
void clear_buffers_();
void deallocate_buffers_();
int read_microphone_();
void set_state_(State state);
void set_state_(State state, State desired_state);

File diff suppressed because it is too large Load Diff

View File

@ -233,7 +233,7 @@ template<typename... Ts> class Automation {
public:
explicit Automation(Trigger<Ts...> *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); }
Action<Ts...> *add_action(Action<Ts...> *action) { this->actions_.add_action(action); }
void add_action(Action<Ts...> *action) { this->actions_.add_action(action); }
void add_actions(const std::vector<Action<Ts...> *> &actions) { this->actions_.add_actions(actions); }
void stop() { this->actions_.stop(); }

View File

@ -3,12 +3,9 @@ import logging
from esphome.const import (
CONF_DISABLED_BY_DEFAULT,
CONF_ENTITY_CATEGORY,
CONF_ESPHOME,
CONF_ICON,
CONF_INTERNAL,
CONF_NAME,
CONF_OTA,
CONF_PLATFORM,
CONF_SAFE_MODE,
CONF_SETUP_PRIORITY,
CONF_TYPE_ID,
@ -141,22 +138,12 @@ async def build_registry_list(registry, config):
async def past_safe_mode():
ota_conf = {}
for ota_item in CORE.config.get(CONF_OTA, []):
if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
ota_conf = ota_item
break
if not ota_conf:
return
safe_mode_enabled = ota_conf[CONF_SAFE_MODE]
if not safe_mode_enabled:
if CONF_SAFE_MODE not in CORE.config:
return
def _safe_mode_generator():
while True:
if CORE.data.get(CONF_OTA, {}).get(KEY_PAST_SAFE_MODE, False):
if CORE.data.get(CONF_SAFE_MODE, {}).get(KEY_PAST_SAFE_MODE, False):
return
yield

View File

@ -39,7 +39,7 @@ lib_deps =
bblanchon/ArduinoJson@6.18.5 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
pavlodn/HaierProtocol@0.9.25 ; haier
pavlodn/HaierProtocol@0.9.28 ; haier
; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
build_flags =

View File

@ -93,3 +93,21 @@ binary_sensor:
name: Haier Indoor Fan Status
outdoor_fan_status:
name: Haier Outdoor Fan Status
button:
- platform: haier
haier_id: haier_ac
self_cleaning:
name: Haier start self cleaning
steri_cleaning:
name: Haier start 56°C steri-cleaning
text_sensor:
- platform: haier
haier_id: haier_ac
appliance_name:
name: Haier appliance name
cleaning_status:
name: Haier cleaning status
protocol_version:
name: Haier protocol version

View File

@ -93,3 +93,21 @@ binary_sensor:
name: Haier Indoor Fan Status
outdoor_fan_status:
name: Haier Outdoor Fan Status
button:
- platform: haier
haier_id: haier_ac
self_cleaning:
name: Haier start self cleaning
steri_cleaning:
name: Haier start 56°C steri-cleaning
text_sensor:
- platform: haier
haier_id: haier_ac
appliance_name:
name: Haier appliance name
cleaning_status:
name: Haier cleaning status
protocol_version:
name: Haier protocol version

View File

@ -93,3 +93,21 @@ binary_sensor:
name: Haier Indoor Fan Status
outdoor_fan_status:
name: Haier Outdoor Fan Status
button:
- platform: haier
haier_id: haier_ac
self_cleaning:
name: Haier start self cleaning
steri_cleaning:
name: Haier start 56°C steri-cleaning
text_sensor:
- platform: haier
haier_id: haier_ac
appliance_name:
name: Haier appliance name
cleaning_status:
name: Haier cleaning status
protocol_version:
name: Haier protocol version

View File

@ -93,3 +93,21 @@ binary_sensor:
name: Haier Indoor Fan Status
outdoor_fan_status:
name: Haier Outdoor Fan Status
button:
- platform: haier
haier_id: haier_ac
self_cleaning:
name: Haier start self cleaning
steri_cleaning:
name: Haier start 56°C steri-cleaning
text_sensor:
- platform: haier
haier_id: haier_ac
appliance_name:
name: Haier appliance name
cleaning_status:
name: Haier cleaning status
protocol_version:
name: Haier protocol version

View File

@ -93,3 +93,21 @@ binary_sensor:
name: Haier Indoor Fan Status
outdoor_fan_status:
name: Haier Outdoor Fan Status
button:
- platform: haier
haier_id: haier_ac
self_cleaning:
name: Haier start self cleaning
steri_cleaning:
name: Haier start 56°C steri-cleaning
text_sensor:
- platform: haier
haier_id: haier_ac
appliance_name:
name: Haier appliance name
cleaning_status:
name: Haier cleaning status
protocol_version:
name: Haier protocol version

View File

@ -93,3 +93,21 @@ binary_sensor:
name: Haier Indoor Fan Status
outdoor_fan_status:
name: Haier Outdoor Fan Status
button:
- platform: haier
haier_id: haier_ac
self_cleaning:
name: Haier start self cleaning
steri_cleaning:
name: Haier start 56°C steri-cleaning
text_sensor:
- platform: haier
haier_id: haier_ac
appliance_name:
name: Haier appliance name
cleaning_status:
name: Haier cleaning status
protocol_version:
name: Haier protocol version

View File

@ -1,14 +1,30 @@
uart:
- id: uart_modbus
- id: uart_modbus_client
tx_pin: 17
rx_pin: 16
baud_rate: 9600
- id: uart_modbus_server
tx_pin: 1
rx_pin: 3
baud_rate: 9600
modbus:
id: mod_bus1
flow_control_pin: 15
- id: mod_bus1
uart_id: uart_modbus_client
flow_control_pin: 15
- id: mod_bus2
uart_id: uart_modbus_server
role: server
modbus_controller:
- id: modbus_controller1
address: 0x2
modbus_id: mod_bus1
- id: modbus_controller2
address: 0x2
modbus_id: mod_bus2
server_registers:
- address: 0x0000
value_type: S_DWORD_R
read_lambda: |-
return 42.3;

View File

@ -0,0 +1,41 @@
i2c:
- id: i2c_mpr121
scl: ${i2c_scl}
sda: ${i2c_sda}
mpr121:
id: mpr121_first
address: 0x5A
binary_sensor:
- platform: mpr121
id: touchkey0
name: touchkey0
channel: 0
- platform: mpr121
id: bin1
name: touchkey1
channel: 1
- platform: mpr121
id: bin2
name: touchkey2
channel: 2
- platform: mpr121
id: bin3
name: touchkey3
channel: 6
output:
- platform: gpio
id: gpio1
pin:
mpr121: mpr121_first
number: 7
mode: OUTPUT
- platform: gpio
id: gpio2
pin:
mpr121: mpr121_first
number: 11
mode: OUTPUT
inverted: true

View File

@ -1,26 +1,5 @@
i2c:
- id: i2c_mpr121
scl: 5
sda: 4
substitutions:
i2c_scl: GPIO5
i2c_sda: GPIO4
mpr121:
id: mpr121_first
address: 0x5A
binary_sensor:
- platform: mpr121
id: touchkey0
name: touchkey0
channel: 0
- platform: mpr121
id: bin1
name: touchkey1
channel: 1
- platform: mpr121
id: bin2
name: touchkey2
channel: 2
- platform: mpr121
id: bin3
name: touchkey3
channel: 3
<<: !include common.yaml

View File

@ -1,26 +1,5 @@
i2c:
- id: i2c_mpr121
scl: 5
sda: 4
substitutions:
i2c_scl: GPIO5
i2c_sda: GPIO4
mpr121:
id: mpr121_first
address: 0x5A
binary_sensor:
- platform: mpr121
id: touchkey0
name: touchkey0
channel: 0
- platform: mpr121
id: bin1
name: touchkey1
channel: 1
- platform: mpr121
id: bin2
name: touchkey2
channel: 2
- platform: mpr121
id: bin3
name: touchkey3
channel: 3
<<: !include common.yaml

View File

@ -1,26 +1,5 @@
i2c:
- id: i2c_mpr121
scl: 16
sda: 17
substitutions:
i2c_scl: GPIO16
i2c_sda: GPIO17
mpr121:
id: mpr121_first
address: 0x5A
binary_sensor:
- platform: mpr121
id: touchkey0
name: touchkey0
channel: 0
- platform: mpr121
id: bin1
name: touchkey1
channel: 1
- platform: mpr121
id: bin2
name: touchkey2
channel: 2
- platform: mpr121
id: bin3
name: touchkey3
channel: 3
<<: !include common.yaml

View File

@ -1,26 +1,5 @@
i2c:
- id: i2c_mpr121
scl: 16
sda: 17
substitutions:
i2c_scl: GPIO16
i2c_sda: GPIO17
mpr121:
id: mpr121_first
address: 0x5A
binary_sensor:
- platform: mpr121
id: touchkey0
name: touchkey0
channel: 0
- platform: mpr121
id: bin1
name: touchkey1
channel: 1
- platform: mpr121
id: bin2
name: touchkey2
channel: 2
- platform: mpr121
id: bin3
name: touchkey3
channel: 3
<<: !include common.yaml

View File

@ -1,26 +1,5 @@
i2c:
- id: i2c_mpr121
scl: 5
sda: 4
substitutions:
i2c_scl: GPIO5
i2c_sda: GPIO4
mpr121:
id: mpr121_first
address: 0x5A
binary_sensor:
- platform: mpr121
id: touchkey0
name: touchkey0
channel: 0
- platform: mpr121
id: bin1
name: touchkey1
channel: 1
- platform: mpr121
id: bin2
name: touchkey2
channel: 2
- platform: mpr121
id: bin3
name: touchkey3
channel: 3
<<: !include common.yaml

View File

@ -1,26 +1,5 @@
i2c:
- id: i2c_mpr121
scl: 5
sda: 4
substitutions:
i2c_scl: GPIO5
i2c_sda: GPIO4
mpr121:
id: mpr121_first
address: 0x5A
binary_sensor:
- platform: mpr121
id: touchkey0
name: touchkey0
channel: 0
- platform: mpr121
id: bin1
name: touchkey1
channel: 1
- platform: mpr121
id: bin2
name: touchkey2
channel: 2
- platform: mpr121
id: bin3
name: touchkey3
channel: 3
<<: !include common.yaml

View File

@ -4,11 +4,8 @@ wifi:
ota:
- platform: esphome
safe_mode: true
password: "superlongpasswordthatnoonewillknow"
port: 3286
reboot_timeout: 2min
num_attempts: 5
on_begin:
then:
- logger.log: "OTA start"

View File

@ -3,6 +3,7 @@ remote_receiver:
pin: ${pin}
rmt_channel: ${rmt_channel}
dump: all
tolerance: 25%
on_abbwelcome:
then:
- logger.log:

View File

@ -2,9 +2,11 @@ wifi:
ssid: MySSID
password: password1
ota:
- platform: esphome
safe_mode: true
safe_mode:
num_attempts: 3
reboot_timeout: 2min
on_safe_mode:
- logger.log: Time for safe mode
button:
- platform: safe_mode

View File

@ -13,3 +13,21 @@ binary_sensor:
pin:
sx1509: sx1509_hub
number: 3
switch:
- platform: gpio
name: GPIO SX1509 Test Out Open Drain
pin:
sx1509: sx1509_hub
number: 0
mode:
output: true
open_drain: true
- platform: gpio
name: GPIO SX1509 Test Out Standard
pin:
sx1509: sx1509_hub
number: 1
mode:
output: true

View File

@ -13,3 +13,21 @@ binary_sensor:
pin:
sx1509: sx1509_hub
number: 3
switch:
- platform: gpio
name: GPIO SX1509 Test Out Open Drain
pin:
sx1509: sx1509_hub
number: 0
mode:
output: true
open_drain: true
- platform: gpio
name: GPIO SX1509 Test Out Standard
pin:
sx1509: sx1509_hub
number: 1
mode:
output: true

View File

@ -13,3 +13,21 @@ binary_sensor:
pin:
sx1509: sx1509_hub
number: 3
switch:
- platform: gpio
name: GPIO SX1509 Test Out Open Drain
pin:
sx1509: sx1509_hub
number: 0
mode:
output: true
open_drain: true
- platform: gpio
name: GPIO SX1509 Test Out Standard
pin:
sx1509: sx1509_hub
number: 1
mode:
output: true

View File

@ -13,3 +13,21 @@ binary_sensor:
pin:
sx1509: sx1509_hub
number: 3
switch:
- platform: gpio
name: GPIO SX1509 Test Out Open Drain
pin:
sx1509: sx1509_hub
number: 0
mode:
output: true
open_drain: true
- platform: gpio
name: GPIO SX1509 Test Out Standard
pin:
sx1509: sx1509_hub
number: 1
mode:
output: true

View File

@ -13,3 +13,21 @@ binary_sensor:
pin:
sx1509: sx1509_hub
number: 3
switch:
- platform: gpio
name: GPIO SX1509 Test Out Open Drain
pin:
sx1509: sx1509_hub
number: 0
mode:
output: true
open_drain: true
- platform: gpio
name: GPIO SX1509 Test Out Standard
pin:
sx1509: sx1509_hub
number: 1
mode:
output: true

View File

@ -13,3 +13,21 @@ binary_sensor:
pin:
sx1509: sx1509_hub
number: 3
switch:
- platform: gpio
name: GPIO SX1509 Test Out Open Drain
pin:
sx1509: sx1509_hub
number: 0
mode:
output: true
open_drain: true
- platform: gpio
name: GPIO SX1509 Test Out Standard
pin:
sx1509: sx1509_hub
number: 1
mode:
output: true

View File

@ -264,13 +264,14 @@ uart:
parity: EVEN
baud_rate: 9600
safe_mode:
num_attempts: 3
reboot_timeout: 2min
ota:
- platform: esphome
safe_mode: true
password: "superlongpasswordthatnoonewillknow"
port: 3286
reboot_timeout: 2min
num_attempts: 5
on_state_change:
then:
lambda: >-

View File

@ -79,11 +79,11 @@ uart:
sequence:
- lambda: UARTDebug::log_hex(direction, bytes, ':');
safe_mode:
ota:
- platform: esphome
safe_mode: true
port: 3286
num_attempts: 15
logger:
level: DEBUG

View File

@ -344,10 +344,6 @@ apds9960:
address: 0x20
update_interval: 60s
mpr121:
id: mpr121_first
address: 0x5A
binary_sensor:
- platform: apds9960
direction: up
@ -371,25 +367,6 @@ binary_sensor:
direction: right
name: APDS9960 Right
- platform: mpr121
id: touchkey0
channel: 0
name: touchkey0
- platform: mpr121
channel: 1
name: touchkey1
id: bin1
- platform: mpr121
channel: 2
name: touchkey2
id: bin2
- platform: mpr121
channel: 3
name: touchkey3
id: bin3
on_press:
then:
- switch.toggle: mpr121_toggle
- platform: ttp229_lsf
channel: 1
name: TTP229 LSF Test
@ -443,10 +420,6 @@ grove_tb6612fng:
address: 0x14
switch:
- platform: template
name: mpr121_toggle
id: mpr121_toggle
optimistic: true
- platform: gpio
id: gpio_switch1
pin:

View File

@ -327,11 +327,13 @@ modbus:
vbus:
uart_id: uart_4
safe_mode:
num_attempts: 5
reboot_timeout: 10min
ota:
- platform: esphome
safe_mode: true
port: 3286
reboot_timeout: 15min
logger:
hardware_uart: UART1

View File

@ -102,9 +102,10 @@ uart:
baud_rate: 1200
parity: EVEN
safe_mode:
ota:
- platform: esphome
safe_mode: true
port: 3286
logger: