mirror of
https://github.com/esphome/esphome.git
synced 2026-01-13 05:27:53 -07:00
Compare commits
32 Commits
memory_api
...
2025.12.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93e38f2608 | ||
|
|
3a888326d8 | ||
|
|
f0d0ea60a7 | ||
|
|
7ca11764ab | ||
|
|
3e38a5e630 | ||
|
|
636be92c97 | ||
|
|
195b1c6323 | ||
|
|
7e08092012 | ||
|
|
0ea5f2fd81 | ||
|
|
fa3d998c3d | ||
|
|
864aaeec01 | ||
|
|
9c88e44300 | ||
|
|
4d6a93f92d | ||
|
|
7216120bfd | ||
|
|
8cf0ee38a3 | ||
|
|
4c926cca60 | ||
|
|
57634b612a | ||
|
|
8dff7ee746 | ||
|
|
803bb742c9 | ||
|
|
3e6a65e7dc | ||
|
|
3a101d8886 | ||
|
|
fa0f07bfe9 | ||
|
|
fffa16e4d8 | ||
|
|
734710d22a | ||
|
|
3a1be6822e | ||
|
|
c85b1b8609 | ||
|
|
2e9ddd967c | ||
|
|
078afe9656 | ||
|
|
46574fcbec | ||
|
|
359f45400f | ||
|
|
4da95ccd7e | ||
|
|
c69d58273a |
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.12.0b2
|
||||
PROJECT_NUMBER = 2025.12.1
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
|
||||
AUTO_LOAD = ["bme68x_bsec2"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c")
|
||||
BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_(
|
||||
|
||||
@@ -65,12 +65,6 @@ void CaptivePortal::start() {
|
||||
this->base_->init();
|
||||
if (!this->initialized_) {
|
||||
this->base_->add_handler(this);
|
||||
#ifdef USE_ESP32
|
||||
// Enable LRU socket purging to handle captive portal detection probe bursts
|
||||
// OS captive portal detection makes many simultaneous HTTP requests which can
|
||||
// exhaust sockets. LRU purging automatically closes oldest idle connections.
|
||||
this->base_->get_server()->set_lru_purge_enable(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
|
||||
@@ -40,10 +40,6 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||
void end() {
|
||||
this->active_ = false;
|
||||
this->disable_loop(); // Stop processing DNS requests
|
||||
#ifdef USE_ESP32
|
||||
// Disable LRU socket purging now that captive portal is done
|
||||
this->base_->get_server()->set_lru_purge_enable(false);
|
||||
#endif
|
||||
this->base_->deinit();
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->stop();
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
from esphome import automation
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, CONF_WAIT_TIME
|
||||
from esphome.const import (
|
||||
CONF_CHANNEL,
|
||||
CONF_DATA,
|
||||
CONF_FREQUENCY,
|
||||
CONF_ID,
|
||||
CONF_WAIT_TIME,
|
||||
)
|
||||
from esphome.core import ID
|
||||
|
||||
CODEOWNERS = ["@lygris", "@gabest11"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
@@ -29,7 +37,6 @@ CONF_MANCHESTER = "manchester"
|
||||
CONF_NUM_PREAMBLE = "num_preamble"
|
||||
CONF_SYNC1 = "sync1"
|
||||
CONF_SYNC0 = "sync0"
|
||||
CONF_PKTLEN = "pktlen"
|
||||
CONF_MAGN_TARGET = "magn_target"
|
||||
CONF_MAX_LNA_GAIN = "max_lna_gain"
|
||||
CONF_MAX_DVGA_GAIN = "max_dvga_gain"
|
||||
@@ -41,6 +48,12 @@ CONF_FILTER_LENGTH_ASK_OOK = "filter_length_ask_ook"
|
||||
CONF_FREEZE = "freeze"
|
||||
CONF_HYST_LEVEL = "hyst_level"
|
||||
|
||||
# Packet mode config keys
|
||||
CONF_PACKET_MODE = "packet_mode"
|
||||
CONF_PACKET_LENGTH = "packet_length"
|
||||
CONF_WHITENING = "whitening"
|
||||
CONF_GDO0_PIN = "gdo0_pin"
|
||||
|
||||
# Enums
|
||||
SyncMode = ns.enum("SyncMode", True)
|
||||
SYNC_MODE = {
|
||||
@@ -167,7 +180,6 @@ CONFIG_MAP = {
|
||||
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
|
||||
CONF_SYNC1: cv.hex_uint8_t,
|
||||
CONF_SYNC0: cv.hex_uint8_t,
|
||||
CONF_PKTLEN: cv.uint8_t,
|
||||
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
|
||||
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
|
||||
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
@@ -179,13 +191,36 @@ CONFIG_MAP = {
|
||||
CONF_FREEZE: cv.enum(FREEZE, upper=False),
|
||||
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
|
||||
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
|
||||
CONF_PACKET_MODE: cv.boolean,
|
||||
CONF_PACKET_LENGTH: cv.uint8_t,
|
||||
CONF_CRC_ENABLE: cv.boolean,
|
||||
CONF_WHITENING: cv.boolean,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema({cv.GenerateID(): cv.declare_id(CC1101Component)})
|
||||
|
||||
def _validate_packet_mode(config):
|
||||
if config.get(CONF_PACKET_MODE, False):
|
||||
if CONF_GDO0_PIN not in config:
|
||||
raise cv.Invalid("gdo0_pin is required when packet_mode is enabled")
|
||||
if CONF_PACKET_LENGTH not in config:
|
||||
raise cv.Invalid("packet_length is required when packet_mode is enabled")
|
||||
if config[CONF_PACKET_LENGTH] > 64:
|
||||
raise cv.Invalid("packet_length must be <= 64 (FIFO size)")
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CC1101Component),
|
||||
cv.Optional(CONF_GDO0_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True))
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True)),
|
||||
_validate_packet_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -198,12 +233,29 @@ async def to_code(config):
|
||||
if key in config:
|
||||
cg.add(getattr(var, f"set_{key}")(config[key]))
|
||||
|
||||
if CONF_GDO0_PIN in config:
|
||||
gdo0_pin = await cg.gpio_pin_expression(config[CONF_GDO0_PIN])
|
||||
cg.add(var.set_gdo0_pin(gdo0_pin))
|
||||
if CONF_ON_PACKET in config:
|
||||
await automation.build_automation(
|
||||
var.get_packet_trigger(),
|
||||
[
|
||||
(cg.std_vector.template(cg.uint8), "x"),
|
||||
(cg.float_, "rssi"),
|
||||
(cg.uint8, "lqi"),
|
||||
],
|
||||
config[CONF_ON_PACKET],
|
||||
)
|
||||
|
||||
|
||||
# Actions
|
||||
BeginTxAction = ns.class_("BeginTxAction", automation.Action)
|
||||
BeginRxAction = ns.class_("BeginRxAction", automation.Action)
|
||||
ResetAction = ns.class_("ResetAction", automation.Action)
|
||||
SetIdleAction = ns.class_("SetIdleAction", automation.Action)
|
||||
SendPacketAction = ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
|
||||
)
|
||||
|
||||
CC1101_ACTION_SCHEMA = cv.Schema(
|
||||
maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)})
|
||||
@@ -218,3 +270,42 @@ async def cc1101_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
def validate_raw_data(value):
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
if isinstance(value, list):
|
||||
return cv.Schema([cv.hex_uint8_t])(value)
|
||||
raise cv.Invalid(
|
||||
"data must either be a string wrapped in quotes or a list of bytes"
|
||||
)
|
||||
|
||||
|
||||
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(CC1101Component),
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
key=CONF_DATA,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
|
||||
)
|
||||
async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
data = config[CONF_DATA]
|
||||
if isinstance(data, bytes):
|
||||
data = list(data)
|
||||
if cg.is_template(data):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
|
||||
@@ -99,11 +99,11 @@ CC1101Component::CC1101Component() {
|
||||
this->state_.FS_AUTOCAL = 1;
|
||||
|
||||
// Default Settings
|
||||
this->set_frequency(433920);
|
||||
this->set_if_frequency(153);
|
||||
this->set_filter_bandwidth(203);
|
||||
this->set_frequency(433920000);
|
||||
this->set_if_frequency(153000);
|
||||
this->set_filter_bandwidth(203000);
|
||||
this->set_channel(0);
|
||||
this->set_channel_spacing(200);
|
||||
this->set_channel_spacing(200000);
|
||||
this->set_symbol_rate(5000);
|
||||
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
|
||||
this->set_carrier_sense_above_threshold(true);
|
||||
@@ -143,6 +143,11 @@ void CC1101Component::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup GDO0 pin if configured
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->initialized_ = true;
|
||||
|
||||
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
|
||||
@@ -151,8 +156,69 @@ void CC1101Component::setup() {
|
||||
}
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->write_(Register::PATABLE, this->pa_table_, sizeof(this->pa_table_));
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->strobe_(Command::RX);
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||
!this->gdo0_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read state
|
||||
this->read_(Register::RXBYTES);
|
||||
uint8_t rx_bytes = this->state_.NUM_RXBYTES;
|
||||
bool overflow = this->state_.RXFIFO_OVERFLOW;
|
||||
if (overflow || rx_bytes == 0) {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, flushing");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read packet
|
||||
uint8_t payload_length;
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
|
||||
this->read_(Register::FIFO, &payload_length, 1);
|
||||
} else {
|
||||
payload_length = this->state_.PKTLEN;
|
||||
}
|
||||
if (payload_length == 0 || payload_length > 64) {
|
||||
ESP_LOGW(TAG, "Invalid payload length: %u", payload_length);
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return;
|
||||
}
|
||||
this->packet_.resize(payload_length);
|
||||
this->read_(Register::FIFO, this->packet_.data(), payload_length);
|
||||
|
||||
// Read status and trigger
|
||||
uint8_t status[2];
|
||||
this->read_(Register::FIFO, status, 2);
|
||||
int8_t rssi_raw = static_cast<int8_t>(status[0]);
|
||||
float rssi = (rssi_raw * RSSI_STEP) - RSSI_OFFSET;
|
||||
bool crc_ok = (status[1] & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = status[1] & STATUS_LQI_MASK;
|
||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||
this->packet_trigger_->trigger(this->packet_, rssi, lqi);
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
}
|
||||
|
||||
void CC1101Component::dump_config() {
|
||||
@@ -177,9 +243,12 @@ void CC1101Component::dump_config() {
|
||||
}
|
||||
|
||||
void CC1101Component::begin_tx() {
|
||||
// Ensure Packet Format is 3 (Async Serial), use GDO0 as input during TX
|
||||
// Ensure Packet Format is 3 (Async Serial)
|
||||
this->write_(Register::PKTCTRL0, 0x32);
|
||||
ESP_LOGV(TAG, "Beginning TX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::TX, 50)) {
|
||||
ESP_LOGW(TAG, "Timed out waiting for TX state!");
|
||||
@@ -188,6 +257,9 @@ void CC1101Component::begin_tx() {
|
||||
|
||||
void CC1101Component::begin_rx() {
|
||||
ESP_LOGV(TAG, "Beginning RX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
this->strobe_(Command::RX);
|
||||
}
|
||||
|
||||
@@ -201,20 +273,6 @@ void CC1101Component::set_idle() {
|
||||
this->enter_idle_();
|
||||
}
|
||||
|
||||
void CC1101Component::set_gdo0_config(uint8_t value) {
|
||||
this->state_.GDO0_CFG = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::IOCFG0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_gdo2_config(uint8_t value) {
|
||||
this->state_.GDO2_CFG = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::IOCFG2);
|
||||
}
|
||||
}
|
||||
|
||||
bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) {
|
||||
uint32_t start = millis();
|
||||
while (millis() - start < timeout_ms) {
|
||||
@@ -282,6 +340,33 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
|
||||
this->disable();
|
||||
}
|
||||
|
||||
CC1101Error CC1101Component::transmit_packet(const std::vector<uint8_t> &packet) {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
|
||||
// Write packet
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FTX);
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
|
||||
this->write_(Register::FIFO, static_cast<uint8_t>(packet.size()));
|
||||
}
|
||||
this->write_(Register::FIFO, packet.data(), packet.size());
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::IDLE, 1000)) {
|
||||
ESP_LOGW(TAG, "TX timeout");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return CC1101Error::TIMEOUT;
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return CC1101Error::NONE;
|
||||
}
|
||||
|
||||
// Setters
|
||||
void CC1101Component::set_output_power(float value) {
|
||||
this->output_power_requested_ = value;
|
||||
@@ -428,6 +513,7 @@ void CC1101Component::set_modulation_type(Modulation value) {
|
||||
this->state_.PA_POWER = value == Modulation::MODULATION_ASK_OOK ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->enter_idle_();
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->write_(Register::MDMCFG2);
|
||||
this->write_(Register::FREND0);
|
||||
this->strobe_(Command::RX);
|
||||
@@ -462,13 +548,6 @@ void CC1101Component::set_sync0(uint8_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_pktlen(uint8_t value) {
|
||||
this->state_.PKTLEN = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTLEN);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_magn_target(MagnTarget value) {
|
||||
this->state_.MAGN_TARGET = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
@@ -546,4 +625,50 @@ void CC1101Component::set_hyst_level(HystLevel value) {
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.PKT_FORMAT =
|
||||
static_cast<uint8_t>(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
|
||||
if (value) {
|
||||
// Configure GDO0 for FIFO status (asserts on RX FIFO threshold or end of packet)
|
||||
this->state_.GDO0_CFG = 0x01;
|
||||
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
|
||||
this->state_.FIFO_THR = 15;
|
||||
} else {
|
||||
// Configure GDO0 for serial data (async serial mode)
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::IOCFG0);
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_length(uint8_t value) {
|
||||
if (value == 0) {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
} else {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->state_.PKTLEN = value;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTLEN);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_crc_enable(bool value) {
|
||||
this->state_.CRC_EN = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_whitening(bool value) {
|
||||
this->state_.WHITE_DATA = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::cc1101
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "cc1101defs.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW };
|
||||
|
||||
class CC1101Component : public Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
||||
@@ -15,6 +18,7 @@ class CC1101Component : public Component,
|
||||
CC1101Component();
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
// Actions
|
||||
@@ -24,8 +28,7 @@ class CC1101Component : public Component,
|
||||
void set_idle();
|
||||
|
||||
// GDO Pin Configuration
|
||||
void set_gdo0_config(uint8_t value);
|
||||
void set_gdo2_config(uint8_t value);
|
||||
void set_gdo0_pin(InternalGPIOPin *pin) { this->gdo0_pin_ = pin; }
|
||||
|
||||
// Configuration Setters
|
||||
void set_output_power(float value);
|
||||
@@ -48,7 +51,6 @@ class CC1101Component : public Component,
|
||||
void set_num_preamble(uint8_t value);
|
||||
void set_sync1(uint8_t value);
|
||||
void set_sync0(uint8_t value);
|
||||
void set_pktlen(uint8_t value);
|
||||
|
||||
// AGC settings
|
||||
void set_magn_target(MagnTarget value);
|
||||
@@ -63,6 +65,16 @@ class CC1101Component : public Component,
|
||||
void set_wait_time(WaitTime value);
|
||||
void set_hyst_level(HystLevel value);
|
||||
|
||||
// Packet mode settings
|
||||
void set_packet_mode(bool value);
|
||||
void set_packet_length(uint8_t value);
|
||||
void set_crc_enable(bool value);
|
||||
void set_whitening(bool value);
|
||||
|
||||
// Packet mode operations
|
||||
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
||||
Trigger<std::vector<uint8_t>, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
uint16_t chip_id_{0};
|
||||
bool initialized_{false};
|
||||
@@ -73,6 +85,13 @@ class CC1101Component : public Component,
|
||||
|
||||
CC1101State state_;
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
|
||||
// Packet handling
|
||||
Trigger<std::vector<uint8_t>, float, uint8_t> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, uint8_t>()};
|
||||
std::vector<uint8_t> packet_;
|
||||
|
||||
// Low-level Helpers
|
||||
uint8_t strobe_(Command cmd);
|
||||
void write_(Register reg);
|
||||
@@ -107,4 +126,28 @@ template<typename... Ts> class SetIdleAction : public Action<Ts...>, public Pare
|
||||
void play(const Ts &...x) override { this->parent_->set_idle(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) { this->data_func_ = func; }
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_static_ = data;
|
||||
this->data_static_len_ = len;
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->data_func_) {
|
||||
auto data = this->data_func_(x...);
|
||||
this->parent_->transmit_packet(data);
|
||||
} else if (this->data_static_ != nullptr) {
|
||||
std::vector<uint8_t> data(this->data_static_, this->data_static_ + this->data_static_len_);
|
||||
this->parent_->transmit_packet(data);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
const uint8_t *data_static_{nullptr};
|
||||
size_t data_static_len_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::cc1101
|
||||
|
||||
@@ -6,6 +6,12 @@ namespace esphome::cc1101 {
|
||||
|
||||
static constexpr float XTAL_FREQUENCY = 26000000;
|
||||
|
||||
static constexpr float RSSI_OFFSET = 74.0f;
|
||||
static constexpr float RSSI_STEP = 0.5f;
|
||||
|
||||
static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80;
|
||||
static constexpr uint8_t STATUS_LQI_MASK = 0x7F;
|
||||
|
||||
static constexpr uint8_t BUS_BURST = 0x40;
|
||||
static constexpr uint8_t BUS_READ = 0x80;
|
||||
static constexpr uint8_t BUS_WRITE = 0x00;
|
||||
@@ -134,6 +140,10 @@ enum class SyncMode : uint8_t {
|
||||
SYNC_MODE_15_16,
|
||||
SYNC_MODE_16_16,
|
||||
SYNC_MODE_30_32,
|
||||
SYNC_MODE_NONE_CS,
|
||||
SYNC_MODE_15_16_CS,
|
||||
SYNC_MODE_16_16_CS,
|
||||
SYNC_MODE_30_32_CS,
|
||||
};
|
||||
|
||||
enum class Modulation : uint8_t {
|
||||
@@ -218,6 +228,19 @@ enum class HystLevel : uint8_t {
|
||||
HYST_LEVEL_HIGH,
|
||||
};
|
||||
|
||||
enum class PacketFormat : uint8_t {
|
||||
PACKET_FORMAT_FIFO,
|
||||
PACKET_FORMAT_SYNC_SERIAL,
|
||||
PACKET_FORMAT_RANDOM_TX,
|
||||
PACKET_FORMAT_ASYNC_SERIAL,
|
||||
};
|
||||
|
||||
enum class LengthConfig : uint8_t {
|
||||
LENGTH_CONFIG_FIXED,
|
||||
LENGTH_CONFIG_VARIABLE,
|
||||
LENGTH_CONFIG_INFINITE,
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) CC1101State {
|
||||
// Byte array accessors for bulk SPI transfers
|
||||
uint8_t *regs() { return reinterpret_cast<uint8_t *>(this); }
|
||||
|
||||
@@ -7,9 +7,11 @@ BYTE_ORDER_LITTLE = "little_endian"
|
||||
BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
|
||||
@@ -4,6 +4,7 @@ import itertools
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from esphome import yaml_util
|
||||
import esphome.codegen as cg
|
||||
@@ -616,10 +617,13 @@ def require_vfs_dir() -> None:
|
||||
|
||||
def _parse_idf_component(value: str) -> ConfigType:
|
||||
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
||||
if "^" not in value:
|
||||
raise cv.Invalid(f"Invalid IDF component shorthand '{value}'")
|
||||
name, ref = value.split("^", 1)
|
||||
return {CONF_NAME: name, CONF_REF: ref}
|
||||
# Match operator followed by version-like string (digit or *)
|
||||
if match := re.search(r"(~=|>=|<=|==|!=|>|<|\^|~)(\d|\*)", value):
|
||||
return {CONF_NAME: value[: match.start()], CONF_REF: value[match.start() :]}
|
||||
raise cv.Invalid(
|
||||
f"Invalid IDF component shorthand '{value}'. "
|
||||
f"Expected format: 'owner/component<op>version' where <op> is one of: ^, ~, ~=, ==, !=, >=, >, <=, <"
|
||||
)
|
||||
|
||||
|
||||
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
||||
|
||||
@@ -308,13 +308,21 @@ bool ESP32BLE::ble_setup_() {
|
||||
bool ESP32BLE::ble_dismantle_() {
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
return false;
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
|
||||
if (err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Already disabled");
|
||||
}
|
||||
err = esp_bluedroid_deinit();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
|
||||
return false;
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine
|
||||
if (err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Already deinitialized");
|
||||
}
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
|
||||
@@ -212,17 +212,23 @@ extern ESP32BLE *global_ble;
|
||||
|
||||
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_ble->is_active(); }
|
||||
bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_ble->enable(); }
|
||||
void play(const Ts &...x) override {
|
||||
if (global_ble != nullptr)
|
||||
global_ble->enable();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_ble->disable(); }
|
||||
void play(const Ts &...x) override {
|
||||
if (global_ble != nullptr)
|
||||
global_ble->disable();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::esp32_ble
|
||||
|
||||
@@ -185,7 +185,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
|
||||
|
||||
void ESP32BLETracker::stop_scan_() {
|
||||
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
|
||||
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
// If scanner is already idle, there's nothing to stop - this is not an error
|
||||
if (this->scanner_state_ != ScannerState::IDLE) {
|
||||
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Reset timeout state machine when stopping scan
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
|
||||
from esphome.components.psram import DOMAIN as psram_domain
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -352,6 +352,8 @@ async def to_code(config):
|
||||
cg.add_define("USE_CAMERA")
|
||||
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
|
||||
|
||||
for conf in config.get(CONF_ON_STREAM_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
|
||||
@@ -13,7 +13,7 @@ static const char *const TAG = "espnow.transport";
|
||||
bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); }
|
||||
|
||||
void ESPNowTransport::setup() {
|
||||
packet_transport::PacketTransport::setup();
|
||||
PacketTransport::setup();
|
||||
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGE(TAG, "ESPNow component not set");
|
||||
@@ -26,15 +26,10 @@ void ESPNowTransport::setup() {
|
||||
this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]);
|
||||
|
||||
// Register received handler
|
||||
this->parent_->register_received_handler(static_cast<ESPNowReceivedPacketHandler *>(this));
|
||||
this->parent_->register_received_handler(this);
|
||||
|
||||
// Register broadcasted handler
|
||||
this->parent_->register_broadcasted_handler(static_cast<ESPNowBroadcastedHandler *>(this));
|
||||
}
|
||||
|
||||
void ESPNowTransport::update() {
|
||||
packet_transport::PacketTransport::update();
|
||||
this->updated_ = true;
|
||||
this->parent_->register_broadcasted_handler(this);
|
||||
}
|
||||
|
||||
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||
|
||||
@@ -18,7 +18,6 @@ class ESPNowTransport : public packet_transport::PacketTransport,
|
||||
public ESPNowBroadcastedHandler {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void set_peer_address(peer_address_t address) {
|
||||
|
||||
@@ -434,10 +434,13 @@ def _final_validate_rmii_pins(config: ConfigType) -> None:
|
||||
|
||||
# Check all used pins against RMII reserved pins
|
||||
for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values():
|
||||
for pin_path, _, pin_config in pin_list:
|
||||
for pin_path, pin_device, pin_config in pin_list:
|
||||
pin_num = pin_config.get(CONF_NUMBER)
|
||||
if pin_num not in rmii_pins:
|
||||
continue
|
||||
# Skip if pin is not directly on ESP, but at some expander (device set to something else than 'None')
|
||||
if pin_device is not None:
|
||||
continue
|
||||
# Found a conflict - show helpful error message
|
||||
pin_function = rmii_pins[pin_num]
|
||||
component_path = ".".join(str(p) for p in pin_path)
|
||||
|
||||
@@ -255,6 +255,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
size_t read_index = 0;
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
if (read <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
read_index += read;
|
||||
|
||||
@@ -132,11 +132,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed");
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
} else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (bufsize <= 0) {
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
// bufsize == 0: no more data available, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// add read bytes to MD5
|
||||
md5_receive.add(buf, bufsize);
|
||||
|
||||
@@ -247,6 +254,9 @@ bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
int read_len = 0;
|
||||
while (container->get_bytes_read() < MD5_SIZE) {
|
||||
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
|
||||
if (read_len <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
}
|
||||
|
||||
@@ -76,6 +76,11 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
|
||||
yield();
|
||||
|
||||
if (read_bytes <= 0) {
|
||||
// Network error or connection closed - break to avoid infinite loop
|
||||
break;
|
||||
}
|
||||
|
||||
read_index += read_bytes;
|
||||
}
|
||||
|
||||
|
||||
@@ -176,17 +176,22 @@ async def register_packet_transport(var, config):
|
||||
if encryption := provider.get(CONF_ENCRYPTION):
|
||||
cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption)))
|
||||
|
||||
is_provider = False
|
||||
for sens_conf in config.get(CONF_SENSORS, ()):
|
||||
is_provider = True
|
||||
sens_id = sens_conf[CONF_ID]
|
||||
sensor = await cg.get_variable(sens_id)
|
||||
bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id)
|
||||
cg.add(var.add_sensor(bcst_id, sensor))
|
||||
for sens_conf in config.get(CONF_BINARY_SENSORS, ()):
|
||||
is_provider = True
|
||||
sens_id = sens_conf[CONF_ID]
|
||||
sensor = await cg.get_variable(sens_id)
|
||||
bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id)
|
||||
cg.add(var.add_binary_sensor(bcst_id, sensor))
|
||||
|
||||
if is_provider:
|
||||
cg.add(var.set_is_provider(True))
|
||||
if encryption := config.get(CONF_ENCRYPTION):
|
||||
cg.add(var.set_encryption_key(hash_encryption_key(encryption)))
|
||||
return providers
|
||||
|
||||
@@ -263,6 +263,7 @@ void PacketTransport::flush_() {
|
||||
xxtea::encrypt((uint32_t *) (encode_buffer.data() + header_len), len / 4,
|
||||
(uint32_t *) this->encryption_key_.data());
|
||||
}
|
||||
ESP_LOGVV(TAG, "Sending packet %s", format_hex_pretty(encode_buffer.data(), encode_buffer.size()).c_str());
|
||||
this->send_packet(encode_buffer);
|
||||
}
|
||||
|
||||
@@ -316,6 +317,9 @@ void PacketTransport::send_data_(bool all) {
|
||||
}
|
||||
|
||||
void PacketTransport::update() {
|
||||
// resend all sensors if required
|
||||
if (this->is_provider_)
|
||||
this->send_data_(true);
|
||||
if (!this->ping_pong_enable_) {
|
||||
return;
|
||||
}
|
||||
@@ -551,7 +555,7 @@ void PacketTransport::loop() {
|
||||
if (this->resend_ping_key_)
|
||||
this->send_ping_pong_request_();
|
||||
if (this->updated_) {
|
||||
this->send_data_(this->resend_data_);
|
||||
this->send_data_(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ class PacketTransport : public PollingComponent {
|
||||
}
|
||||
}
|
||||
|
||||
void set_is_provider(bool is_provider) { this->is_provider_ = is_provider; }
|
||||
void set_encryption_key(std::vector<uint8_t> key) { this->encryption_key_ = std::move(key); }
|
||||
void set_rolling_code_enable(bool enable) { this->rolling_code_enable_ = enable; }
|
||||
void set_ping_pong_enable(bool enable) { this->ping_pong_enable_ = enable; }
|
||||
@@ -129,7 +130,7 @@ class PacketTransport : public PollingComponent {
|
||||
uint32_t ping_pong_recyle_time_{};
|
||||
uint32_t last_key_time_{};
|
||||
bool resend_ping_key_{};
|
||||
bool resend_data_{};
|
||||
bool is_provider_{};
|
||||
const char *name_{};
|
||||
ESPPreferenceObject pref_{};
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ from esphome.const import (
|
||||
CONF_UPDATE_INTERVAL,
|
||||
DEVICE_CLASS_PM25,
|
||||
ICON_BLUR,
|
||||
SCHEDULER_DONT_RUN,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
from esphome.core import TimePeriodMilliseconds
|
||||
|
||||
CODEOWNERS = ["@habbie"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
@@ -41,16 +41,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
def validate_interval_uart(config):
|
||||
require_tx = False
|
||||
|
||||
interval = config.get(CONF_UPDATE_INTERVAL)
|
||||
|
||||
if isinstance(interval, TimePeriodMilliseconds):
|
||||
# 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects
|
||||
require_tx = True
|
||||
|
||||
uart.final_validate_device_schema(
|
||||
"pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx
|
||||
"pm1006",
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN,
|
||||
)(config)
|
||||
|
||||
|
||||
|
||||
@@ -232,10 +232,10 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
|
||||
data.set_message_id(this->message_id_.value(x...));
|
||||
data.auto_message_id = this->auto_message_id_.value(x...);
|
||||
std::vector<uint8_t> data_vec;
|
||||
if (this->len_ >= 0) {
|
||||
if (this->len_ > 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
data_vec.assign(this->data_.data, this->data_.data + this->len_);
|
||||
} else {
|
||||
} else if (this->len_ < 0) {
|
||||
// Template mode: call function
|
||||
data_vec = this->data_.func(x...);
|
||||
}
|
||||
@@ -245,7 +245,7 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
|
||||
}
|
||||
|
||||
protected:
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
ssize_t len_{0}; // <0 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
|
||||
@@ -188,7 +188,7 @@ class LWIPRawImpl : public Socket {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen);
|
||||
return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen);
|
||||
}
|
||||
std::string getpeername() override {
|
||||
if (pcb_ == nullptr) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID, TimePeriod
|
||||
@@ -14,7 +15,6 @@ CONF_SX126X_ID = "sx126x_id"
|
||||
CONF_BANDWIDTH = "bandwidth"
|
||||
CONF_BITRATE = "bitrate"
|
||||
CONF_CODING_RATE = "coding_rate"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_CRC_INVERTED = "crc_inverted"
|
||||
CONF_CRC_SIZE = "crc_size"
|
||||
CONF_CRC_POLYNOMIAL = "crc_polynomial"
|
||||
@@ -23,7 +23,6 @@ CONF_DEVIATION = "deviation"
|
||||
CONF_DIO1_PIN = "dio1_pin"
|
||||
CONF_HW_VERSION = "hw_version"
|
||||
CONF_MODULATION = "modulation"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
CONF_PA_POWER = "pa_power"
|
||||
CONF_PA_RAMP = "pa_ramp"
|
||||
CONF_PAYLOAD_LENGTH = "payload_length"
|
||||
|
||||
@@ -12,12 +12,6 @@ void SX126xTransport::setup() {
|
||||
this->parent_->register_listener(this);
|
||||
}
|
||||
|
||||
void SX126xTransport::update() {
|
||||
PacketTransport::update();
|
||||
this->updated_ = true;
|
||||
this->resend_data_ = true;
|
||||
}
|
||||
|
||||
void SX126xTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->transmit_packet(buf); }
|
||||
|
||||
void SX126xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); }
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace sx126x {
|
||||
class SX126xTransport : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID
|
||||
@@ -16,11 +17,9 @@ CONF_BANDWIDTH = "bandwidth"
|
||||
CONF_BITRATE = "bitrate"
|
||||
CONF_BITSYNC = "bitsync"
|
||||
CONF_CODING_RATE = "coding_rate"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DEVIATION = "deviation"
|
||||
CONF_DIO0_PIN = "dio0_pin"
|
||||
CONF_MODULATION = "modulation"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
CONF_PA_PIN = "pa_pin"
|
||||
CONF_PA_POWER = "pa_power"
|
||||
CONF_PA_RAMP = "pa_ramp"
|
||||
|
||||
@@ -12,12 +12,6 @@ void SX127xTransport::setup() {
|
||||
this->parent_->register_listener(this);
|
||||
}
|
||||
|
||||
void SX127xTransport::update() {
|
||||
PacketTransport::update();
|
||||
this->updated_ = true;
|
||||
this->resend_data_ = true;
|
||||
}
|
||||
|
||||
void SX127xTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->transmit_packet(buf); }
|
||||
|
||||
void SX127xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); }
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace sx127x {
|
||||
class SX127xTransport : public packet_transport::PacketTransport, public Parented<SX127x>, public SX127xListener {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ enum TemplateAlarmControlPanelRestoreMode {
|
||||
ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
|
||||
};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct SensorDataStore {
|
||||
bool last_chime_state;
|
||||
};
|
||||
@@ -49,7 +50,6 @@ struct SensorInfo {
|
||||
uint8_t store_index;
|
||||
};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct AlarmSensor {
|
||||
binary_sensor::BinarySensor *sensor;
|
||||
SensorInfo info;
|
||||
@@ -139,6 +139,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
FixedVector<AlarmSensor> sensors_;
|
||||
// a list of automatically bypassed sensors
|
||||
std::vector<uint8_t> bypassed_sensor_indicies_;
|
||||
// Per sensor data store
|
||||
std::vector<SensorDataStore> sensor_data_;
|
||||
uint8_t next_store_index_ = 0;
|
||||
#endif
|
||||
TemplateAlarmControlPanelRestoreMode restore_mode_{};
|
||||
|
||||
@@ -154,14 +157,11 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
uint32_t trigger_time_;
|
||||
// a list of codes
|
||||
std::vector<std::string> codes_;
|
||||
// Per sensor data store
|
||||
std::vector<SensorDataStore> sensor_data_;
|
||||
// requires a code to arm
|
||||
bool requires_code_to_arm_ = false;
|
||||
bool supports_arm_home_ = false;
|
||||
bool supports_arm_night_ = false;
|
||||
bool sensors_ready_ = false;
|
||||
uint8_t next_store_index_ = 0;
|
||||
// check if the code is valid
|
||||
bool is_code_valid_(optional<std::string> code);
|
||||
|
||||
|
||||
@@ -55,12 +55,6 @@ void UARTTransport::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void UARTTransport::update() {
|
||||
this->updated_ = true;
|
||||
this->resend_data_ = true;
|
||||
PacketTransport::update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a byte to the UART bus. If the byte is a flag or control byte, it will be escaped.
|
||||
* @param byte The byte to write.
|
||||
|
||||
@@ -23,7 +23,6 @@ static const uint8_t CONTROL_BYTE = 0x7D;
|
||||
class UARTTransport : public packet_transport::PacketTransport, public UARTDevice {
|
||||
public:
|
||||
void loop() override;
|
||||
void update() override;
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
protected:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "soc/gpio_num.h"
|
||||
#include "soc/uart_pins.h"
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
@@ -139,6 +140,22 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||
|
||||
// Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459
|
||||
// Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks
|
||||
// UART on default UART0 pins that may have residual state from boot console.
|
||||
// Reset these pins before configuring UART to ensure they're in a clean state.
|
||||
if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) {
|
||||
gpio_reset_pin(static_cast<gpio_num_t>(tx));
|
||||
}
|
||||
if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) {
|
||||
gpio_reset_pin(static_cast<gpio_num_t>(rx));
|
||||
}
|
||||
|
||||
// Setup pins after reset to preserve open drain/pullup/pulldown flags
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
@@ -154,10 +171,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||
|
||||
uint32_t invert = 0;
|
||||
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
|
||||
invert |= UART_SIGNAL_TXD_INV;
|
||||
|
||||
@@ -8,29 +8,14 @@ namespace udp {
|
||||
|
||||
static const char *const TAG = "udp_transport";
|
||||
|
||||
bool UDPTransport::should_send() { return this->should_broadcast_ && network::is_connected(); }
|
||||
bool UDPTransport::should_send() { return network::is_connected(); }
|
||||
void UDPTransport::setup() {
|
||||
PacketTransport::setup();
|
||||
this->should_broadcast_ = this->ping_pong_enable_;
|
||||
#ifdef USE_SENSOR
|
||||
this->should_broadcast_ |= !this->sensors_.empty();
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
this->should_broadcast_ |= !this->binary_sensors_.empty();
|
||||
#endif
|
||||
if (this->should_broadcast_)
|
||||
this->parent_->set_should_broadcast();
|
||||
if (!this->providers_.empty() || this->is_encrypted_()) {
|
||||
this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); });
|
||||
}
|
||||
}
|
||||
|
||||
void UDPTransport::update() {
|
||||
PacketTransport::update();
|
||||
this->updated_ = true;
|
||||
this->resend_data_ = this->should_broadcast_;
|
||||
}
|
||||
|
||||
void UDPTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->send_packet(buf); }
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
|
||||
@@ -12,14 +12,12 @@ namespace udp {
|
||||
class UDPTransport : public packet_transport::PacketTransport, public Parented<UDPComponent> {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
protected:
|
||||
void send_packet(const std::vector<uint8_t> &buf) const override;
|
||||
bool should_send() override;
|
||||
bool should_broadcast_{false};
|
||||
size_t get_max_packet_size() override { return MAX_PACKET_SIZE; }
|
||||
};
|
||||
|
||||
|
||||
@@ -117,18 +117,6 @@ void AsyncWebServer::end() {
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncWebServer::set_lru_purge_enable(bool enable) {
|
||||
if (this->lru_purge_enable_ == enable) {
|
||||
return; // No change needed
|
||||
}
|
||||
this->lru_purge_enable_ = enable;
|
||||
// If server is already running, restart it with new config
|
||||
if (this->server_) {
|
||||
this->end();
|
||||
this->begin();
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncWebServer::begin() {
|
||||
if (this->server_) {
|
||||
this->end();
|
||||
@@ -136,8 +124,11 @@ void AsyncWebServer::begin() {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = this->port_;
|
||||
config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
|
||||
// Enable LRU purging if requested (e.g., by captive portal to handle probe bursts)
|
||||
config.lru_purge_enable = this->lru_purge_enable_;
|
||||
// Always enable LRU purging to handle socket exhaustion gracefully.
|
||||
// When max sockets is reached, the oldest connection is closed to make room for new ones.
|
||||
// This prevents "httpd_accept_conn: error in accept (23)" errors.
|
||||
// See: https://github.com/esphome/esphome/issues/12464
|
||||
config.lru_purge_enable = true;
|
||||
// Use custom close function that shuts down before closing to prevent lwIP race conditions
|
||||
config.close_fn = AsyncWebServer::safe_close_with_shutdown;
|
||||
if (httpd_start(&this->server_, &config) == ESP_OK) {
|
||||
|
||||
@@ -199,13 +199,11 @@ class AsyncWebServer {
|
||||
return *handler;
|
||||
}
|
||||
|
||||
void set_lru_purge_enable(bool enable);
|
||||
httpd_handle_t get_server() { return this->server_; }
|
||||
|
||||
protected:
|
||||
uint16_t port_{};
|
||||
httpd_handle_t server_{};
|
||||
bool lru_purge_enable_{false};
|
||||
static esp_err_t request_handler(httpd_req_t *r);
|
||||
static esp_err_t request_post_handler(httpd_req_t *r);
|
||||
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
|
||||
|
||||
@@ -205,6 +205,21 @@ static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
|
||||
/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown
|
||||
static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
|
||||
|
||||
/// Timeout for WiFi scan operations
|
||||
/// This is a fallback in case we don't receive a scan done callback from the WiFi driver.
|
||||
/// Normal scans complete via callback; this only triggers if something goes wrong.
|
||||
static constexpr uint32_t WIFI_SCAN_TIMEOUT_MS = 31000;
|
||||
|
||||
/// Timeout for WiFi connection attempts
|
||||
/// This is a fallback in case we don't receive connection success/failure callbacks.
|
||||
/// Some platforms (especially LibreTiny/Beken) can take 30-60 seconds to connect,
|
||||
/// particularly with fast_connect enabled where no prior scan provides channel info.
|
||||
/// Do not lower this value - connection failures are detected via callbacks, not timeout.
|
||||
/// If this timeout fires prematurely while a connection is still in progress, it causes
|
||||
/// cascading failures: the subsequent scan will also fail because the WiFi driver is
|
||||
/// still busy with the previous connection attempt.
|
||||
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 46000;
|
||||
|
||||
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
|
||||
switch (phase) {
|
||||
case WiFiRetryPhase::INITIAL_CONNECT:
|
||||
@@ -1035,7 +1050,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
|
||||
|
||||
void WiFiComponent::check_scanning_finished() {
|
||||
if (!this->scan_done_) {
|
||||
if (millis() - this->action_started_ > 30000) {
|
||||
if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
|
||||
ESP_LOGE(TAG, "Scan timeout");
|
||||
this->retry_connect();
|
||||
}
|
||||
@@ -1184,8 +1199,9 @@ void WiFiComponent::check_connecting_finished() {
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now - this->action_started_ > 30000) {
|
||||
ESP_LOGW(TAG, "Connection timeout");
|
||||
if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "Connection timeout, aborting connection attempt");
|
||||
this->wifi_disconnect_();
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
@@ -1405,6 +1421,10 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
||||
// without disrupting the captive portal/improv connection
|
||||
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
|
||||
this->restart_adapter();
|
||||
} else {
|
||||
// Even when skipping full restart, disconnect to clear driver state
|
||||
// Without this, platforms like LibreTiny may think we're still connecting
|
||||
this->wifi_disconnect_();
|
||||
}
|
||||
// Clear scan flag - we're starting a new retry cycle
|
||||
this->did_scan_this_cycle_ = false;
|
||||
|
||||
@@ -720,6 +720,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
|
||||
ESP_LOGV(TAG, "STA stop");
|
||||
s_sta_started = false;
|
||||
s_sta_connecting = false;
|
||||
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
|
||||
const auto &it = data->data.sta_authmode_change;
|
||||
|
||||
@@ -291,6 +291,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
|
||||
ESP_LOGV(TAG, "STA stop");
|
||||
s_sta_connecting = false;
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
|
||||
@@ -322,7 +323,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
|
||||
// wifi_sta_connect_status_() to return IDLE. The main loop then sees
|
||||
// "Unknown connection status 0" (wifi_component.cpp check_connecting_finished)
|
||||
// and calls retry_connect(), aborting a connection that may succeed moments later.
|
||||
// Real connection failures will have ssid/bssid populated, or we'll hit the 30s timeout.
|
||||
// Real connection failures will have ssid/bssid populated, or we'll hit the connection timeout.
|
||||
if (it.ssid_len == 0 && s_sta_connecting) {
|
||||
ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)",
|
||||
get_disconnect_reason_str(it.reason));
|
||||
@@ -527,7 +528,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); }
|
||||
bool WiFiComponent::wifi_disconnect_() {
|
||||
// Clear connecting flag first so disconnect events aren't ignored
|
||||
// and wifi_sta_connect_status_() returns IDLE instead of CONNECTING
|
||||
s_sta_connecting = false;
|
||||
return WiFi.disconnect();
|
||||
}
|
||||
|
||||
bssid_t WiFiComponent::wifi_bssid() {
|
||||
bssid_t bssid{};
|
||||
|
||||
@@ -16,7 +16,12 @@ class WiFiSignalSensor : public sensor::Sensor, public PollingComponent {
|
||||
#ifdef USE_WIFI_LISTENERS
|
||||
void setup() override { wifi::global_wifi_component->add_connect_state_listener(this); }
|
||||
#endif
|
||||
void update() override { this->publish_state(wifi::global_wifi_component->wifi_rssi()); }
|
||||
void update() override {
|
||||
int8_t rssi = wifi::global_wifi_component->wifi_rssi();
|
||||
if (rssi != wifi::WIFI_RSSI_DISCONNECTED) {
|
||||
this->publish_state(rssi);
|
||||
}
|
||||
}
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
@@ -1010,14 +1010,14 @@ def validate_config(
|
||||
result.add_error(err)
|
||||
return result
|
||||
|
||||
CORE.raw_config = config
|
||||
|
||||
# 1.1. Merge packages
|
||||
if CONF_PACKAGES in config:
|
||||
from esphome.components.packages import merge_packages
|
||||
|
||||
config = merge_packages(config)
|
||||
|
||||
CORE.raw_config = config
|
||||
|
||||
# 1.2. Resolve !extend and !remove and check for REPLACEME
|
||||
# After this step, there will not be any Extend or Remove values in the config anymore
|
||||
try:
|
||||
|
||||
@@ -71,6 +71,7 @@ from esphome.const import (
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
SCHEDULER_DONT_RUN,
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
VALID_SUBSTITUTIONS_CHARACTERS,
|
||||
@@ -894,7 +895,7 @@ def time_period_in_minutes_(value):
|
||||
|
||||
def update_interval(value):
|
||||
if value == "never":
|
||||
return 4294967295 # uint32_t max
|
||||
return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN)
|
||||
return positive_time_period_milliseconds(value)
|
||||
|
||||
|
||||
@@ -2009,7 +2010,7 @@ def polling_component_schema(default_update_interval):
|
||||
if default_update_interval is None:
|
||||
return COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
Required(CONF_UPDATE_INTERVAL): default_update_interval,
|
||||
Required(CONF_UPDATE_INTERVAL): update_interval,
|
||||
}
|
||||
)
|
||||
assert isinstance(default_update_interval, str)
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.12.0b2"
|
||||
__version__ = "2025.12.1"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
// Platform-agnostic macros for PROGMEM string handling
|
||||
// On ESP32 (both Arduino and IDF): Use plain strings (no PROGMEM)
|
||||
// On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings
|
||||
// On other platforms: Use plain strings (no PROGMEM)
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#define ESPHOME_F(string_literal) (string_literal)
|
||||
#define ESPHOME_PGM_P const char *
|
||||
#define ESPHOME_strncpy_P strncpy
|
||||
#else
|
||||
// ESP8266 and other Arduino platforms use Arduino macros
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266 uses Arduino macros
|
||||
#define ESPHOME_F(string_literal) F(string_literal)
|
||||
#define ESPHOME_PGM_P PGM_P
|
||||
#define ESPHOME_strncpy_P strncpy_P
|
||||
#else
|
||||
#define ESPHOME_F(string_literal) (string_literal)
|
||||
#define ESPHOME_PGM_P const char *
|
||||
#define ESPHOME_strncpy_P strncpy
|
||||
#endif
|
||||
|
||||
@@ -164,8 +164,24 @@ def websocket_method(name):
|
||||
return wrap
|
||||
|
||||
|
||||
class CheckOriginMixin:
|
||||
"""Mixin to handle WebSocket origin checks for reverse proxy setups."""
|
||||
|
||||
def check_origin(self, origin: str) -> bool:
|
||||
if "ESPHOME_TRUSTED_DOMAINS" not in os.environ:
|
||||
return super().check_origin(origin)
|
||||
trusted_domains = [
|
||||
s.strip() for s in os.environ["ESPHOME_TRUSTED_DOMAINS"].split(",")
|
||||
]
|
||||
url = urlparse(origin)
|
||||
if url.hostname in trusted_domains:
|
||||
return True
|
||||
_LOGGER.info("check_origin %s, domain is not trusted", origin)
|
||||
return False
|
||||
|
||||
|
||||
@websocket_class
|
||||
class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
|
||||
class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandler):
|
||||
"""Base class for ESPHome websocket commands."""
|
||||
|
||||
def __init__(
|
||||
@@ -183,18 +199,6 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
|
||||
# use Popen() with a reading thread instead
|
||||
self._use_popen = os.name == "nt"
|
||||
|
||||
def check_origin(self, origin):
|
||||
if "ESPHOME_TRUSTED_DOMAINS" not in os.environ:
|
||||
return super().check_origin(origin)
|
||||
trusted_domains = [
|
||||
s.strip() for s in os.environ["ESPHOME_TRUSTED_DOMAINS"].split(",")
|
||||
]
|
||||
url = urlparse(origin)
|
||||
if url.hostname in trusted_domains:
|
||||
return True
|
||||
_LOGGER.info("check_origin %s, domain is not trusted", origin)
|
||||
return False
|
||||
|
||||
def open(self, *args: str, **kwargs: str) -> None:
|
||||
"""Handle new WebSocket connection."""
|
||||
# Ensure messages from the subprocess are sent immediately
|
||||
@@ -601,7 +605,7 @@ DASHBOARD_SUBSCRIBER = DashboardSubscriber()
|
||||
|
||||
|
||||
@websocket_class
|
||||
class DashboardEventsWebSocket(tornado.websocket.WebSocketHandler):
|
||||
class DashboardEventsWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandler):
|
||||
"""WebSocket handler for real-time dashboard events."""
|
||||
|
||||
_event_listeners: list[Callable[[], None]] | None = None
|
||||
|
||||
@@ -322,8 +322,8 @@ def perform_ota(
|
||||
hash_func, nonce_size, hash_name = _AUTH_METHODS[auth]
|
||||
perform_auth(sock, password, hash_func, nonce_size, hash_name)
|
||||
|
||||
# Set higher timeout during upload
|
||||
sock.settimeout(30.0)
|
||||
# Timeout must match device-side OTA_SOCKET_TIMEOUT_DATA to prevent premature failures
|
||||
sock.settimeout(90.0)
|
||||
|
||||
upload_size = len(upload_contents)
|
||||
upload_size_encoded = [
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages
|
||||
import esphome.config as config_module
|
||||
from esphome.config import resolve_extend_remove
|
||||
from esphome.config_helpers import Extend, Remove
|
||||
import esphome.config_validation as cv
|
||||
@@ -33,6 +34,7 @@ from esphome.const import (
|
||||
CONF_VARS,
|
||||
CONF_WIFI,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.util import OrderedDict
|
||||
|
||||
# Test strings
|
||||
@@ -991,3 +993,35 @@ def test_package_merge_invalid(invalid_package) -> None:
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
merge_packages(config)
|
||||
|
||||
|
||||
def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None:
|
||||
"""Test that CORE.raw_config contains esphome section from merged package.
|
||||
|
||||
This is a regression test for the bug where CORE.raw_config was set before
|
||||
packages were merged, causing KeyError when components accessed
|
||||
CORE.raw_config[CONF_ESPHOME] and the esphome section came from a package.
|
||||
"""
|
||||
# Create a config where esphome section comes from a package
|
||||
test_config = OrderedDict()
|
||||
test_config[CONF_PACKAGES] = {
|
||||
"base": {
|
||||
CONF_ESPHOME: {CONF_NAME: TEST_DEVICE_NAME},
|
||||
}
|
||||
}
|
||||
test_config["esp32"] = {"board": "esp32dev"}
|
||||
|
||||
# Set up CORE for the test
|
||||
test_yaml = tmp_path / "test.yaml"
|
||||
test_yaml.write_text("# test config")
|
||||
CORE.reset()
|
||||
CORE.config_path = test_yaml
|
||||
|
||||
# Call validate_config - this should merge packages and set CORE.raw_config
|
||||
config_module.validate_config(test_config, {})
|
||||
|
||||
# Verify that CORE.raw_config contains the esphome section from the package
|
||||
assert CONF_ESPHOME in CORE.raw_config, (
|
||||
"CORE.raw_config should contain esphome section after package merge"
|
||||
)
|
||||
assert CORE.raw_config[CONF_ESPHOME][CONF_NAME] == TEST_DEVICE_NAME
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
cc1101:
|
||||
id: transceiver
|
||||
cs_pin: ${cs_pin}
|
||||
gdo0_pin: ${gdo0_pin}
|
||||
frequency: 433.92MHz
|
||||
if_frequency: 153kHz
|
||||
filter_bandwidth: 203kHz
|
||||
channel: 0
|
||||
channel_spacing: 200kHz
|
||||
symbol_rate: 5000
|
||||
modulation_type: ASK/OOK
|
||||
symbol_rate: 4800
|
||||
modulation_type: GFSK
|
||||
packet_mode: true
|
||||
packet_length: 8
|
||||
crc_enable: true
|
||||
whitening: false
|
||||
sync_mode: "16/16"
|
||||
sync0: 0x91
|
||||
sync1: 0xD3
|
||||
num_preamble: 2
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("cc1101", "packet %s rssi %.1f dBm lqi %u", format_hex(x).c_str(), rssi, lqi);
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
@@ -18,3 +31,7 @@ button:
|
||||
- cc1101.begin_rx: transceiver
|
||||
- cc1101.set_idle: transceiver
|
||||
- cc1101.reset: transceiver
|
||||
- cc1101.send_packet:
|
||||
data: [0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef]
|
||||
- cc1101.send_packet: !lambda |-
|
||||
return {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
gdo0_pin: GPIO4
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
gdo0_pin: GPIO4
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -62,7 +62,7 @@ packet_transport:
|
||||
sensors:
|
||||
- temp_sensor
|
||||
providers:
|
||||
- name: test_provider
|
||||
- name: test-provider
|
||||
encryption:
|
||||
key: "0123456789abcdef0123456789abcdef"
|
||||
|
||||
@@ -71,6 +71,6 @@ sensor:
|
||||
id: temp_sensor
|
||||
|
||||
- platform: packet_transport
|
||||
provider: test_provider
|
||||
provider: test-provider
|
||||
remote_id: temp_sensor
|
||||
id: remote_temp
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<<: !include common-lan8720.yaml
|
||||
|
||||
sn74hc165:
|
||||
- id: sn74hc165_hub
|
||||
clock_pin: GPIO13
|
||||
data_pin: GPIO14
|
||||
load_pin: GPIO15
|
||||
sr_count: 3
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin:
|
||||
sn74hc165: sn74hc165_hub
|
||||
number: 19
|
||||
id: relay_2
|
||||
@@ -1567,3 +1567,90 @@ async def test_dashboard_yaml_loading_with_packages_and_secrets(
|
||||
# If we get here, secret resolution worked!
|
||||
assert "esphome" in config
|
||||
assert config["esphome"]["name"] == "test-download-secrets"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_check_origin_default_same_origin(
|
||||
dashboard: DashboardTestHelper,
|
||||
) -> None:
|
||||
"""Test WebSocket uses default same-origin check when ESPHOME_TRUSTED_DOMAINS not set."""
|
||||
# Ensure ESPHOME_TRUSTED_DOMAINS is not set
|
||||
env = os.environ.copy()
|
||||
env.pop("ESPHOME_TRUSTED_DOMAINS", None)
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
from tornado.httpclient import HTTPRequest
|
||||
|
||||
url = f"ws://127.0.0.1:{dashboard.port}/events"
|
||||
# Same origin should work (default Tornado behavior)
|
||||
request = HTTPRequest(
|
||||
url, headers={"Origin": f"http://127.0.0.1:{dashboard.port}"}
|
||||
)
|
||||
ws = await websocket_connect(request)
|
||||
try:
|
||||
msg = await ws.read_message()
|
||||
assert msg is not None
|
||||
data = json.loads(msg)
|
||||
assert data["event"] == "initial_state"
|
||||
finally:
|
||||
ws.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_check_origin_trusted_domain(
|
||||
dashboard: DashboardTestHelper,
|
||||
) -> None:
|
||||
"""Test WebSocket accepts connections from trusted domains."""
|
||||
with patch.dict(os.environ, {"ESPHOME_TRUSTED_DOMAINS": "trusted.example.com"}):
|
||||
from tornado.httpclient import HTTPRequest
|
||||
|
||||
url = f"ws://127.0.0.1:{dashboard.port}/events"
|
||||
request = HTTPRequest(url, headers={"Origin": "https://trusted.example.com"})
|
||||
ws = await websocket_connect(request)
|
||||
try:
|
||||
# Should receive initial state
|
||||
msg = await ws.read_message()
|
||||
assert msg is not None
|
||||
data = json.loads(msg)
|
||||
assert data["event"] == "initial_state"
|
||||
finally:
|
||||
ws.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_check_origin_untrusted_domain(
|
||||
dashboard: DashboardTestHelper,
|
||||
) -> None:
|
||||
"""Test WebSocket rejects connections from untrusted domains."""
|
||||
with patch.dict(os.environ, {"ESPHOME_TRUSTED_DOMAINS": "trusted.example.com"}):
|
||||
from tornado.httpclient import HTTPRequest
|
||||
|
||||
url = f"ws://127.0.0.1:{dashboard.port}/events"
|
||||
request = HTTPRequest(url, headers={"Origin": "https://untrusted.example.com"})
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await websocket_connect(request)
|
||||
# Should get HTTP 403 Forbidden due to origin check failure
|
||||
assert exc_info.value.code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_check_origin_multiple_trusted_domains(
|
||||
dashboard: DashboardTestHelper,
|
||||
) -> None:
|
||||
"""Test WebSocket accepts connections from multiple trusted domains."""
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"ESPHOME_TRUSTED_DOMAINS": "first.example.com, second.example.com"},
|
||||
):
|
||||
from tornado.httpclient import HTTPRequest
|
||||
|
||||
url = f"ws://127.0.0.1:{dashboard.port}/events"
|
||||
# Test second domain in list (with space after comma)
|
||||
request = HTTPRequest(url, headers={"Origin": "https://second.example.com"})
|
||||
ws = await websocket_connect(request)
|
||||
try:
|
||||
msg = await ws.read_message()
|
||||
assert msg is not None
|
||||
data = json.loads(msg)
|
||||
assert data["event"] == "initial_state"
|
||||
finally:
|
||||
ws.close()
|
||||
|
||||
Reference in New Issue
Block a user