mirror of
https://github.com/esphome/esphome.git
synced 2025-01-08 13:51:43 +01:00
Merge branch 'dev' into msa3xx
This commit is contained in:
commit
2dad225f35
37 changed files with 680 additions and 234 deletions
|
@ -306,7 +306,7 @@ esphome/components/rp2040_pwm/* @jesserockz
|
||||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||||
esphome/components/rtl87xx/* @kuba2k2
|
esphome/components/rtl87xx/* @kuba2k2
|
||||||
esphome/components/rtttl/* @glmnet
|
esphome/components/rtttl/* @glmnet
|
||||||
esphome/components/safe_mode/* @jsuanet @paulmonigatti
|
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||||
esphome/components/scd4x/* @martgras @sjtrny
|
esphome/components/scd4x/* @martgras @sjtrny
|
||||||
esphome/components/script/* @esphome/core
|
esphome/components/script/* @esphome/core
|
||||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
from esphome.cpp_generator import RawExpression
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
|
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_NUM_ATTEMPTS,
|
CONF_NUM_ATTEMPTS,
|
||||||
CONF_OTA,
|
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_REBOOT_TIMEOUT,
|
CONF_REBOOT_TIMEOUT,
|
||||||
CONF_SAFE_MODE,
|
CONF_SAFE_MODE,
|
||||||
CONF_VERSION,
|
CONF_VERSION,
|
||||||
KEY_PAST_SAFE_MODE,
|
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import coroutine_with_priority
|
||||||
|
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
@ -28,7 +25,6 @@ CONFIG_SCHEMA = (
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
|
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.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
|
||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
@ -39,10 +35,15 @@ CONFIG_SCHEMA = (
|
||||||
rtl87xx=8892,
|
rtl87xx=8892,
|
||||||
): cv.port,
|
): cv.port,
|
||||||
cv.Optional(CONF_PASSWORD): cv.string,
|
cv.Optional(CONF_PASSWORD): cv.string,
|
||||||
cv.Optional(
|
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
|
||||||
CONF_REBOOT_TIMEOUT, default="5min"
|
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.positive_time_period_milliseconds,
|
),
|
||||||
cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
|
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)
|
.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):
|
async def to_code(config):
|
||||||
CORE.data[CONF_OTA] = {}
|
|
||||||
|
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await ota_to_code(var, config)
|
await ota_to_code(var, config)
|
||||||
cg.add(var.set_port(config[CONF_PORT]))
|
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])
|
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
|
||||||
|
|
||||||
await cg.register_component(var, config)
|
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
|
|
||||||
|
|
|
@ -78,23 +78,9 @@ void ESPHomeOTAComponent::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, " Password configured");
|
ESP_LOGCONFIG(TAG, " Password configured");
|
||||||
}
|
}
|
||||||
#endif
|
#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() {
|
void ESPHomeOTAComponent::loop() { this->handle_(); }
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
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; }
|
float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||||
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
|
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
|
||||||
void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = 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
|
} // namespace esphome
|
||||||
|
|
|
@ -15,17 +15,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||||
void set_auth_password(const std::string &password) { password_ = password; }
|
void set_auth_password(const std::string &password) { password_ = password; }
|
||||||
#endif // USE_OTA_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);
|
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 setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
|
@ -33,14 +25,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||||
|
|
||||||
uint16_t get_port() const;
|
uint16_t get_port() const;
|
||||||
|
|
||||||
void clean_rtc();
|
|
||||||
|
|
||||||
void on_safe_shutdown() override;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void write_rtc_(uint32_t val);
|
|
||||||
uint32_t read_rtc_();
|
|
||||||
|
|
||||||
void handle_();
|
void handle_();
|
||||||
bool readall_(uint8_t *buf, size_t len);
|
bool readall_(uint8_t *buf, size_t len);
|
||||||
bool writeall_(const 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> server_;
|
||||||
std::unique_ptr<socket::Socket> client_;
|
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
|
} // namespace esphome
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
import esphome.final_validate as fv
|
||||||
from esphome.cpp_helpers import gpio_pin_expression
|
from esphome.cpp_helpers import gpio_pin_expression
|
||||||
from esphome.components import uart
|
from esphome.components import uart
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
@ -17,13 +21,21 @@ Modbus = modbus_ns.class_("Modbus", cg.Component, uart.UARTDevice)
|
||||||
ModbusDevice = modbus_ns.class_("ModbusDevice")
|
ModbusDevice = modbus_ns.class_("ModbusDevice")
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
CONF_ROLE = "role"
|
||||||
CONF_MODBUS_ID = "modbus_id"
|
CONF_MODBUS_ID = "modbus_id"
|
||||||
CONF_SEND_WAIT_TIME = "send_wait_time"
|
CONF_SEND_WAIT_TIME = "send_wait_time"
|
||||||
|
|
||||||
|
ModbusRole = modbus_ns.enum("ModbusRole")
|
||||||
|
MODBUS_ROLES = {
|
||||||
|
"client": ModbusRole.CLIENT,
|
||||||
|
"server": ModbusRole.SERVER,
|
||||||
|
}
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
CONFIG_SCHEMA = (
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(Modbus),
|
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_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema,
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_SEND_WAIT_TIME, default="250ms"
|
CONF_SEND_WAIT_TIME, default="250ms"
|
||||||
|
@ -43,6 +55,7 @@ async def to_code(config):
|
||||||
|
|
||||||
await uart.register_uart_device(var, config)
|
await uart.register_uart_device(var, config)
|
||||||
|
|
||||||
|
cg.add(var.set_role(config[CONF_ROLE]))
|
||||||
if CONF_FLOW_CONTROL_PIN in config:
|
if CONF_FLOW_CONTROL_PIN in config:
|
||||||
pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN])
|
pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN])
|
||||||
cg.add(var.set_flow_control_pin(pin))
|
cg.add(var.set_flow_control_pin(pin))
|
||||||
|
@ -62,6 +75,28 @@ def modbus_device_schema(default_address):
|
||||||
return cv.Schema(schema)
|
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):
|
async def register_modbus_device(var, config):
|
||||||
parent = await cg.get_variable(config[CONF_MODBUS_ID])
|
parent = await cg.get_variable(config[CONF_MODBUS_ID])
|
||||||
cg.add(var.set_parent(parent))
|
cg.add(var.set_parent(parent))
|
||||||
|
|
|
@ -77,7 +77,13 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||||
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
|
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
|
||||||
|
|
||||||
} else {
|
} 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) {
|
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
|
||||||
data_offset = 2;
|
data_offset = 2;
|
||||||
data_len = 4;
|
data_len = 4;
|
||||||
|
@ -123,6 +129,9 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||||
// Ignore modbus exception not related to a pending command
|
// Ignore modbus exception not related to a pending command
|
||||||
ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response");
|
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 {
|
} else {
|
||||||
device->on_modbus_data(data);
|
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;
|
std::vector<uint8_t> data;
|
||||||
data.push_back(address);
|
data.push_back(address);
|
||||||
data.push_back(function_code);
|
data.push_back(function_code);
|
||||||
data.push_back(start_address >> 8);
|
if (this->role == ModbusRole::CLIENT) {
|
||||||
data.push_back(start_address >> 0);
|
data.push_back(start_address >> 8);
|
||||||
if (function_code != 0x5 && function_code != 0x6) {
|
data.push_back(start_address >> 0);
|
||||||
data.push_back(number_of_entities >> 8);
|
if (function_code != 0x5 && function_code != 0x6) {
|
||||||
data.push_back(number_of_entities >> 0);
|
data.push_back(number_of_entities >> 8);
|
||||||
|
data.push_back(number_of_entities >> 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload != nullptr) {
|
if (payload != nullptr) {
|
||||||
if (function_code == 0xF || function_code == 0x10) { // Write multiple
|
if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) { // Write multiple
|
||||||
data.push_back(payload_len); // Byte count is required for write
|
data.push_back(payload_len); // Byte count is required for write
|
||||||
} else {
|
} else {
|
||||||
payload_len = 2; // Write single register or coil
|
payload_len = 2; // Write single register or coil
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,11 @@
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace modbus {
|
namespace modbus {
|
||||||
|
|
||||||
|
enum ModbusRole {
|
||||||
|
CLIENT,
|
||||||
|
SERVER,
|
||||||
|
};
|
||||||
|
|
||||||
class ModbusDevice;
|
class ModbusDevice;
|
||||||
|
|
||||||
class Modbus : public uart::UARTDevice, public Component {
|
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,
|
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);
|
uint8_t payload_len = 0, const uint8_t *payload = nullptr);
|
||||||
void send_raw(const std::vector<uint8_t> &payload);
|
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; }
|
void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
|
||||||
uint8_t waiting_for_response{0};
|
uint8_t waiting_for_response{0};
|
||||||
void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; }
|
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; }
|
void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; }
|
||||||
|
|
||||||
|
ModbusRole role;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
GPIOPin *flow_control_pin_{nullptr};
|
GPIOPin *flow_control_pin_{nullptr};
|
||||||
|
|
||||||
|
@ -50,6 +58,7 @@ class ModbusDevice {
|
||||||
void set_address(uint8_t address) { address_ = address; }
|
void set_address(uint8_t address) { address_ = address; }
|
||||||
virtual void on_modbus_data(const std::vector<uint8_t> &data) = 0;
|
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_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,
|
void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
|
||||||
const uint8_t *payload = nullptr) {
|
const uint8_t *payload = nullptr) {
|
||||||
this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload);
|
this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload);
|
||||||
|
|
|
@ -23,6 +23,8 @@ CODEOWNERS = ["@martgras"]
|
||||||
|
|
||||||
AUTO_LOAD = ["modbus"]
|
AUTO_LOAD = ["modbus"]
|
||||||
|
|
||||||
|
CONF_READ_LAMBDA = "read_lambda"
|
||||||
|
CONF_SERVER_REGISTERS = "server_registers"
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
|
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
|
||||||
|
@ -31,6 +33,7 @@ ModbusController = modbus_controller_ns.class_(
|
||||||
)
|
)
|
||||||
|
|
||||||
SensorItem = modbus_controller_ns.struct("SensorItem")
|
SensorItem = modbus_controller_ns.struct("SensorItem")
|
||||||
|
ServerRegister = modbus_controller_ns.struct("ServerRegister")
|
||||||
|
|
||||||
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
|
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
|
||||||
ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode")
|
ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode")
|
||||||
|
@ -94,10 +97,18 @@ TYPE_REGISTER_MAP = {
|
||||||
"FP32_R": 2,
|
"FP32_R": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
MULTI_CONF = True
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
|
@ -106,6 +117,9 @@ CONFIG_SCHEMA = cv.All(
|
||||||
CONF_COMMAND_THROTTLE, default="0ms"
|
CONF_COMMAND_THROTTLE, default="0ms"
|
||||||
): cv.positive_time_period_milliseconds,
|
): cv.positive_time_period_milliseconds,
|
||||||
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
|
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"))
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
@ -154,6 +168,17 @@ def validate_modbus_register(config):
|
||||||
return 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):
|
def modbus_calc_properties(config):
|
||||||
byte_offset = 0
|
byte_offset = 0
|
||||||
reg_count = 0
|
reg_count = 0
|
||||||
|
@ -183,7 +208,7 @@ def modbus_calc_properties(config):
|
||||||
|
|
||||||
|
|
||||||
async def add_modbus_base_properties(
|
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:
|
if CONF_CUSTOM_COMMAND in config:
|
||||||
cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND]))
|
cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND]))
|
||||||
|
@ -196,13 +221,13 @@ async def add_modbus_base_properties(
|
||||||
config[CONF_LAMBDA],
|
config[CONF_LAMBDA],
|
||||||
[
|
[
|
||||||
(sensor_type.operator("ptr"), "item"),
|
(sensor_type.operator("ptr"), "item"),
|
||||||
(lamdba_param_type, "x"),
|
(lambda_param_type, "x"),
|
||||||
(
|
(
|
||||||
cg.std_vector.template(cg.uint8).operator("const").operator("ref"),
|
cg.std_vector.template(cg.uint8).operator("const").operator("ref"),
|
||||||
"data",
|
"data",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
return_type=cg.optional.template(lamdba_return_type),
|
return_type=cg.optional.template(lambda_return_type),
|
||||||
)
|
)
|
||||||
cg.add(var.set_template(template_))
|
cg.add(var.set_template(template_))
|
||||||
|
|
||||||
|
@ -211,6 +236,23 @@ async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
|
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
|
||||||
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
|
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)
|
await register_modbus_device(var, config)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,7 @@ namespace modbus_controller {
|
||||||
|
|
||||||
static const char *const TAG = "modbus_controller";
|
static const char *const TAG = "modbus_controller";
|
||||||
|
|
||||||
void ModbusController::setup() {
|
void ModbusController::setup() { this->create_register_ranges_(); }
|
||||||
// Modbus::setup();
|
|
||||||
this->create_register_ranges_();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
To work with the existing modbus class and avoid polling for responses a command queue is used.
|
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 {
|
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) {
|
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);
|
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
|
// walk through the sensors and determine the register ranges to read
|
||||||
size_t ModbusController::create_register_ranges_() {
|
size_t ModbusController::create_register_ranges_() {
|
||||||
register_ranges_.clear();
|
register_ranges_.clear();
|
||||||
if (sensorset_.empty()) {
|
if (this->parent_->role == modbus::ModbusRole::CLIENT && sensorset_.empty()) {
|
||||||
ESP_LOGW(TAG, "No sensors registered");
|
ESP_LOGW(TAG, "No sensors registered");
|
||||||
return 0;
|
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),
|
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);
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
@ -251,6 +252,21 @@ class SensorItem {
|
||||||
bool force_new_range{false};
|
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
|
// ModbusController::create_register_ranges_ tries to optimize register range
|
||||||
// for this the sensors must be ordered by register_type, start_address and bitmask
|
// for this the sensors must be ordered by register_type, start_address and bitmask
|
||||||
class SensorItemsComparator {
|
class SensorItemsComparator {
|
||||||
|
@ -418,10 +434,14 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
||||||
void queue_command(const ModbusCommandItem &command);
|
void queue_command(const ModbusCommandItem &command);
|
||||||
/// Registers a sensor with the controller. Called by esphomes code generator
|
/// Registers a sensor with the controller. Called by esphomes code generator
|
||||||
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
|
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
|
/// called when a modbus response was parsed without errors
|
||||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||||
/// called when a modbus error response was received
|
/// called when a modbus error response was received
|
||||||
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
|
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
|
/// 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);
|
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
|
/// 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_();
|
void dump_sensors_();
|
||||||
/// Collection of all sensors for this component
|
/// Collection of all sensors for this component
|
||||||
SensorSet sensorset_;
|
SensorSet sensorset_;
|
||||||
|
/// Collection of all server registers for this component
|
||||||
|
std::vector<ServerRegister *> server_registers_;
|
||||||
/// Continuous range of modbus registers
|
/// Continuous range of modbus registers
|
||||||
std::vector<RegisterRange> register_ranges_;
|
std::vector<RegisterRange> register_ranges_;
|
||||||
/// Hold the pending requests to be sent
|
/// Hold the pending requests to be sent
|
||||||
|
|
|
@ -6,7 +6,7 @@ from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID
|
from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
AUTO_LOAD = ["md5"]
|
AUTO_LOAD = ["md5", "safe_mode"]
|
||||||
|
|
||||||
IS_PLATFORM_COMPONENT = True
|
IS_PLATFORM_COMPONENT = True
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ BASE_OTA_SCHEMA = cv.Schema(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(51.0)
|
@coroutine_with_priority(54.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_OTA")
|
cg.add_define("USE_OTA")
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,70 @@
|
||||||
|
from esphome.cpp_generator import RawExpression
|
||||||
import esphome.codegen as cg
|
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")
|
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
|
||||||
|
|
17
esphome/components/safe_mode/automation.h
Normal file
17
esphome/components/safe_mode/automation.h
Normal 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
|
|
@ -1,16 +1,15 @@
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.components import button
|
from esphome.components import button
|
||||||
from esphome.components.esphome.ota import ESPHomeOTAComponent
|
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ESPHOME,
|
CONF_SAFE_MODE,
|
||||||
DEVICE_CLASS_RESTART,
|
DEVICE_CLASS_RESTART,
|
||||||
ENTITY_CATEGORY_CONFIG,
|
ENTITY_CATEGORY_CONFIG,
|
||||||
ICON_RESTART_ALERT,
|
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)
|
SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component)
|
||||||
|
|
||||||
|
@ -21,7 +20,7 @@ CONFIG_SCHEMA = (
|
||||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||||
icon=ICON_RESTART_ALERT,
|
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)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,5 +29,5 @@ async def to_code(config):
|
||||||
var = await button.new_button(config)
|
var = await button.new_button(config)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
ota = await cg.get_variable(config[CONF_ESPHOME])
|
safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE])
|
||||||
cg.add(var.set_ota(ota))
|
cg.add(var.set_safe_mode(safe_mode_component))
|
||||||
|
|
|
@ -8,11 +8,13 @@ namespace safe_mode {
|
||||||
|
|
||||||
static const char *const TAG = "safe_mode.button";
|
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() {
|
void SafeModeButton::press_action() {
|
||||||
ESP_LOGI(TAG, "Restarting device in safe mode...");
|
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
|
// Let MQTT settle a bit
|
||||||
delay(100); // NOLINT
|
delay(100); // NOLINT
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/components/button/button.h"
|
#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"
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
@ -10,10 +10,10 @@ namespace safe_mode {
|
||||||
class SafeModeButton : public button::Button, public Component {
|
class SafeModeButton : public button::Button, public Component {
|
||||||
public:
|
public:
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void set_ota(esphome::ESPHomeOTAComponent *ota);
|
void set_safe_mode(SafeModeComponent *safe_mode_component);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
esphome::ESPHomeOTAComponent *ota_;
|
SafeModeComponent *safe_mode_component_;
|
||||||
void press_action() override;
|
void press_action() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
127
esphome/components/safe_mode/safe_mode.cpp
Normal file
127
esphome/components/safe_mode/safe_mode.cpp
Normal 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
|
49
esphome/components/safe_mode/safe_mode.h
Normal file
49
esphome/components/safe_mode/safe_mode.h
Normal 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
|
|
@ -1,15 +1,14 @@
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.components import switch
|
from esphome.components import switch
|
||||||
from esphome.components.esphome.ota import ESPHomeOTAComponent
|
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ESPHOME,
|
CONF_SAFE_MODE,
|
||||||
ENTITY_CATEGORY_CONFIG,
|
ENTITY_CATEGORY_CONFIG,
|
||||||
ICON_RESTART_ALERT,
|
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)
|
SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component)
|
||||||
|
|
||||||
|
@ -20,7 +19,7 @@ CONFIG_SCHEMA = (
|
||||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||||
icon=ICON_RESTART_ALERT,
|
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)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,5 +28,5 @@ async def to_code(config):
|
||||||
var = await switch.new_switch(config)
|
var = await switch.new_switch(config)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
ota = await cg.get_variable(config[CONF_ESPHOME])
|
safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE])
|
||||||
cg.add(var.set_ota(ota))
|
cg.add(var.set_safe_mode(safe_mode_component))
|
||||||
|
|
|
@ -6,9 +6,11 @@
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace safe_mode {
|
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) {
|
void SafeModeSwitch::write_state(bool state) {
|
||||||
// Acknowledge
|
// Acknowledge
|
||||||
|
@ -16,13 +18,14 @@ void SafeModeSwitch::write_state(bool state) {
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
ESP_LOGI(TAG, "Restarting device in safe mode...");
|
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
|
// Let MQTT settle a bit
|
||||||
delay(100); // NOLINT
|
delay(100); // NOLINT
|
||||||
App.safe_reboot();
|
App.safe_reboot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); }
|
void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); }
|
||||||
|
|
||||||
} // namespace safe_mode
|
} // namespace safe_mode
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#pragma once
|
#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/components/switch/switch.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@ namespace safe_mode {
|
||||||
class SafeModeSwitch : public switch_::Switch, public Component {
|
class SafeModeSwitch : public switch_::Switch, public Component {
|
||||||
public:
|
public:
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void set_ota(esphome::ESPHomeOTAComponent *ota);
|
void set_safe_mode(SafeModeComponent *safe_mode_component);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
esphome::ESPHomeOTAComponent *ota_;
|
SafeModeComponent *safe_mode_component_;
|
||||||
void write_state(bool state) override;
|
void write_state(bool state) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ from esphome.const import (
|
||||||
CONF_OUTPUT,
|
CONF_OUTPUT,
|
||||||
CONF_PULLDOWN,
|
CONF_PULLDOWN,
|
||||||
CONF_PULLUP,
|
CONF_PULLUP,
|
||||||
|
CONF_OPEN_DRAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
CONF_KEYPAD = "keypad"
|
CONF_KEYPAD = "keypad"
|
||||||
|
@ -79,6 +80,8 @@ def validate_mode(value):
|
||||||
raise cv.Invalid("Pulldown only available with input")
|
raise cv.Invalid("Pulldown only available with input")
|
||||||
if value[CONF_PULLUP] and value[CONF_PULLDOWN]:
|
if value[CONF_PULLUP] and value[CONF_PULLDOWN]:
|
||||||
raise cv.Invalid("Can only have one of pullup or 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
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,6 +97,7 @@ SX1509_PIN_SCHEMA = cv.All(
|
||||||
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
|
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
|
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
|
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
|
||||||
},
|
},
|
||||||
validate_mode,
|
validate_mode,
|
||||||
),
|
),
|
||||||
|
|
|
@ -86,33 +86,63 @@ void SX1509Component::digital_write(uint8_t pin, bool bit_value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
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_);
|
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->ddr_mask_ &= ~(1 << pin);
|
||||||
|
this->write_byte_16(REG_DIR_B, this->ddr_mask_);
|
||||||
} else {
|
} else {
|
||||||
this->ddr_mask_ |= (1 << pin);
|
ESP_LOGD(TAG, "Input Mode for %u", pin);
|
||||||
|
|
||||||
uint16_t temp_pullup;
|
// Always enable input buffer
|
||||||
this->read_byte_16(REG_PULL_UP_B, &temp_pullup);
|
this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word);
|
||||||
uint16_t temp_pulldown;
|
temp_word &= ~(1 << pin);
|
||||||
this->read_byte_16(REG_PULL_DOWN_B, &temp_pulldown);
|
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) {
|
if (flags & gpio::FLAG_PULLUP) {
|
||||||
temp_pullup |= (1 << pin);
|
temp_word |= (1 << pin);
|
||||||
} else {
|
} 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) {
|
if (flags & gpio::FLAG_PULLDOWN) {
|
||||||
temp_pulldown |= (1 << pin);
|
temp_word |= (1 << pin);
|
||||||
} else {
|
} 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);
|
// Set direction to input
|
||||||
this->write_byte_16(REG_PULL_DOWN_B, temp_pulldown);
|
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) {
|
void SX1509Component::setup_led_driver(uint8_t pin) {
|
||||||
|
|
|
@ -3,12 +3,9 @@ import logging
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_DISABLED_BY_DEFAULT,
|
CONF_DISABLED_BY_DEFAULT,
|
||||||
CONF_ENTITY_CATEGORY,
|
CONF_ENTITY_CATEGORY,
|
||||||
CONF_ESPHOME,
|
|
||||||
CONF_ICON,
|
CONF_ICON,
|
||||||
CONF_INTERNAL,
|
CONF_INTERNAL,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_OTA,
|
|
||||||
CONF_PLATFORM,
|
|
||||||
CONF_SAFE_MODE,
|
CONF_SAFE_MODE,
|
||||||
CONF_SETUP_PRIORITY,
|
CONF_SETUP_PRIORITY,
|
||||||
CONF_TYPE_ID,
|
CONF_TYPE_ID,
|
||||||
|
@ -141,22 +138,12 @@ async def build_registry_list(registry, config):
|
||||||
|
|
||||||
|
|
||||||
async def past_safe_mode():
|
async def past_safe_mode():
|
||||||
ota_conf = {}
|
if CONF_SAFE_MODE not in CORE.config:
|
||||||
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:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _safe_mode_generator():
|
def _safe_mode_generator():
|
||||||
while True:
|
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
|
return
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,30 @@
|
||||||
uart:
|
uart:
|
||||||
- id: uart_modbus
|
- id: uart_modbus_client
|
||||||
tx_pin: 17
|
tx_pin: 17
|
||||||
rx_pin: 16
|
rx_pin: 16
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
|
- id: uart_modbus_server
|
||||||
|
tx_pin: 1
|
||||||
|
rx_pin: 3
|
||||||
|
baud_rate: 9600
|
||||||
|
|
||||||
modbus:
|
modbus:
|
||||||
id: mod_bus1
|
- id: mod_bus1
|
||||||
flow_control_pin: 15
|
uart_id: uart_modbus_client
|
||||||
|
flow_control_pin: 15
|
||||||
|
- id: mod_bus2
|
||||||
|
uart_id: uart_modbus_server
|
||||||
|
role: server
|
||||||
|
|
||||||
modbus_controller:
|
modbus_controller:
|
||||||
- id: modbus_controller1
|
- id: modbus_controller1
|
||||||
address: 0x2
|
address: 0x2
|
||||||
modbus_id: mod_bus1
|
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;
|
||||||
|
|
|
@ -4,11 +4,8 @@ wifi:
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
safe_mode: true
|
|
||||||
password: "superlongpasswordthatnoonewillknow"
|
password: "superlongpasswordthatnoonewillknow"
|
||||||
port: 3286
|
port: 3286
|
||||||
reboot_timeout: 2min
|
|
||||||
num_attempts: 5
|
|
||||||
on_begin:
|
on_begin:
|
||||||
then:
|
then:
|
||||||
- logger.log: "OTA start"
|
- logger.log: "OTA start"
|
||||||
|
|
|
@ -2,9 +2,11 @@ wifi:
|
||||||
ssid: MySSID
|
ssid: MySSID
|
||||||
password: password1
|
password: password1
|
||||||
|
|
||||||
ota:
|
safe_mode:
|
||||||
- platform: esphome
|
num_attempts: 3
|
||||||
safe_mode: true
|
reboot_timeout: 2min
|
||||||
|
on_safe_mode:
|
||||||
|
- logger.log: Time for safe mode
|
||||||
|
|
||||||
button:
|
button:
|
||||||
- platform: safe_mode
|
- platform: safe_mode
|
||||||
|
|
|
@ -13,3 +13,21 @@ binary_sensor:
|
||||||
pin:
|
pin:
|
||||||
sx1509: sx1509_hub
|
sx1509: sx1509_hub
|
||||||
number: 3
|
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
|
||||||
|
|
|
@ -13,3 +13,21 @@ binary_sensor:
|
||||||
pin:
|
pin:
|
||||||
sx1509: sx1509_hub
|
sx1509: sx1509_hub
|
||||||
number: 3
|
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
|
||||||
|
|
|
@ -13,3 +13,21 @@ binary_sensor:
|
||||||
pin:
|
pin:
|
||||||
sx1509: sx1509_hub
|
sx1509: sx1509_hub
|
||||||
number: 3
|
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
|
||||||
|
|
|
@ -13,3 +13,21 @@ binary_sensor:
|
||||||
pin:
|
pin:
|
||||||
sx1509: sx1509_hub
|
sx1509: sx1509_hub
|
||||||
number: 3
|
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
|
||||||
|
|
|
@ -13,3 +13,21 @@ binary_sensor:
|
||||||
pin:
|
pin:
|
||||||
sx1509: sx1509_hub
|
sx1509: sx1509_hub
|
||||||
number: 3
|
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
|
||||||
|
|
|
@ -13,3 +13,21 @@ binary_sensor:
|
||||||
pin:
|
pin:
|
||||||
sx1509: sx1509_hub
|
sx1509: sx1509_hub
|
||||||
number: 3
|
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
|
||||||
|
|
|
@ -264,13 +264,14 @@ uart:
|
||||||
parity: EVEN
|
parity: EVEN
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
|
|
||||||
|
safe_mode:
|
||||||
|
num_attempts: 3
|
||||||
|
reboot_timeout: 2min
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
safe_mode: true
|
|
||||||
password: "superlongpasswordthatnoonewillknow"
|
password: "superlongpasswordthatnoonewillknow"
|
||||||
port: 3286
|
port: 3286
|
||||||
reboot_timeout: 2min
|
|
||||||
num_attempts: 5
|
|
||||||
on_state_change:
|
on_state_change:
|
||||||
then:
|
then:
|
||||||
lambda: >-
|
lambda: >-
|
||||||
|
|
|
@ -79,11 +79,11 @@ uart:
|
||||||
sequence:
|
sequence:
|
||||||
- lambda: UARTDebug::log_hex(direction, bytes, ':');
|
- lambda: UARTDebug::log_hex(direction, bytes, ':');
|
||||||
|
|
||||||
|
safe_mode:
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
safe_mode: true
|
|
||||||
port: 3286
|
port: 3286
|
||||||
num_attempts: 15
|
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
level: DEBUG
|
level: DEBUG
|
||||||
|
|
|
@ -327,11 +327,13 @@ modbus:
|
||||||
vbus:
|
vbus:
|
||||||
uart_id: uart_4
|
uart_id: uart_4
|
||||||
|
|
||||||
|
safe_mode:
|
||||||
|
num_attempts: 5
|
||||||
|
reboot_timeout: 10min
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
safe_mode: true
|
|
||||||
port: 3286
|
port: 3286
|
||||||
reboot_timeout: 15min
|
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
hardware_uart: UART1
|
hardware_uart: UART1
|
||||||
|
|
|
@ -102,9 +102,10 @@ uart:
|
||||||
baud_rate: 1200
|
baud_rate: 1200
|
||||||
parity: EVEN
|
parity: EVEN
|
||||||
|
|
||||||
|
safe_mode:
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
safe_mode: true
|
|
||||||
port: 3286
|
port: 3286
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
|
|
Loading…
Reference in a new issue