Compare commits

...

71 Commits

Author SHA1 Message Date
Jonathan Swoboda
e6790f0042 Merge pull request #13308 from esphome/bump-2025.12.7
2025.12.7
2026-01-16 22:49:26 -05:00
Jonathan Swoboda
ec7f72e280 Bump version to 2025.12.7 2026-01-16 22:24:05 -05:00
J. Nick Koston
6f29dbd6f1 [api] Use subtraction for protobuf bounds checking (#13306) 2026-01-16 22:24:05 -05:00
Kevin Ahrendt
9caf78aa7e [i2s_audio] Bugfix: Buffer overflow in software volume control (#13190) 2026-01-16 22:24:05 -05:00
Jonathan Swoboda
6e01c4f86e Merge pull request #13188 from esphome/bump-2025.12.6
2025.12.6
2026-01-13 11:55:44 -05:00
Jonathan Swoboda
f4c17e15ea Bump version to 2025.12.6 2026-01-13 11:01:21 -05:00
J. Nick Koston
d6507ce329 [esphome] Fix OTA backend abort not being called on error (#13182) 2026-01-13 11:01:21 -05:00
Jonathan Swoboda
9504e92458 [remote_transmitter] Fix ESP8266 timing by using busy loop (#13172)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 11:01:21 -05:00
Jonathan Swoboda
3911991de2 [packet_transport] Fix packet size check to account for round4 padding (#13165)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:01:21 -05:00
Jonathan Swoboda
dede47477b [ltr_als_ps] Remove incorrect device_class from count sensors (#13167)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:01:21 -05:00
Jonathan Swoboda
dca8def0f2 [seeed_mr24hpc1] Add ifdef guards for conditional entity types (#13147)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:01:21 -05:00
Samuel Sieb
a1727a8901 [espnow] fix channel validation (#13057) 2026-01-13 11:01:20 -05:00
Jonathan Swoboda
b6f3a5d8b7 Merge pull request #13024 from esphome/bump-2025.12.5
2025.12.5
2026-01-06 10:22:48 -05:00
Jonathan Swoboda
3322b04e00 Bump version to 2025.12.5 2026-01-06 09:35:38 -05:00
Jonathan Swoboda
47d0d3cfeb [cc1101] Add PLL lock verification and retry support (#13006) 2026-01-06 09:35:37 -05:00
Clyde Stubbs
8255c02d5d [esp32_ble] Remove requirement for configured network (#12891) 2026-01-06 09:35:37 -05:00
Conrad Juhl Andersen
8b4ba8dfe6 [wts01] Fix negative values for WTS01 sensor (#12835) 2026-01-06 09:35:37 -05:00
Artur
178a61b6fd [sn74hc595]: fix 'Attempted read from write-only channel' when using esp-idf framework (#12801) 2026-01-06 09:35:37 -05:00
Clyde Stubbs
b5df4cdf1d [lvgl] Fix arc background angles (#12773) 2026-01-06 09:35:37 -05:00
Jonathan Swoboda
d8c23d4fc9 Merge pull request #12772 from esphome/bump-2025.12.4
2025.12.4
2025-12-31 17:42:39 -05:00
Jonathan Swoboda
e9e0712959 Bump version to 2025.12.4 2025-12-31 16:07:00 -05:00
J. Nick Koston
062840dd7b [docker] Add build-essential to fix ruamel.yaml 0.19.0 compilation (#12769)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-31 16:07:00 -05:00
J. Nick Koston
f0f01c081a [wifi] Fix ESP-IDF reporting connected before DHCP completes on reconnect (#12755) 2025-12-31 16:07:00 -05:00
Stuart Parmenter
dd855985be [hub75] Add clipping check (#12762) 2025-12-31 16:06:59 -05:00
Jonathan Swoboda
5b5cede5f9 Merge pull request #12752 from esphome/bump-2025.12.3
2025.12.3
2025-12-30 09:31:31 -05:00
Jonathan Swoboda
c737033cc4 Bump version to 2025.12.3 2025-12-30 09:22:03 -05:00
J. Nick Koston
0194bfd9ea [core] Fix incremental build failures when adding components on ESP32-Arduino (#12745) 2025-12-30 09:22:03 -05:00
J. Nick Koston
339399eb70 [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) 2025-12-30 09:22:03 -05:00
Jonathan Swoboda
99f7e9aeb7 Merge pull request #12632 from esphome/bump-2025.12.2
2025.12.2
2025-12-23 11:17:01 -05:00
Jonathan Swoboda
ebb6babb3d Fix hash 2025-12-23 09:26:38 -05:00
Jonathan Swoboda
0922f240e0 Bump version to 2025.12.2 2025-12-23 09:23:04 -05:00
Jonathan Swoboda
c8fb694dcb [cc1101] Fix packet mode RSSI/LQI (#12630)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 09:23:04 -05:00
J. Nick Koston
6054685dae [esp32_camera] Throttle frame logging to reduce overhead and improve throughput (#12586) 2025-12-23 09:23:04 -05:00
Anna Oake
61ec3508ed [cc1101] Fix option defaults and move them to YAML (#12608) 2025-12-23 09:23:04 -05:00
Leo Bergolth
086ec770ea send NIL ("-") as timestamp if time source is not valid (#12588) 2025-12-23 09:23:04 -05:00
Stuart Parmenter
b055f5b4bf [hub75] Bump esp-hub75 version to 0.1.7 (#12564) 2025-12-23 09:23:00 -05:00
Eduard Llull
726db746c8 [display_menu_base] Call on_value_ after updating the select (#12584) 2025-12-23 09:21:54 -05:00
Keith Burzinski
1922455fa7 [wifi] Fix for wifi_info when static IP is configured (#12576) 2025-12-23 09:21:54 -05:00
Thomas Rupprecht
dc943d7e7a [pca9685,sx126x,sx127x] Use frequency/float_range check (#12490)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-23 09:21:54 -05:00
Jonathan Swoboda
93e38f2608 Merge pull request #12569 from esphome/bump-2025.12.1
2025.12.1
2025-12-19 10:53:05 -05:00
Jonathan Swoboda
3a888326d8 Bump version to 2025.12.1 2025-12-19 10:13:35 -05:00
Keith Burzinski
f0d0ea60a7 [esp32_ble, esp32_ble_tracker] Fix crash, error messages when ble.disable called during boot (#12560) 2025-12-19 10:13:35 -05:00
Jonathan Swoboda
7ca11764ab [template.alarm_control_panel] Fix compile without binary_sensor (#12548)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
3e38a5e630 [esp32_camera] Fix I2C driver conflict with other components (#12533)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
636be92c97 [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jack Wilsdon
195b1c6323 [pm1006] Fix "never" update interval detection (#12529) 2025-12-19 10:13:35 -05:00
Anna Oake
7e08092012 [cc1101] Fix default frequencies (#12539) 2025-12-19 10:13:35 -05:00
Jonathan Swoboda
0ea5f2fd81 Merge pull request #12525 from esphome/bump-2025.12.0
2025.12.0
2025-12-16 18:57:20 -05:00
Jonathan Swoboda
fa3d998c3d Bump version to 2025.12.0 2025-12-16 17:15:50 -05:00
Jonathan Swoboda
864aaeec01 Merge pull request #12520 from esphome/bump-2025.12.0b5
2025.12.0b5
2025-12-16 11:25:57 -05:00
Jonathan Swoboda
9c88e44300 Bump version to 2025.12.0b5 2025-12-16 10:35:31 -05:00
Jonathan Swoboda
4d6a93f92d [uart] Fix UART on default UART0 pins for ESP-IDF (#12519)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 10:35:31 -05:00
J. Nick Koston
7216120bfd [socket] Fix getpeername() returning local address instead of remote in LWIP raw TCP (#12475) 2025-12-16 10:35:31 -05:00
Jonathan Swoboda
8cf0ee38a3 Merge pull request #12513 from esphome/bump-2025.12.0b4
2025.12.0b4
2025-12-15 19:01:02 -05:00
Jonathan Swoboda
4c926cca60 Bump version to 2025.12.0b4 2025-12-15 18:09:42 -05:00
Pascal Vizeli
57634b612a [http_request] Fix infinite loop when server doesn't send Content-Length header (#12480)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 18:09:42 -05:00
Jonathan Swoboda
8dff7ee746 [esp32] Support all IDF component version operators in shorthand syntax (#12499)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 18:09:42 -05:00
Jonathan Swoboda
803bb742c9 [remote_base] Fix crash when ABBWelcome action has no data field (#12493)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 18:09:42 -05:00
Jonathan Swoboda
3e6a65e7dc Merge pull request #12488 from esphome/bump-2025.12.0b3
2025.12.0b3
2025-12-14 19:17:58 -05:00
Jonathan Swoboda
3a101d8886 Bump version to 2025.12.0b3 2025-12-14 18:17:00 -05:00
J. Nick Koston
fa0f07bfe9 [wifi] Fix WiFi recovery after failed connection attempts (#12483) 2025-12-14 18:17:00 -05:00
mbohdal
fffa16e4d8 [ethernet] fix used pins validation in configuration of RMII pins (#12486) 2025-12-14 18:17:00 -05:00
guillempages
734710d22a [core] Use Arduino string macros only on ESP8266 (#12471) 2025-12-14 18:17:00 -05:00
J. Nick Koston
3a1be6822e [ota] Match client timeout to device timeout to prevent premature failures (#12484) 2025-12-14 18:17:00 -05:00
J. Nick Koston
c85b1b8609 [web_server_idf] Always enable LRU purge to prevent socket exhaustion (#12481) 2025-12-14 18:17:00 -05:00
J. Nick Koston
2e9ddd967c [wifi_signal] Skip publishing disconnected RSSI value (#12482) 2025-12-14 18:17:00 -05:00
J. Nick Koston
078afe9656 [dashboard] Add ESPHOME_TRUSTED_DOMAINS support to events WebSocket (#12479) 2025-12-14 18:17:00 -05:00
Jonathan Swoboda
46574fcbec [cc1101] Add packet mode support (#12474)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 18:17:00 -05:00
Jonathan Swoboda
359f45400f [core] Fix polling_component_schema and type consistency (#12478)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 18:16:59 -05:00
Clyde Stubbs
4da95ccd7e [packet_transport] Ensure retransmission at update intervals (#12472)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-14 18:16:59 -05:00
J. Nick Koston
c69d58273a [core] Fix CORE.raw_config not updated after package merge (#12456) 2025-12-14 18:16:59 -05:00
96 changed files with 1448 additions and 503 deletions

View File

@@ -1 +1 @@
766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07 5969e705693278d984c5292e998df0cbaf34f7e1f04dfc7f7b7ad7168527bfa7

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2025.12.0b2 PROJECT_NUMBER = 2025.12.7
# Using the PROJECT_BRIEF tag one can provide an optional one line description # 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 # for a project that appears at the top of each page and should give viewer a

View File

@@ -11,6 +11,16 @@ FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*" RUN git config --system --add safe.directory "*"
# Install build tools for Python packages that require compilation
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
RUN if command -v apk > /dev/null; then \
apk add --no-cache build-base; \
else \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*; \
fi
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14 RUN pip install --no-cache-dir -U pip uv==0.6.14

View File

@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(ADE7880), cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All( cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.Range(min=45.0, max=66.0) cv.frequency, cv.float_range(min=45.0, max=66.0)
), ),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,

View File

@@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
} }
uint32_t field_length = res->as_uint32(); uint32_t field_length = res->as_uint32();
ptr += consumed; ptr += consumed;
if (ptr + field_length > end) { if (field_length > static_cast<size_t>(end - ptr)) {
return count; // Out of bounds return count; // Out of bounds
} }
ptr += field_length; ptr += field_length;
break; break;
} }
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (ptr + 4 > end) { if (end - ptr < 4) {
return count; return count;
} }
ptr += 4; ptr += 4;
@@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
} }
uint32_t field_length = res->as_uint32(); uint32_t field_length = res->as_uint32();
ptr += consumed; ptr += consumed;
if (ptr + field_length > end) { if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return; return;
} }
@@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break; break;
} }
case WIRE_TYPE_FIXED32: { // 32-bit case WIRE_TYPE_FIXED32: { // 32-bit
if (ptr + 4 > end) { if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return; return;
} }

View File

@@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
AUTO_LOAD = ["bme68x_bsec2"] AUTO_LOAD = ["bme68x_bsec2"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
MULTI_CONF = True
bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c") bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c")
BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_( BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_(

View File

@@ -65,12 +65,6 @@ void CaptivePortal::start() {
this->base_->init(); this->base_->init();
if (!this->initialized_) { if (!this->initialized_) {
this->base_->add_handler(this); 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(); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();

View File

@@ -40,10 +40,6 @@ class CaptivePortal : public AsyncWebHandler, public Component {
void end() { void end() {
this->active_ = false; this->active_ = false;
this->disable_loop(); // Stop processing DNS requests 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(); this->base_->deinit();
if (this->dns_server_ != nullptr) { if (this->dns_server_ != nullptr) {
this->dns_server_->stop(); this->dns_server_->stop();

View File

@@ -1,9 +1,17 @@
from esphome import automation from esphome import automation, pins
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import spi from esphome.components import spi
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
import esphome.config_validation as cv 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"] CODEOWNERS = ["@lygris", "@gabest11"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
@@ -29,7 +37,6 @@ CONF_MANCHESTER = "manchester"
CONF_NUM_PREAMBLE = "num_preamble" CONF_NUM_PREAMBLE = "num_preamble"
CONF_SYNC1 = "sync1" CONF_SYNC1 = "sync1"
CONF_SYNC0 = "sync0" CONF_SYNC0 = "sync0"
CONF_PKTLEN = "pktlen"
CONF_MAGN_TARGET = "magn_target" CONF_MAGN_TARGET = "magn_target"
CONF_MAX_LNA_GAIN = "max_lna_gain" CONF_MAX_LNA_GAIN = "max_lna_gain"
CONF_MAX_DVGA_GAIN = "max_dvga_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_FREEZE = "freeze"
CONF_HYST_LEVEL = "hyst_level" 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 # Enums
SyncMode = ns.enum("SyncMode", True) SyncMode = ns.enum("SyncMode", True)
SYNC_MODE = { SYNC_MODE = {
@@ -147,45 +160,89 @@ HYST_LEVEL = {
"High": HystLevel.HYST_LEVEL_HIGH, "High": HystLevel.HYST_LEVEL_HIGH,
} }
# Config key -> Validator mapping # Optional settings to generate setter calls for
CONFIG_MAP = { CONFIG_MAP = {
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0), cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False), cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum(
CONF_DC_BLOCKING_FILTER: cv.boolean, RX_ATTENUATION, upper=False
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)), ),
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean,
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All(
CONF_CHANNEL: cv.uint8_t, cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)), ),
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)), cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All(
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8), cv.frequency, cv.float_range(min=25000, max=788000)
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000), ),
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False), cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All(
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean, cv.frequency, cv.float_range(min=58000, max=812000)
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False), ),
CONF_MANCHESTER: cv.boolean, cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t,
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7), cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All(
CONF_SYNC1: cv.hex_uint8_t, cv.frequency, cv.float_range(min=25000, max=405000)
CONF_SYNC0: cv.hex_uint8_t, ),
CONF_PKTLEN: cv.uint8_t, cv.Optional(CONF_FSK_DEVIATION): cv.All(
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False), cv.frequency, cv.float_range(min=1500, max=381000)
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False), ),
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False), cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8),
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7), cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000),
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False), cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False),
CONF_LNA_PRIORITY: cv.boolean, cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean,
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False), cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum(
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False), MODULATION, upper=False
CONF_FREEZE: cv.enum(FREEZE, upper=False), ),
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False), cv.Optional(CONF_MANCHESTER, default=False): cv.boolean,
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False), cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7),
cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t,
cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t,
cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False),
cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum(
MAX_LNA_GAIN, upper=False
),
cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False),
cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7),
cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum(
CARRIER_SENSE_REL_THR, upper=False
),
cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean,
cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum(
FILTER_LENGTH_FSK_MSK, upper=False
),
cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum(
FILTER_LENGTH_ASK_OOK, upper=False
),
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
} }
CONFIG_SCHEMA = (
cv.Schema({cv.GenerateID(): cv.declare_id(CC1101Component)}) def _validate_packet_mode(config):
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()}) 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(CONFIG_MAP)
.extend(cv.COMPONENT_SCHEMA) .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,
) )
@@ -194,16 +251,34 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
for key in CONFIG_MAP: for opt in CONFIG_MAP:
key = opt.schema
if key in config: if key in config:
cg.add(getattr(var, f"set_{key}")(config[key])) 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 # Actions
BeginTxAction = ns.class_("BeginTxAction", automation.Action) BeginTxAction = ns.class_("BeginTxAction", automation.Action)
BeginRxAction = ns.class_("BeginRxAction", automation.Action) BeginRxAction = ns.class_("BeginRxAction", automation.Action)
ResetAction = ns.class_("ResetAction", automation.Action) ResetAction = ns.class_("ResetAction", automation.Action)
SetIdleAction = ns.class_("SetIdleAction", automation.Action) SetIdleAction = ns.class_("SetIdleAction", automation.Action)
SendPacketAction = ns.class_(
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
)
CC1101_ACTION_SCHEMA = cv.Schema( CC1101_ACTION_SCHEMA = cv.Schema(
maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)}) maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)})
@@ -218,3 +293,42 @@ async def cc1101_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
return var 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

View File

@@ -98,25 +98,8 @@ CC1101Component::CC1101Component() {
this->state_.LENGTH_CONFIG = 2; this->state_.LENGTH_CONFIG = 2;
this->state_.FS_AUTOCAL = 1; this->state_.FS_AUTOCAL = 1;
// Default Settings
this->set_frequency(433920);
this->set_if_frequency(153);
this->set_filter_bandwidth(203);
this->set_channel(0);
this->set_channel_spacing(200);
this->set_symbol_rate(5000);
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
this->set_carrier_sense_above_threshold(true);
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
this->set_lna_priority(false);
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence) // CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
memset(this->pa_table_, 0, sizeof(this->pa_table_)); memset(this->pa_table_, 0, sizeof(this->pa_table_));
this->set_output_power(10.0f);
} }
void CC1101Component::setup() { void CC1101Component::setup() {
@@ -143,6 +126,11 @@ void CC1101Component::setup() {
return; return;
} }
// Setup GDO0 pin if configured
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->setup();
}
this->initialized_ = true; this->initialized_ = true;
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) { for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
@@ -151,8 +139,70 @@ void CC1101Component::setup() {
} }
this->write_(static_cast<Register>(i)); 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); if (!this->enter_rx_()) {
this->mark_failed();
return;
}
// 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->enter_rx_();
return;
}
// Read packet
uint8_t payload_length, expected_rx;
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
this->read_(Register::FIFO, &payload_length, 1);
expected_rx = payload_length + 1;
} else {
payload_length = this->state_.PKTLEN;
expected_rx = payload_length;
}
if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) {
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
this->enter_idle_();
this->strobe_(Command::FRX);
this->enter_rx_();
return;
}
this->packet_.resize(payload_length);
this->read_(Register::FIFO, this->packet_.data(), payload_length);
// Read status from registers (more reliable than FIFO status bytes due to timing issues)
this->read_(Register::RSSI);
this->read_(Register::LQI);
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & 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->enter_rx_();
} }
void CC1101Component::dump_config() { void CC1101Component::dump_config() {
@@ -177,18 +227,25 @@ void CC1101Component::dump_config() {
} }
void CC1101Component::begin_tx() { 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); this->write_(Register::PKTCTRL0, 0x32);
ESP_LOGV(TAG, "Beginning TX sequence"); ESP_LOGV(TAG, "Beginning TX sequence");
this->strobe_(Command::TX); if (this->gdo0_pin_ != nullptr) {
if (!this->wait_for_state_(State::TX, 50)) { this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
ESP_LOGW(TAG, "Timed out waiting for TX state!"); }
if (!this->enter_tx_()) {
ESP_LOGW(TAG, "Failed to enter TX state!");
} }
} }
void CC1101Component::begin_rx() { void CC1101Component::begin_rx() {
ESP_LOGV(TAG, "Beginning RX sequence"); ESP_LOGV(TAG, "Beginning RX sequence");
this->strobe_(Command::RX); if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
}
if (!this->enter_rx_()) {
ESP_LOGW(TAG, "Failed to enter RX state!");
}
} }
void CC1101Component::reset() { void CC1101Component::reset() {
@@ -201,20 +258,6 @@ void CC1101Component::set_idle() {
this->enter_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) { bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) {
uint32_t start = millis(); uint32_t start = millis();
while (millis() - start < timeout_ms) { while (millis() - start < timeout_ms) {
@@ -228,11 +271,33 @@ bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) {
return false; return false;
} }
bool CC1101Component::enter_calibrated_(State target_state, Command cmd) {
// The PLL must be recalibrated until PLL lock is achieved
for (uint8_t retries = PLL_LOCK_RETRIES; retries > 0; retries--) {
this->strobe_(cmd);
if (!this->wait_for_state_(target_state)) {
return false;
}
this->read_(Register::FSCAL1);
if (this->state_.FSCAL1 != FSCAL1_PLL_NOT_LOCKED) {
return true;
}
ESP_LOGW(TAG, "PLL lock failed, retrying calibration");
this->enter_idle_();
}
ESP_LOGE(TAG, "PLL lock failed after retries");
return false;
}
void CC1101Component::enter_idle_() { void CC1101Component::enter_idle_() {
this->strobe_(Command::IDLE); this->strobe_(Command::IDLE);
this->wait_for_state_(State::IDLE); this->wait_for_state_(State::IDLE);
} }
bool CC1101Component::enter_rx_() { return this->enter_calibrated_(State::RX, Command::RX); }
bool CC1101Component::enter_tx_() { return this->enter_calibrated_(State::TX, Command::TX); }
uint8_t CC1101Component::strobe_(Command cmd) { uint8_t CC1101Component::strobe_(Command cmd) {
uint8_t index = static_cast<uint8_t>(cmd); uint8_t index = static_cast<uint8_t>(cmd);
if (cmd < Command::RES || cmd > Command::NOP) { if (cmd < Command::RES || cmd > Command::NOP) {
@@ -282,6 +347,41 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
this->disable(); 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());
// Calibrate PLL
if (!this->enter_calibrated_(State::FSTXON, Command::FSTXON)) {
ESP_LOGW(TAG, "PLL lock failed during TX");
this->enter_idle_();
this->enter_rx_();
return CC1101Error::PLL_LOCK;
}
// Transmit packet
this->strobe_(Command::TX);
if (!this->wait_for_state_(State::IDLE, 1000)) {
ESP_LOGW(TAG, "TX timeout");
this->enter_idle_();
this->enter_rx_();
return CC1101Error::TIMEOUT;
}
// Return to rx
this->enter_rx_();
return CC1101Error::NONE;
}
// Setters // Setters
void CC1101Component::set_output_power(float value) { void CC1101Component::set_output_power(float value) {
this->output_power_requested_ = value; this->output_power_requested_ = value;
@@ -335,7 +435,7 @@ void CC1101Component::set_frequency(float value) {
this->write_(Register::FREQ2); this->write_(Register::FREQ2);
this->write_(Register::FREQ1); this->write_(Register::FREQ1);
this->write_(Register::FREQ0); this->write_(Register::FREQ0);
this->strobe_(Command::RX); this->enter_rx_();
} }
} }
@@ -362,7 +462,7 @@ void CC1101Component::set_channel(uint8_t value) {
if (this->initialized_) { if (this->initialized_) {
this->enter_idle_(); this->enter_idle_();
this->write_(Register::CHANNR); this->write_(Register::CHANNR);
this->strobe_(Command::RX); this->enter_rx_();
} }
} }
@@ -428,9 +528,10 @@ void CC1101Component::set_modulation_type(Modulation value) {
this->state_.PA_POWER = value == Modulation::MODULATION_ASK_OOK ? 1 : 0; this->state_.PA_POWER = value == Modulation::MODULATION_ASK_OOK ? 1 : 0;
if (this->initialized_) { if (this->initialized_) {
this->enter_idle_(); this->enter_idle_();
this->set_output_power(this->output_power_requested_);
this->write_(Register::MDMCFG2); this->write_(Register::MDMCFG2);
this->write_(Register::FREND0); this->write_(Register::FREND0);
this->strobe_(Command::RX); this->enter_rx_();
} }
} }
@@ -462,13 +563,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) { void CC1101Component::set_magn_target(MagnTarget value) {
this->state_.MAGN_TARGET = static_cast<uint8_t>(value); this->state_.MAGN_TARGET = static_cast<uint8_t>(value);
if (this->initialized_) { if (this->initialized_) {
@@ -546,4 +640,53 @@ 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;
// Don't append status bytes to FIFO - we read from registers instead
this->state_.APPEND_STATUS = 0;
} else {
// Configure GDO0 for serial data (async serial mode)
this->state_.GDO0_CFG = 0x0D;
}
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
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 } // namespace esphome::cc1101

View File

@@ -5,9 +5,12 @@
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "cc1101defs.h" #include "cc1101defs.h"
#include <vector>
namespace esphome::cc1101 { namespace esphome::cc1101 {
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
class CC1101Component : public Component, class CC1101Component : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> { spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
@@ -15,6 +18,7 @@ class CC1101Component : public Component,
CC1101Component(); CC1101Component();
void setup() override; void setup() override;
void loop() override;
void dump_config() override; void dump_config() override;
// Actions // Actions
@@ -24,8 +28,7 @@ class CC1101Component : public Component,
void set_idle(); void set_idle();
// GDO Pin Configuration // GDO Pin Configuration
void set_gdo0_config(uint8_t value); void set_gdo0_pin(InternalGPIOPin *pin) { this->gdo0_pin_ = pin; }
void set_gdo2_config(uint8_t value);
// Configuration Setters // Configuration Setters
void set_output_power(float value); void set_output_power(float value);
@@ -48,7 +51,6 @@ class CC1101Component : public Component,
void set_num_preamble(uint8_t value); void set_num_preamble(uint8_t value);
void set_sync1(uint8_t value); void set_sync1(uint8_t value);
void set_sync0(uint8_t value); void set_sync0(uint8_t value);
void set_pktlen(uint8_t value);
// AGC settings // AGC settings
void set_magn_target(MagnTarget value); void set_magn_target(MagnTarget value);
@@ -63,6 +65,16 @@ class CC1101Component : public Component,
void set_wait_time(WaitTime value); void set_wait_time(WaitTime value);
void set_hyst_level(HystLevel 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: protected:
uint16_t chip_id_{0}; uint16_t chip_id_{0};
bool initialized_{false}; bool initialized_{false};
@@ -73,6 +85,13 @@ class CC1101Component : public Component,
CC1101State state_; 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 // Low-level Helpers
uint8_t strobe_(Command cmd); uint8_t strobe_(Command cmd);
void write_(Register reg); void write_(Register reg);
@@ -83,7 +102,10 @@ class CC1101Component : public Component,
// State Management // State Management
bool wait_for_state_(State target_state, uint32_t timeout_ms = 100); bool wait_for_state_(State target_state, uint32_t timeout_ms = 100);
bool enter_calibrated_(State target_state, Command cmd);
void enter_idle_(); void enter_idle_();
bool enter_rx_();
bool enter_tx_();
}; };
// Action Wrappers // Action Wrappers
@@ -107,4 +129,28 @@ template<typename... Ts> class SetIdleAction : public Action<Ts...>, public Pare
void play(const Ts &...x) override { this->parent_->set_idle(); } 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 } // namespace esphome::cc1101

View File

@@ -6,6 +6,15 @@ namespace esphome::cc1101 {
static constexpr float XTAL_FREQUENCY = 26000000; static constexpr float XTAL_FREQUENCY = 26000000;
static constexpr float RSSI_OFFSET = 74.0f;
static constexpr float RSSI_STEP = 0.5f;
static constexpr uint8_t FSCAL1_PLL_NOT_LOCKED = 0x3F;
static constexpr uint8_t PLL_LOCK_RETRIES = 3;
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_BURST = 0x40;
static constexpr uint8_t BUS_READ = 0x80; static constexpr uint8_t BUS_READ = 0x80;
static constexpr uint8_t BUS_WRITE = 0x00; static constexpr uint8_t BUS_WRITE = 0x00;
@@ -134,6 +143,10 @@ enum class SyncMode : uint8_t {
SYNC_MODE_15_16, SYNC_MODE_15_16,
SYNC_MODE_16_16, SYNC_MODE_16_16,
SYNC_MODE_30_32, 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 { enum class Modulation : uint8_t {
@@ -218,6 +231,19 @@ enum class HystLevel : uint8_t {
HYST_LEVEL_HIGH, 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 { struct __attribute__((packed)) CC1101State {
// Byte array accessors for bulk SPI transfers // Byte array accessors for bulk SPI transfers
uint8_t *regs() { return reinterpret_cast<uint8_t *>(this); } uint8_t *regs() { return reinterpret_cast<uint8_t *>(this); }

View File

@@ -7,9 +7,11 @@ BYTE_ORDER_LITTLE = "little_endian"
BYTE_ORDER_BIG = "big_endian" BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth" CONF_COLOR_DEPTH = "color_depth"
CONF_CRC_ENABLE = "crc_enable"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLED = "enabled" CONF_ENABLED = "enabled"
CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_ON_PACKET = "on_packet"
CONF_ON_RECEIVE = "on_receive" CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"

View File

@@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() {
if (this->select_var_ != nullptr) { if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_next(true).perform(); this->select_var_->make_call().select_next(true).perform();
this->on_value_();
changed = true; changed = true;
} }
@@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() {
if (this->select_var_ != nullptr) { if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_previous(true).perform(); this->select_var_->make_call().select_previous(true).perform();
this->on_value_();
changed = true; changed = true;
} }

View File

@@ -4,6 +4,7 @@ import itertools
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
import re
from esphome import yaml_util from esphome import yaml_util
import esphome.codegen as cg import esphome.codegen as cg
@@ -616,10 +617,13 @@ def require_vfs_dir() -> None:
def _parse_idf_component(value: str) -> ConfigType: def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'""" """Parse IDF component shorthand syntax like 'owner/component^version'"""
if "^" not in value: # Match operator followed by version-like string (digit or *)
raise cv.Invalid(f"Invalid IDF component shorthand '{value}'") if match := re.search(r"(~=|>=|<=|==|!=|>|<|\^|~)(\d|\*)", value):
name, ref = value.split("^", 1) return {CONF_NAME: value[: match.start()], CONF_REF: value[match.start() :]}
return {CONF_NAME: name, CONF_REF: ref} 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: def _validate_idf_component(config: ConfigType) -> ConfigType:

View File

@@ -22,7 +22,6 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
import esphome.final_validate as fv import esphome.final_validate as fv
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble" DOMAIN = "esp32_ble"

View File

@@ -308,13 +308,21 @@ bool ESP32BLE::ble_setup_() {
bool ESP32BLE::ble_dismantle_() { bool ESP32BLE::ble_dismantle_() {
esp_err_t err = esp_bluedroid_disable(); esp_err_t err = esp_bluedroid_disable();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); // ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
return false; 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(); err = esp_bluedroid_deinit();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err); // ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine
return false; 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 #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID

View File

@@ -212,17 +212,23 @@ extern ESP32BLE *global_ble;
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> { template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
public: 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...> { template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
public: 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...> { template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
public: 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 } // namespace esphome::esp32_ble

View File

@@ -185,7 +185,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
void ESP32BLETracker::stop_scan_() { void ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { 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; return;
} }
// Reset timeout state machine when stopping scan // Reset timeout state machine when stopping scan

View File

@@ -3,7 +3,7 @@ import logging
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c 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 from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All(
cv.frequency, cv.Range(min=8e6, max=20e6) cv.frequency, cv.float_range(min=8e6, max=20e6)
), ),
} }
), ),
@@ -352,6 +352,8 @@ async def to_code(config):
cg.add_define("USE_CAMERA") cg.add_define("USE_CAMERA")
add_idf_component(name="espressif/esp32-camera", ref="2.1.1") 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, []): for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -11,6 +11,9 @@ namespace esphome {
namespace esp32_camera { namespace esp32_camera {
static const char *const TAG = "esp32_camera"; static const char *const TAG = "esp32_camera";
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
#endif
/* ---------------- public API (derivated) ---------------- */ /* ---------------- public API (derivated) ---------------- */
void ESP32Camera::setup() { void ESP32Camera::setup() {
@@ -204,7 +207,20 @@ void ESP32Camera::loop() {
} }
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_); this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
ESP_LOGD(TAG, "Got Image: len=%u", fb->len); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Got Image: len=%u", fb->len);
#else
// Initialize log time on first frame to ensure accurate interval measurement
if (this->frame_count_ == 0) {
this->last_log_time_ = now;
}
this->frame_count_++;
if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) {
ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
this->last_log_time_ = now;
this->frame_count_ = 0;
}
#endif
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
listener->on_camera_image(this->current_image_); listener->on_camera_image(this->current_image_);
} }

View File

@@ -213,6 +213,10 @@ class ESP32Camera : public camera::Camera {
uint32_t last_idle_request_{0}; uint32_t last_idle_request_{0};
uint32_t last_update_{0}; uint32_t last_update_{0};
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
uint32_t last_log_time_{0};
uint16_t frame_count_{0};
#endif
#ifdef USE_I2C #ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr}; i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C #endif // USE_I2C

View File

@@ -16,7 +16,7 @@ def valid_pwm_pin(value):
esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm") esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm")
ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component) ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component)
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action) SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend( output.FLOAT_OUTPUT_SCHEMA.extend(

View File

@@ -395,12 +395,14 @@ void ESPHomeOTAComponent::handle_data_() {
error: error:
this->write_byte_(static_cast<uint8_t>(error_code)); this->write_byte_(static_cast<uint8_t>(error_code));
this->cleanup_connection_();
// Abort backend before cleanup - cleanup_connection_() destroys the backend
if (this->backend_ != nullptr && update_started) { if (this->backend_ != nullptr && update_started) {
this->backend_->abort(); this->backend_->abort();
} }
this->cleanup_connection_();
this->status_momentary_error("err", 5000); this->status_momentary_error("err", 5000);
#ifdef USE_OTA_STATE_CALLBACK #ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code)); this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));

View File

@@ -66,11 +66,17 @@ CONF_WAIT_FOR_SENT = "wait_for_sent"
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
def validate_channel(value):
if value is None:
raise cv.Invalid("channel is required if wifi is not configured")
return wifi.validate_channel(value)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESPNowComponent), cv.GenerateID(): cv.declare_id(ESPNowComponent),
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): wifi.validate_channel, cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): validate_channel,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean, cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean,
cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address), cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address),

View File

@@ -13,7 +13,7 @@ static const char *const TAG = "espnow.transport";
bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); } bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); }
void ESPNowTransport::setup() { void ESPNowTransport::setup() {
packet_transport::PacketTransport::setup(); PacketTransport::setup();
if (this->parent_ == nullptr) { if (this->parent_ == nullptr) {
ESP_LOGE(TAG, "ESPNow component not set"); 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]); this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]);
// Register received handler // Register received handler
this->parent_->register_received_handler(static_cast<ESPNowReceivedPacketHandler *>(this)); this->parent_->register_received_handler(this);
// Register broadcasted handler // Register broadcasted handler
this->parent_->register_broadcasted_handler(static_cast<ESPNowBroadcastedHandler *>(this)); this->parent_->register_broadcasted_handler(this);
}
void ESPNowTransport::update() {
packet_transport::PacketTransport::update();
this->updated_ = true;
} }
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const { void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {

View File

@@ -18,7 +18,6 @@ class ESPNowTransport : public packet_transport::PacketTransport,
public ESPNowBroadcastedHandler { public ESPNowBroadcastedHandler {
public: public:
void setup() override; void setup() override;
void update() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void set_peer_address(peer_address_t address) { void set_peer_address(peer_address_t address) {

View File

@@ -434,10 +434,13 @@ def _final_validate_rmii_pins(config: ConfigType) -> None:
# Check all used pins against RMII reserved pins # Check all used pins against RMII reserved pins
for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values(): 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) pin_num = pin_config.get(CONF_NUMBER)
if pin_num not in rmii_pins: if pin_num not in rmii_pins:
continue 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 # Found a conflict - show helpful error message
pin_function = rmii_pins[pin_num] pin_function = rmii_pins[pin_num]
component_path = ".".join(str(p) for p in pin_path) component_path = ".".join(str(p) for p in pin_path)

View File

@@ -255,6 +255,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
size_t read_index = 0; size_t read_index = 0;
while (container->get_bytes_read() < max_length) { while (container->get_bytes_read() < max_length) {
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512)); int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
if (read <= 0) {
break;
}
App.feed_wdt(); App.feed_wdt();
yield(); yield();
read_index += read; read_index += read;

View File

@@ -132,11 +132,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
App.feed_wdt(); App.feed_wdt();
yield(); yield();
if (bufsize < 0) { // Exit loop if no data available (stream closed or end of data)
ESP_LOGE(TAG, "Stream closed"); if (bufsize <= 0) {
this->cleanup_(std::move(backend), container); if (bufsize < 0) {
return OTA_CONNECTION_ERROR; ESP_LOGE(TAG, "Stream closed with error");
} else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) { 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 // add read bytes to MD5
md5_receive.add(buf, bufsize); md5_receive.add(buf, bufsize);
@@ -247,6 +254,9 @@ bool OtaHttpRequestComponent::http_get_md5_() {
int read_len = 0; int read_len = 0;
while (container->get_bytes_read() < MD5_SIZE) { while (container->get_bytes_read() < MD5_SIZE) {
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE); read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
if (read_len <= 0) {
break;
}
App.feed_wdt(); App.feed_wdt();
yield(); yield();
} }

View File

@@ -76,6 +76,11 @@ void HttpRequestUpdate::update_task(void *params) {
yield(); yield();
if (read_bytes <= 0) {
// Network error or connection closed - break to avoid infinite loop
break;
}
read_index += read_bytes; read_index += read_bytes;
} }

View File

@@ -93,35 +93,35 @@ CONF_DOUBLE_BUFFER = "double_buffer"
CONF_MIN_REFRESH_RATE = "min_refresh_rate" CONF_MIN_REFRESH_RATE = "min_refresh_rate"
# Map to hub75 library enums (in global namespace) # Map to hub75 library enums (in global namespace)
ShiftDriver = cg.global_ns.enum("ShiftDriver", is_class=True) Hub75ShiftDriver = cg.global_ns.enum("Hub75ShiftDriver", is_class=True)
SHIFT_DRIVERS = { SHIFT_DRIVERS = {
"GENERIC": ShiftDriver.GENERIC, "GENERIC": Hub75ShiftDriver.GENERIC,
"FM6126A": ShiftDriver.FM6126A, "FM6126A": Hub75ShiftDriver.FM6126A,
"ICN2038S": ShiftDriver.ICN2038S, "ICN2038S": Hub75ShiftDriver.ICN2038S,
"FM6124": ShiftDriver.FM6124, "FM6124": Hub75ShiftDriver.FM6124,
"MBI5124": ShiftDriver.MBI5124, "MBI5124": Hub75ShiftDriver.MBI5124,
"DP3246": ShiftDriver.DP3246, "DP3246": Hub75ShiftDriver.DP3246,
} }
PanelLayout = cg.global_ns.enum("PanelLayout", is_class=True) Hub75PanelLayout = cg.global_ns.enum("Hub75PanelLayout", is_class=True)
PANEL_LAYOUTS = { PANEL_LAYOUTS = {
"HORIZONTAL": PanelLayout.HORIZONTAL, "HORIZONTAL": Hub75PanelLayout.HORIZONTAL,
"TOP_LEFT_DOWN": PanelLayout.TOP_LEFT_DOWN, "TOP_LEFT_DOWN": Hub75PanelLayout.TOP_LEFT_DOWN,
"TOP_RIGHT_DOWN": PanelLayout.TOP_RIGHT_DOWN, "TOP_RIGHT_DOWN": Hub75PanelLayout.TOP_RIGHT_DOWN,
"BOTTOM_LEFT_UP": PanelLayout.BOTTOM_LEFT_UP, "BOTTOM_LEFT_UP": Hub75PanelLayout.BOTTOM_LEFT_UP,
"BOTTOM_RIGHT_UP": PanelLayout.BOTTOM_RIGHT_UP, "BOTTOM_RIGHT_UP": Hub75PanelLayout.BOTTOM_RIGHT_UP,
"TOP_LEFT_DOWN_ZIGZAG": PanelLayout.TOP_LEFT_DOWN_ZIGZAG, "TOP_LEFT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
"TOP_RIGHT_DOWN_ZIGZAG": PanelLayout.TOP_RIGHT_DOWN_ZIGZAG, "TOP_RIGHT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
"BOTTOM_LEFT_UP_ZIGZAG": PanelLayout.BOTTOM_LEFT_UP_ZIGZAG, "BOTTOM_LEFT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
"BOTTOM_RIGHT_UP_ZIGZAG": PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG, "BOTTOM_RIGHT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
} }
ScanPattern = cg.global_ns.enum("ScanPattern", is_class=True) Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_PATTERNS = { SCAN_PATTERNS = {
"STANDARD_TWO_SCAN": ScanPattern.STANDARD_TWO_SCAN, "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": ScanPattern.FOUR_SCAN_16PX_HIGH, "FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": ScanPattern.FOUR_SCAN_32PX_HIGH, "FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": ScanPattern.FOUR_SCAN_64PX_HIGH, "FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
} }
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True) Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
@@ -528,7 +528,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
add_idf_component( add_idf_component(
name="esphome/esp-hub75", name="esphome/esp-hub75",
ref="0.1.6", ref="0.1.7",
) )
# Set compile-time configuration via defines # Set compile-time configuration via defines

View File

@@ -111,6 +111,9 @@ void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]] if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]]
return; return;
if (!this->get_clipping().inside(x, y))
return;
driver_->set_pixel(x, y, color.r, color.g, color.b); driver_->set_pixel(x, y, color.r, color.g, color.b);
App.feed_wdt(); App.feed_wdt();
} }

View File

@@ -121,7 +121,7 @@ CONFIG_SCHEMA = cv.All(
nrf52="100kHz", nrf52="100kHz",
): cv.All( ): cv.All(
cv.frequency, cv.frequency,
cv.Range(min=0, min_included=False), cv.float_range(min=0, min_included=False),
), ),
cv.Optional(CONF_TIMEOUT): cv.All( cv.Optional(CONF_TIMEOUT): cv.All(
cv.only_with_framework(["arduino", "esp-idf"]), cv.only_with_framework(["arduino", "esp-idf"]),

View File

@@ -340,8 +340,8 @@ void I2SAudioSpeaker::speaker_task(void *params) {
const uint32_t read_delay = const uint32_t read_delay =
(this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; (this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2;
uint8_t *new_data = transfer_buffer->get_buffer_end(); // track start of any newly copied bytes
size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay)); size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay));
uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read;
if (bytes_read > 0) { if (bytes_read > 0) {
if (this_speaker->q15_volume_factor_ < INT16_MAX) { if (this_speaker->q15_volume_factor_ < INT16_MAX) {

View File

@@ -45,7 +45,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{ {
cv.Required(CONF_ID): cv.declare_id(LEDCOutput), cv.Required(CONF_ID): cv.declare_id(LEDCOutput),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All(
cv.frequency, cv.float_range(min=0, min_included=False)
),
cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15),
cv.Optional(CONF_PHASE_ANGLE): cv.All( cv.Optional(CONF_PHASE_ANGLE): cv.All(
cv.only_with_esp_idf, cv.angle, cv.float_range(min=0.0, max=360.0) cv.only_with_esp_idf, cv.angle, cv.float_range(min=0.0, max=360.0)

View File

@@ -14,7 +14,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{ {
cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM), cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All(
cv.frequency, cv.float_range(min=0, min_included=False)
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)

View File

@@ -16,7 +16,6 @@ from esphome.const import (
CONF_REPEAT, CONF_REPEAT,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TYPE, CONF_TYPE,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
ICON_BRIGHTNESS_5, ICON_BRIGHTNESS_5,
ICON_BRIGHTNESS_6, ICON_BRIGHTNESS_6,
@@ -169,7 +168,6 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_COUNTS, unit_of_measurement=UNIT_COUNTS,
icon=ICON_BRIGHTNESS_5, icon=ICON_BRIGHTNESS_5,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
key=CONF_NAME, key=CONF_NAME,
@@ -179,7 +177,6 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_COUNTS, unit_of_measurement=UNIT_COUNTS,
icon=ICON_BRIGHTNESS_7, icon=ICON_BRIGHTNESS_7,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
key=CONF_NAME, key=CONF_NAME,
@@ -189,7 +186,6 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_COUNTS, unit_of_measurement=UNIT_COUNTS,
icon=ICON_PROXIMITY, icon=ICON_PROXIMITY,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
key=CONF_NAME, key=CONF_NAME,
@@ -198,7 +194,6 @@ CONFIG_SCHEMA = cv.All(
sensor.sensor_schema( sensor.sensor_schema(
icon=ICON_GAIN, icon=ICON_GAIN,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
key=CONF_NAME, key=CONF_NAME,

View File

@@ -5,7 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i
""" """
import logging import logging
from typing import TYPE_CHECKING, Any from typing import Any
from esphome import codegen as cg, config_validation as cv from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS from esphome.const import CONF_ITEMS
@@ -96,13 +96,9 @@ class LValidator:
return None return None
if isinstance(value, Lambda): if isinstance(value, Lambda):
# Local import to avoid circular import # Local import to avoid circular import
from .lvcode import CodeContext, LambdaContext from .lvcode import get_lambda_context_args
if TYPE_CHECKING: args = args or get_lambda_context_args()
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
return cg.RawExpression( return cg.RawExpression(
call_lambda( call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype) await cg.process_lambda(value, args, return_type=self.rtype)

View File

@@ -1,5 +1,5 @@
import re import re
from typing import TYPE_CHECKING, Any from typing import Any
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import image from esphome.components import image
@@ -404,14 +404,9 @@ class TextValidator(LValidator):
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression: ) -> Expression:
# Local import to avoid circular import at module level # Local import to avoid circular import at module level
from .lvcode import get_lambda_context_args
from .lvcode import CodeContext, LambdaContext args = args or get_lambda_context_args()
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
if isinstance(value, dict): if isinstance(value, dict):
if format_str := value.get(CONF_FORMAT): if format_str := value.get(CONF_FORMAT):

View File

@@ -1,4 +1,5 @@
import abc import abc
from typing import TYPE_CHECKING
from esphome import codegen as cg from esphome import codegen as cg
from esphome.config import Config from esphome.config import Config
@@ -200,6 +201,21 @@ class LvContext(LambdaContext):
return self.add(*args) return self.add(*args)
def get_lambda_context_args() -> list[tuple[SafeExpType, str]]:
"""Get automation parameters from the current lambda context if available.
When called from outside LVGL's context (e.g., from interval),
CodeContext.code_context will be None, so return empty args.
"""
if CodeContext.code_context is None:
return []
if TYPE_CHECKING:
# CodeContext base class doesn't define get_automation_parameters(),
# but LambdaContext and LvContext (the concrete implementations) do.
assert isinstance(CodeContext.code_context, LambdaContext)
return CodeContext.code_context.get_automation_parameters()
class LocalVariable(MockObj): class LocalVariable(MockObj):
""" """
Create a local variable and enclose the code using it within a block. Create a local variable and enclose the code using it within a block.

View File

@@ -85,11 +85,11 @@ class ArcType(NumberType):
lv.arc_set_range(w.obj, min_value, max_value) lv.arc_set_range(w.obj, min_value, max_value)
await w.set_property( await w.set_property(
CONF_START_ANGLE, "bg_start_angle",
await lv_angle_degrees.process(config.get(CONF_START_ANGLE)), await lv_angle_degrees.process(config.get(CONF_START_ANGLE)),
) )
await w.set_property( await w.set_property(
CONF_END_ANGLE, await lv_angle_degrees.process(config.get(CONF_END_ANGLE)) "bg_end_angle", await lv_angle_degrees.process(config.get(CONF_END_ANGLE))
) )
await w.set_property( await w.set_property(
CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION)) CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION))

View File

@@ -176,17 +176,22 @@ async def register_packet_transport(var, config):
if encryption := provider.get(CONF_ENCRYPTION): if encryption := provider.get(CONF_ENCRYPTION):
cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption)))
is_provider = False
for sens_conf in config.get(CONF_SENSORS, ()): for sens_conf in config.get(CONF_SENSORS, ()):
is_provider = True
sens_id = sens_conf[CONF_ID] sens_id = sens_conf[CONF_ID]
sensor = await cg.get_variable(sens_id) sensor = await cg.get_variable(sens_id)
bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id)
cg.add(var.add_sensor(bcst_id, sensor)) cg.add(var.add_sensor(bcst_id, sensor))
for sens_conf in config.get(CONF_BINARY_SENSORS, ()): for sens_conf in config.get(CONF_BINARY_SENSORS, ()):
is_provider = True
sens_id = sens_conf[CONF_ID] sens_id = sens_conf[CONF_ID]
sensor = await cg.get_variable(sens_id) sensor = await cg.get_variable(sens_id)
bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id)
cg.add(var.add_binary_sensor(bcst_id, sensor)) 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): if encryption := config.get(CONF_ENCRYPTION):
cg.add(var.set_encryption_key(hash_encryption_key(encryption))) cg.add(var.set_encryption_key(hash_encryption_key(encryption)))
return providers return providers

View File

@@ -263,12 +263,13 @@ void PacketTransport::flush_() {
xxtea::encrypt((uint32_t *) (encode_buffer.data() + header_len), len / 4, xxtea::encrypt((uint32_t *) (encode_buffer.data() + header_len), len / 4,
(uint32_t *) this->encryption_key_.data()); (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); this->send_packet(encode_buffer);
} }
void PacketTransport::add_binary_data_(uint8_t key, const char *id, bool data) { void PacketTransport::add_binary_data_(uint8_t key, const char *id, bool data) {
auto len = 1 + 1 + 1 + strlen(id); auto len = 1 + 1 + 1 + strlen(id);
if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) { if (round4(this->header_.size()) + round4(this->data_.size() + len) > this->get_max_packet_size()) {
this->flush_(); this->flush_();
this->init_data_(); this->init_data_();
} }
@@ -283,7 +284,7 @@ void PacketTransport::add_data_(uint8_t key, const char *id, float data) {
void PacketTransport::add_data_(uint8_t key, const char *id, uint32_t data) { void PacketTransport::add_data_(uint8_t key, const char *id, uint32_t data) {
auto len = 4 + 1 + 1 + strlen(id); auto len = 4 + 1 + 1 + strlen(id);
if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) { if (round4(this->header_.size()) + round4(this->data_.size() + len) > this->get_max_packet_size()) {
this->flush_(); this->flush_();
this->init_data_(); this->init_data_();
} }
@@ -316,6 +317,9 @@ void PacketTransport::send_data_(bool all) {
} }
void PacketTransport::update() { void PacketTransport::update() {
// resend all sensors if required
if (this->is_provider_)
this->send_data_(true);
if (!this->ping_pong_enable_) { if (!this->ping_pong_enable_) {
return; return;
} }
@@ -551,7 +555,7 @@ void PacketTransport::loop() {
if (this->resend_ping_key_) if (this->resend_ping_key_)
this->send_ping_pong_request_(); this->send_ping_pong_request_();
if (this->updated_) { if (this->updated_) {
this->send_data_(this->resend_data_); this->send_data_(false);
} }
} }

View File

@@ -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_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_rolling_code_enable(bool enable) { this->rolling_code_enable_ = enable; }
void set_ping_pong_enable(bool enable) { this->ping_pong_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 ping_pong_recyle_time_{};
uint32_t last_key_time_{}; uint32_t last_key_time_{};
bool resend_ping_key_{}; bool resend_ping_key_{};
bool resend_data_{}; bool is_provider_{};
const char *name_{}; const char *name_{};
ESPPreferenceObject pref_{}; ESPPreferenceObject pref_{};

View File

@@ -38,7 +38,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(PCA9685Output), cv.GenerateID(): cv.declare_id(PCA9685Output),
cv.Optional(CONF_FREQUENCY): cv.All( cv.Optional(CONF_FREQUENCY): cv.All(
cv.frequency, cv.Range(min=23.84, max=1525.88) cv.frequency, cv.float_range(min=23.84, max=1525.88)
), ),
cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean, cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean,
cv.Optional(CONF_PHASE_BALANCER, default="linear"): cv.enum( cv.Optional(CONF_PHASE_BALANCER, default="linear"): cv.enum(

View File

@@ -7,10 +7,10 @@ from esphome.const import (
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
DEVICE_CLASS_PM25, DEVICE_CLASS_PM25,
ICON_BLUR, ICON_BLUR,
SCHEDULER_DONT_RUN,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_MICROGRAMS_PER_CUBIC_METER,
) )
from esphome.core import TimePeriodMilliseconds
CODEOWNERS = ["@habbie"] CODEOWNERS = ["@habbie"]
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@@ -41,16 +41,12 @@ CONFIG_SCHEMA = cv.All(
def validate_interval_uart(config): def validate_interval_uart(config):
require_tx = False
interval = config.get(CONF_UPDATE_INTERVAL) 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( 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) )(config)

View File

@@ -232,10 +232,10 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
data.set_message_id(this->message_id_.value(x...)); data.set_message_id(this->message_id_.value(x...));
data.auto_message_id = this->auto_message_id_.value(x...); data.auto_message_id = this->auto_message_id_.value(x...);
std::vector<uint8_t> data_vec; std::vector<uint8_t> data_vec;
if (this->len_ >= 0) { if (this->len_ > 0) {
// Static mode: copy from flash to vector // Static mode: copy from flash to vector
data_vec.assign(this->data_.data, this->data_.data + this->len_); data_vec.assign(this->data_.data, this->data_.data + this->len_);
} else { } else if (this->len_ < 0) {
// Template mode: call function // Template mode: call function
data_vec = this->data_.func(x...); data_vec = this->data_.func(x...);
} }
@@ -245,7 +245,7 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
} }
protected: 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 { union Data {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas) std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash const uint8_t *data; // Pointer to static data in flash

View File

@@ -40,13 +40,10 @@ void RemoteTransmitterComponent::await_target_time_() {
if (this->target_time_ == 0) { if (this->target_time_ == 0) {
this->target_time_ = current_time; this->target_time_ = current_time;
} else if ((int32_t) (this->target_time_ - current_time) > 0) { } else if ((int32_t) (this->target_time_ - current_time) > 0) {
#if defined(USE_LIBRETINY) || defined(USE_RP2040) // busy loop is required as interrupts are disabled and delayMicroseconds()
// busy loop is required for libretiny and rp2040 as interrupts are disabled // may not work correctly in interrupt-disabled contexts on all platforms
while ((int32_t) (this->target_time_ - micros()) > 0) while ((int32_t) (this->target_time_ - micros()) > 0)
; ;
#else
delayMicroseconds(this->target_time_ - current_time);
#endif
} }
} }

View File

@@ -11,7 +11,7 @@ DEPENDENCIES = ["rp2040"]
rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm") rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm")
RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component) RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component)
SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action) SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{ {

View File

@@ -64,15 +64,21 @@ void MR24HPC1Component::dump_config() {
void MR24HPC1Component::setup() { void MR24HPC1Component::setup() {
this->check_uart_settings(115200); this->check_uart_settings(115200);
#ifdef USE_NUMBER
if (this->custom_mode_number_ != nullptr) { if (this->custom_mode_number_ != nullptr) {
this->custom_mode_number_->publish_state(0); // Zero out the custom mode this->custom_mode_number_->publish_state(0); // Zero out the custom mode
} }
#endif
#ifdef USE_SENSOR
if (this->custom_mode_num_sensor_ != nullptr) { if (this->custom_mode_num_sensor_ != nullptr) {
this->custom_mode_num_sensor_->publish_state(0); this->custom_mode_num_sensor_->publish_state(0);
} }
#endif
#ifdef USE_TEXT_SENSOR
if (this->custom_mode_end_text_sensor_ != nullptr) { if (this->custom_mode_end_text_sensor_ != nullptr) {
this->custom_mode_end_text_sensor_->publish_state("Not in custom mode"); this->custom_mode_end_text_sensor_->publish_state("Not in custom mode");
} }
#endif
this->set_custom_end_mode(); this->set_custom_end_mode();
this->poll_time_base_func_check_ = true; this->poll_time_base_func_check_ = true;
this->check_dev_inf_sign_ = true; this->check_dev_inf_sign_ = true;
@@ -353,6 +359,7 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
// Parses data frames related to product information // Parses data frames related to product information
void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) { void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) {
#ifdef USE_TEXT_SENSOR
uint16_t product_len = encode_uint16(data[FRAME_COMMAND_WORD_INDEX + 1], data[FRAME_COMMAND_WORD_INDEX + 2]); uint16_t product_len = encode_uint16(data[FRAME_COMMAND_WORD_INDEX + 1], data[FRAME_COMMAND_WORD_INDEX + 2]);
if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_PRODUCT_MODE) { if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_PRODUCT_MODE) {
if ((this->product_model_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) { if ((this->product_model_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) {
@@ -388,109 +395,153 @@ void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) {
ESP_LOGD(TAG, "Reply: get firmwareVersion error!"); ESP_LOGD(TAG, "Reply: get firmwareVersion error!");
} }
} }
#endif
} }
// Parsing the underlying open parameters // Parsing the underlying open parameters
void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *data) { void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *data) {
if (data[FRAME_COMMAND_WORD_INDEX] == 0x00) { switch (data[FRAME_COMMAND_WORD_INDEX]) {
if (this->underlying_open_function_switch_ != nullptr) { case 0x00:
this->underlying_open_function_switch_->publish_state( case 0x80:
data[FRAME_DATA_INDEX]); // Underlying Open Parameter Switch Status Updates #ifdef USE_SWITCH
} if (this->underlying_open_function_switch_ != nullptr) {
if (data[FRAME_DATA_INDEX]) { this->underlying_open_function_switch_->publish_state(data[FRAME_DATA_INDEX]);
this->s_output_info_switch_flag_ = OUTPUT_SWITCH_ON; }
} else { #endif
this->s_output_info_switch_flag_ = OUTPUT_SWTICH_OFF; this->s_output_info_switch_flag_ = data[FRAME_DATA_INDEX] ? OUTPUT_SWITCH_ON : OUTPUT_SWTICH_OFF;
} break;
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x01) { #ifdef USE_SENSOR
if (this->custom_spatial_static_value_sensor_ != nullptr) { case 0x01:
this->custom_spatial_static_value_sensor_->publish_state(data[FRAME_DATA_INDEX]); if (this->custom_spatial_static_value_sensor_ != nullptr) {
} this->custom_spatial_static_value_sensor_->publish_state(data[FRAME_DATA_INDEX]);
if (this->custom_presence_of_detection_sensor_ != nullptr) { }
this->custom_presence_of_detection_sensor_->publish_state(data[FRAME_DATA_INDEX + 1] * 0.5f); if (this->custom_presence_of_detection_sensor_ != nullptr) {
} this->custom_presence_of_detection_sensor_->publish_state(data[FRAME_DATA_INDEX + 1] * 0.5f);
if (this->custom_spatial_motion_value_sensor_ != nullptr) { }
this->custom_spatial_motion_value_sensor_->publish_state(data[FRAME_DATA_INDEX + 2]); if (this->custom_spatial_motion_value_sensor_ != nullptr) {
} this->custom_spatial_motion_value_sensor_->publish_state(data[FRAME_DATA_INDEX + 2]);
if (this->custom_motion_distance_sensor_ != nullptr) { }
this->custom_motion_distance_sensor_->publish_state(data[FRAME_DATA_INDEX + 3] * 0.5f); if (this->custom_motion_distance_sensor_ != nullptr) {
} this->custom_motion_distance_sensor_->publish_state(data[FRAME_DATA_INDEX + 3] * 0.5f);
if (this->custom_motion_speed_sensor_ != nullptr) { }
this->custom_motion_speed_sensor_->publish_state((data[FRAME_DATA_INDEX + 4] - 10) * 0.5f); if (this->custom_motion_speed_sensor_ != nullptr) {
} this->custom_motion_speed_sensor_->publish_state((data[FRAME_DATA_INDEX + 4] - 10) * 0.5f);
} else if ((data[FRAME_COMMAND_WORD_INDEX] == 0x06) || (data[FRAME_COMMAND_WORD_INDEX] == 0x86)) { }
// none:0x00 close_to:0x01 far_away:0x02 break;
if ((this->keep_away_text_sensor_ != nullptr) && (data[FRAME_DATA_INDEX] < 3)) { case 0x07:
this->keep_away_text_sensor_->publish_state(S_KEEP_AWAY_STR[data[FRAME_DATA_INDEX]]); case 0x87:
} if (this->movement_signs_sensor_ != nullptr) {
} else if ((this->movement_signs_sensor_ != nullptr) && this->movement_signs_sensor_->publish_state(data[FRAME_DATA_INDEX]);
((data[FRAME_COMMAND_WORD_INDEX] == 0x07) || (data[FRAME_COMMAND_WORD_INDEX] == 0x87))) { }
this->movement_signs_sensor_->publish_state(data[FRAME_DATA_INDEX]); break;
} else if ((this->existence_threshold_number_ != nullptr) && case 0x81:
((data[FRAME_COMMAND_WORD_INDEX] == 0x08) || (data[FRAME_COMMAND_WORD_INDEX] == 0x88))) { if (this->custom_spatial_static_value_sensor_ != nullptr) {
this->existence_threshold_number_->publish_state(data[FRAME_DATA_INDEX]); this->custom_spatial_static_value_sensor_->publish_state(data[FRAME_DATA_INDEX]);
} else if ((this->motion_threshold_number_ != nullptr) && }
((data[FRAME_COMMAND_WORD_INDEX] == 0x09) || (data[FRAME_COMMAND_WORD_INDEX] == 0x89))) { break;
this->motion_threshold_number_->publish_state(data[FRAME_DATA_INDEX]); case 0x82:
} else if ((this->existence_boundary_select_ != nullptr) && if (this->custom_spatial_motion_value_sensor_ != nullptr) {
((data[FRAME_COMMAND_WORD_INDEX] == 0x0a) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8a))) { this->custom_spatial_motion_value_sensor_->publish_state(data[FRAME_DATA_INDEX]);
if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { }
this->existence_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); break;
} case 0x83:
} else if ((this->motion_boundary_select_ != nullptr) && if (this->custom_presence_of_detection_sensor_ != nullptr) {
((data[FRAME_COMMAND_WORD_INDEX] == 0x0b) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8b))) { this->custom_presence_of_detection_sensor_->publish_state(
if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { S_PRESENCE_OF_DETECTION_RANGE_STR[data[FRAME_DATA_INDEX]]);
this->motion_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); }
} break;
} else if ((this->motion_trigger_number_ != nullptr) && case 0x84:
((data[FRAME_COMMAND_WORD_INDEX] == 0x0c) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8c))) { if (this->custom_motion_distance_sensor_ != nullptr) {
uint32_t motion_trigger_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1], this->custom_motion_distance_sensor_->publish_state(data[FRAME_DATA_INDEX] * 0.5f);
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]); }
this->motion_trigger_number_->publish_state(motion_trigger_time); break;
} else if ((this->motion_to_rest_number_ != nullptr) && case 0x85:
((data[FRAME_COMMAND_WORD_INDEX] == 0x0d) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8d))) { if (this->custom_motion_speed_sensor_ != nullptr) {
uint32_t move_to_rest_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1], this->custom_motion_speed_sensor_->publish_state((data[FRAME_DATA_INDEX] - 10) * 0.5f);
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]); }
this->motion_to_rest_number_->publish_state(move_to_rest_time); break;
} else if ((this->custom_unman_time_number_ != nullptr) && #endif
((data[FRAME_COMMAND_WORD_INDEX] == 0x0e) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8e))) { #ifdef USE_TEXT_SENSOR
uint32_t enter_unmanned_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1], case 0x06:
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]); case 0x86:
float custom_unmanned_time = enter_unmanned_time / 1000.0; // none:0x00 close_to:0x01 far_away:0x02
this->custom_unman_time_number_->publish_state(custom_unmanned_time); if ((this->keep_away_text_sensor_ != nullptr) && (data[FRAME_DATA_INDEX] < 3)) {
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x80) { this->keep_away_text_sensor_->publish_state(S_KEEP_AWAY_STR[data[FRAME_DATA_INDEX]]);
if (data[FRAME_DATA_INDEX]) { }
this->s_output_info_switch_flag_ = OUTPUT_SWITCH_ON; break;
} else { #endif
this->s_output_info_switch_flag_ = OUTPUT_SWTICH_OFF; #ifdef USE_NUMBER
} case 0x08:
if (this->underlying_open_function_switch_ != nullptr) { case 0x88:
this->underlying_open_function_switch_->publish_state(data[FRAME_DATA_INDEX]); if (this->existence_threshold_number_ != nullptr) {
} this->existence_threshold_number_->publish_state(data[FRAME_DATA_INDEX]);
} else if ((this->custom_spatial_static_value_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x81)) { }
this->custom_spatial_static_value_sensor_->publish_state(data[FRAME_DATA_INDEX]); break;
} else if ((this->custom_spatial_motion_value_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x82)) { case 0x09:
this->custom_spatial_motion_value_sensor_->publish_state(data[FRAME_DATA_INDEX]); case 0x89:
} else if ((this->custom_presence_of_detection_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x83)) { if (this->motion_threshold_number_ != nullptr) {
this->custom_presence_of_detection_sensor_->publish_state( this->motion_threshold_number_->publish_state(data[FRAME_DATA_INDEX]);
S_PRESENCE_OF_DETECTION_RANGE_STR[data[FRAME_DATA_INDEX]]); }
} else if ((this->custom_motion_distance_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x84)) { break;
this->custom_motion_distance_sensor_->publish_state(data[FRAME_DATA_INDEX] * 0.5f); case 0x0c:
} else if ((this->custom_motion_speed_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x85)) { case 0x8c:
this->custom_motion_speed_sensor_->publish_state((data[FRAME_DATA_INDEX] - 10) * 0.5f); if (this->motion_trigger_number_ != nullptr) {
uint32_t motion_trigger_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1],
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]);
this->motion_trigger_number_->publish_state(motion_trigger_time);
}
break;
case 0x0d:
case 0x8d:
if (this->motion_to_rest_number_ != nullptr) {
uint32_t move_to_rest_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1],
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]);
this->motion_to_rest_number_->publish_state(move_to_rest_time);
}
break;
case 0x0e:
case 0x8e:
if (this->custom_unman_time_number_ != nullptr) {
uint32_t enter_unmanned_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1],
data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]);
this->custom_unman_time_number_->publish_state(enter_unmanned_time / 1000.0f);
}
break;
#endif
#ifdef USE_SELECT
case 0x0a:
case 0x8a:
if (this->existence_boundary_select_ != nullptr) {
if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) {
this->existence_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1);
}
}
break;
case 0x0b:
case 0x8b:
if (this->motion_boundary_select_ != nullptr) {
if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) {
this->motion_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1);
}
}
break;
#endif
} }
} }
void MR24HPC1Component::r24_parse_data_frame_(uint8_t *data, uint8_t len) { void MR24HPC1Component::r24_parse_data_frame_(uint8_t *data, uint8_t len) {
switch (data[FRAME_CONTROL_WORD_INDEX]) { switch (data[FRAME_CONTROL_WORD_INDEX]) {
case 0x01: { case 0x01: {
if ((this->heartbeat_state_text_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x01)) { if (data[FRAME_COMMAND_WORD_INDEX] == 0x02) {
this->heartbeat_state_text_sensor_->publish_state("Equipment Normal");
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x02) {
ESP_LOGD(TAG, "Reply: query restart packet"); ESP_LOGD(TAG, "Reply: query restart packet");
} else if (this->heartbeat_state_text_sensor_ != nullptr) { break;
this->heartbeat_state_text_sensor_->publish_state("Equipment Abnormal");
} }
#ifdef USE_TEXT_SENSOR
if (this->heartbeat_state_text_sensor_ != nullptr) {
this->heartbeat_state_text_sensor_->publish_state(
data[FRAME_COMMAND_WORD_INDEX] == 0x01 ? "Equipment Normal" : "Equipment Abnormal");
}
#endif
} break; } break;
case 0x02: { case 0x02: {
this->r24_frame_parse_product_information_(data); this->r24_frame_parse_product_information_(data);
@@ -511,86 +562,123 @@ void MR24HPC1Component::r24_parse_data_frame_(uint8_t *data, uint8_t len) {
} }
void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) {
if (data[FRAME_COMMAND_WORD_INDEX] == 0x01) { switch (data[FRAME_COMMAND_WORD_INDEX]) {
ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); case 0x01:
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x07) { case 0x81:
if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]);
this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); break;
} else { case 0x09:
ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); #ifdef USE_SENSOR
} if (this->custom_mode_num_sensor_ != nullptr) {
} else if ((this->sensitivity_number_ != nullptr) && this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]);
((data[FRAME_COMMAND_WORD_INDEX] == 0x08) || (data[FRAME_COMMAND_WORD_INDEX] == 0x88))) {
// 1-3
this->sensitivity_number_->publish_state(data[FRAME_DATA_INDEX]);
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x09) {
// 1-4
if (this->custom_mode_num_sensor_ != nullptr) {
this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]);
}
if (this->custom_mode_number_ != nullptr) {
this->custom_mode_number_->publish_state(0);
}
if (this->custom_mode_end_text_sensor_ != nullptr) {
this->custom_mode_end_text_sensor_->publish_state("Setup in progress");
}
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x81) {
ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]);
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x87) {
if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) {
this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]);
} else {
ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]);
}
} else if ((this->custom_mode_end_text_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x0A)) {
this->custom_mode_end_text_sensor_->publish_state("Set Success!");
} else if (data[FRAME_COMMAND_WORD_INDEX] == 0x89) {
if (data[FRAME_DATA_INDEX] == 0) {
if (this->custom_mode_end_text_sensor_ != nullptr) {
this->custom_mode_end_text_sensor_->publish_state("Not in custom mode");
} }
#endif
#ifdef USE_NUMBER
if (this->custom_mode_number_ != nullptr) { if (this->custom_mode_number_ != nullptr) {
this->custom_mode_number_->publish_state(0); this->custom_mode_number_->publish_state(0);
} }
#endif
#ifdef USE_TEXT_SENSOR
if (this->custom_mode_end_text_sensor_ != nullptr) {
this->custom_mode_end_text_sensor_->publish_state("Setup in progress");
}
#endif
break;
case 0x89:
#ifdef USE_SENSOR
if (this->custom_mode_num_sensor_ != nullptr) { if (this->custom_mode_num_sensor_ != nullptr) {
this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]); this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]);
} }
} else { #endif
if (this->custom_mode_num_sensor_ != nullptr) { if (data[FRAME_DATA_INDEX] == 0) {
this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]); #ifdef USE_TEXT_SENSOR
if (this->custom_mode_end_text_sensor_ != nullptr) {
this->custom_mode_end_text_sensor_->publish_state("Not in custom mode");
}
#endif
#ifdef USE_NUMBER
if (this->custom_mode_number_ != nullptr) {
this->custom_mode_number_->publish_state(0);
}
#endif
} }
} break;
} else { #ifdef USE_SELECT
ESP_LOGD(TAG, "[%s] No found COMMAND_WORD(%02X) in Frame", __FUNCTION__, data[FRAME_COMMAND_WORD_INDEX]); case 0x07:
case 0x87:
if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) {
this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]);
} else {
ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]);
}
break;
#endif
#ifdef USE_NUMBER
case 0x08:
case 0x88:
if (this->sensitivity_number_ != nullptr) {
this->sensitivity_number_->publish_state(data[FRAME_DATA_INDEX]);
}
break;
#endif
#ifdef USE_TEXT_SENSOR
case 0x0A:
if (this->custom_mode_end_text_sensor_ != nullptr) {
this->custom_mode_end_text_sensor_->publish_state("Set Success!");
}
break;
#endif
default:
ESP_LOGD(TAG, "[%s] No found COMMAND_WORD(%02X) in Frame", __FUNCTION__, data[FRAME_COMMAND_WORD_INDEX]);
break;
} }
} }
void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) {
if ((this->has_target_binary_sensor_ != nullptr) && switch (data[FRAME_COMMAND_WORD_INDEX]) {
((data[FRAME_COMMAND_WORD_INDEX] == 0x01) || (data[FRAME_COMMAND_WORD_INDEX] == 0x81))) { #ifdef USE_BINARY_SENSOR
this->has_target_binary_sensor_->publish_state(S_SOMEONE_EXISTS_STR[data[FRAME_DATA_INDEX]]); case 0x01:
} else if ((this->motion_status_text_sensor_ != nullptr) && case 0x81:
((data[FRAME_COMMAND_WORD_INDEX] == 0x02) || (data[FRAME_COMMAND_WORD_INDEX] == 0x82))) { if (this->has_target_binary_sensor_ != nullptr) {
if (data[FRAME_DATA_INDEX] < 3) { this->has_target_binary_sensor_->publish_state(S_SOMEONE_EXISTS_STR[data[FRAME_DATA_INDEX]]);
this->motion_status_text_sensor_->publish_state(S_MOTION_STATUS_STR[data[FRAME_DATA_INDEX]]); }
} break;
} else if ((this->movement_signs_sensor_ != nullptr) && #endif
((data[FRAME_COMMAND_WORD_INDEX] == 0x03) || (data[FRAME_COMMAND_WORD_INDEX] == 0x83))) { #ifdef USE_SENSOR
this->movement_signs_sensor_->publish_state(data[FRAME_DATA_INDEX]); case 0x03:
} else if ((this->unman_time_select_ != nullptr) && case 0x83:
((data[FRAME_COMMAND_WORD_INDEX] == 0x0A) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8A))) { if (this->movement_signs_sensor_ != nullptr) {
// none:0x00 1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08 this->movement_signs_sensor_->publish_state(data[FRAME_DATA_INDEX]);
if (data[FRAME_DATA_INDEX] < 9) { }
this->unman_time_select_->publish_state(data[FRAME_DATA_INDEX]); break;
} #endif
} else if ((this->keep_away_text_sensor_ != nullptr) && #ifdef USE_TEXT_SENSOR
((data[FRAME_COMMAND_WORD_INDEX] == 0x0B) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8B))) { case 0x02:
// none:0x00 close_to:0x01 far_away:0x02 case 0x82:
if (data[FRAME_DATA_INDEX] < 3) { if ((this->motion_status_text_sensor_ != nullptr) && (data[FRAME_DATA_INDEX] < 3)) {
this->keep_away_text_sensor_->publish_state(S_KEEP_AWAY_STR[data[FRAME_DATA_INDEX]]); this->motion_status_text_sensor_->publish_state(S_MOTION_STATUS_STR[data[FRAME_DATA_INDEX]]);
} }
} else { break;
ESP_LOGD(TAG, "[%s] No found COMMAND_WORD(%02X) in Frame", __FUNCTION__, data[FRAME_COMMAND_WORD_INDEX]); case 0x0B:
case 0x8B:
// none:0x00 close_to:0x01 far_away:0x02
if ((this->keep_away_text_sensor_ != nullptr) && (data[FRAME_DATA_INDEX] < 3)) {
this->keep_away_text_sensor_->publish_state(S_KEEP_AWAY_STR[data[FRAME_DATA_INDEX]]);
}
break;
#endif
#ifdef USE_SELECT
case 0x0A:
case 0x8A:
// none:0x00 1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08
if ((this->unman_time_select_ != nullptr) && (data[FRAME_DATA_INDEX] < 9)) {
this->unman_time_select_->publish_state(data[FRAME_DATA_INDEX]);
}
break;
#endif
default:
ESP_LOGD(TAG, "[%s] No found COMMAND_WORD(%02X) in Frame", __FUNCTION__, data[FRAME_COMMAND_WORD_INDEX]);
break;
} }
} }
@@ -695,12 +783,15 @@ void MR24HPC1Component::set_underlying_open_function(bool enable) {
} else { } else {
this->send_query_(UNDERLYING_SWITCH_OFF, sizeof(UNDERLYING_SWITCH_OFF)); this->send_query_(UNDERLYING_SWITCH_OFF, sizeof(UNDERLYING_SWITCH_OFF));
} }
#ifdef USE_TEXT_SENSOR
if (this->keep_away_text_sensor_ != nullptr) { if (this->keep_away_text_sensor_ != nullptr) {
this->keep_away_text_sensor_->publish_state(""); this->keep_away_text_sensor_->publish_state("");
} }
if (this->motion_status_text_sensor_ != nullptr) { if (this->motion_status_text_sensor_ != nullptr) {
this->motion_status_text_sensor_->publish_state(""); this->motion_status_text_sensor_->publish_state("");
} }
#endif
#ifdef USE_SENSOR
if (this->custom_spatial_static_value_sensor_ != nullptr) { if (this->custom_spatial_static_value_sensor_ != nullptr) {
this->custom_spatial_static_value_sensor_->publish_state(NAN); this->custom_spatial_static_value_sensor_->publish_state(NAN);
} }
@@ -716,6 +807,7 @@ void MR24HPC1Component::set_underlying_open_function(bool enable) {
if (this->custom_motion_speed_sensor_ != nullptr) { if (this->custom_motion_speed_sensor_ != nullptr) {
this->custom_motion_speed_sensor_->publish_state(NAN); this->custom_motion_speed_sensor_->publish_state(NAN);
} }
#endif
} }
void MR24HPC1Component::set_scene_mode(uint8_t value) { void MR24HPC1Component::set_scene_mode(uint8_t value) {
@@ -723,12 +815,16 @@ void MR24HPC1Component::set_scene_mode(uint8_t value) {
uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x07, 0x00, 0x01, value, 0x00, 0x54, 0x43}; uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x07, 0x00, 0x01, value, 0x00, 0x54, 0x43};
send_data[7] = get_frame_crc_sum(send_data, send_data_len); send_data[7] = get_frame_crc_sum(send_data, send_data_len);
this->send_query_(send_data, send_data_len); this->send_query_(send_data, send_data_len);
#ifdef USE_NUMBER
if (this->custom_mode_number_ != nullptr) { if (this->custom_mode_number_ != nullptr) {
this->custom_mode_number_->publish_state(0); this->custom_mode_number_->publish_state(0);
} }
#endif
#ifdef USE_SENSOR
if (this->custom_mode_num_sensor_ != nullptr) { if (this->custom_mode_num_sensor_ != nullptr) {
this->custom_mode_num_sensor_->publish_state(0); this->custom_mode_num_sensor_->publish_state(0);
} }
#endif
this->get_scene_mode(); this->get_scene_mode();
this->get_sensitivity(); this->get_sensitivity();
this->get_custom_mode(); this->get_custom_mode();
@@ -768,9 +864,11 @@ void MR24HPC1Component::set_unman_time(uint8_t value) {
void MR24HPC1Component::set_custom_mode(uint8_t mode) { void MR24HPC1Component::set_custom_mode(uint8_t mode) {
if (mode == 0) { if (mode == 0) {
this->set_custom_end_mode(); // Equivalent to end setting this->set_custom_end_mode(); // Equivalent to end setting
#ifdef USE_NUMBER
if (this->custom_mode_number_ != nullptr) { if (this->custom_mode_number_ != nullptr) {
this->custom_mode_number_->publish_state(0); this->custom_mode_number_->publish_state(0);
} }
#endif
return; return;
} }
uint8_t send_data_len = 10; uint8_t send_data_len = 10;
@@ -793,9 +891,11 @@ void MR24HPC1Component::set_custom_end_mode() {
uint8_t send_data_len = 10; uint8_t send_data_len = 10;
uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x0a, 0x00, 0x01, 0x0F, 0xCB, 0x54, 0x43}; uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x0a, 0x00, 0x01, 0x0F, 0xCB, 0x54, 0x43};
this->send_query_(send_data, send_data_len); this->send_query_(send_data, send_data_len);
#ifdef USE_NUMBER
if (this->custom_mode_number_ != nullptr) { if (this->custom_mode_number_ != nullptr) {
this->custom_mode_number_->publish_state(0); // Clear setpoints this->custom_mode_number_->publish_state(0); // Clear setpoints
} }
#endif
this->get_existence_boundary(); this->get_existence_boundary();
this->get_motion_boundary(); this->get_motion_boundary();
this->get_existence_threshold(); this->get_existence_threshold();
@@ -809,8 +909,10 @@ void MR24HPC1Component::set_custom_end_mode() {
} }
void MR24HPC1Component::set_existence_boundary(uint8_t value) { void MR24HPC1Component::set_existence_boundary(uint8_t value) {
#ifdef USE_SENSOR
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
return; // You'll have to check that you're in custom mode to set it up return; // You'll have to check that you're in custom mode to set it up
#endif
uint8_t send_data_len = 10; uint8_t send_data_len = 10;
uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x0A, 0x00, 0x01, (uint8_t) (value + 1), 0x00, 0x54, 0x43}; uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x0A, 0x00, 0x01, (uint8_t) (value + 1), 0x00, 0x54, 0x43};
send_data[7] = get_frame_crc_sum(send_data, send_data_len); send_data[7] = get_frame_crc_sum(send_data, send_data_len);
@@ -819,8 +921,10 @@ void MR24HPC1Component::set_existence_boundary(uint8_t value) {
} }
void MR24HPC1Component::set_motion_boundary(uint8_t value) { void MR24HPC1Component::set_motion_boundary(uint8_t value) {
#ifdef USE_SENSOR
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
return; // You'll have to check that you're in custom mode to set it up return; // You'll have to check that you're in custom mode to set it up
#endif
uint8_t send_data_len = 10; uint8_t send_data_len = 10;
uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x0B, 0x00, 0x01, (uint8_t) (value + 1), 0x00, 0x54, 0x43}; uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x0B, 0x00, 0x01, (uint8_t) (value + 1), 0x00, 0x54, 0x43};
send_data[7] = get_frame_crc_sum(send_data, send_data_len); send_data[7] = get_frame_crc_sum(send_data, send_data_len);
@@ -829,8 +933,10 @@ void MR24HPC1Component::set_motion_boundary(uint8_t value) {
} }
void MR24HPC1Component::set_existence_threshold(uint8_t value) { void MR24HPC1Component::set_existence_threshold(uint8_t value) {
#ifdef USE_SENSOR
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
return; // You'll have to check that you're in custom mode to set it up return; // You'll have to check that you're in custom mode to set it up
#endif
uint8_t send_data_len = 10; uint8_t send_data_len = 10;
uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x08, 0x00, 0x01, value, 0x00, 0x54, 0x43}; uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x08, 0x00, 0x01, value, 0x00, 0x54, 0x43};
send_data[7] = get_frame_crc_sum(send_data, send_data_len); send_data[7] = get_frame_crc_sum(send_data, send_data_len);
@@ -839,8 +945,10 @@ void MR24HPC1Component::set_existence_threshold(uint8_t value) {
} }
void MR24HPC1Component::set_motion_threshold(uint8_t value) { void MR24HPC1Component::set_motion_threshold(uint8_t value) {
#ifdef USE_SENSOR
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
return; // You'll have to check that you're in custom mode to set it up return; // You'll have to check that you're in custom mode to set it up
#endif
uint8_t send_data_len = 10; uint8_t send_data_len = 10;
uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x09, 0x00, 0x01, value, 0x00, 0x54, 0x43}; uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x09, 0x00, 0x01, value, 0x00, 0x54, 0x43};
send_data[7] = get_frame_crc_sum(send_data, send_data_len); send_data[7] = get_frame_crc_sum(send_data, send_data_len);
@@ -849,8 +957,10 @@ void MR24HPC1Component::set_motion_threshold(uint8_t value) {
} }
void MR24HPC1Component::set_motion_trigger_time(uint8_t value) { void MR24HPC1Component::set_motion_trigger_time(uint8_t value) {
#ifdef USE_SENSOR
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
return; // You'll have to check that you're in custom mode to set it up return; // You'll have to check that you're in custom mode to set it up
#endif
uint8_t send_data_len = 13; uint8_t send_data_len = 13;
uint8_t send_data[13] = {0x53, 0x59, 0x08, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, value, 0x00, 0x54, 0x43}; uint8_t send_data[13] = {0x53, 0x59, 0x08, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, value, 0x00, 0x54, 0x43};
send_data[10] = get_frame_crc_sum(send_data, send_data_len); send_data[10] = get_frame_crc_sum(send_data, send_data_len);
@@ -859,8 +969,10 @@ void MR24HPC1Component::set_motion_trigger_time(uint8_t value) {
} }
void MR24HPC1Component::set_motion_to_rest_time(uint16_t value) { void MR24HPC1Component::set_motion_to_rest_time(uint16_t value) {
#ifdef USE_SENSOR
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
return; // You'll have to check that you're in custom mode to set it up return; // You'll have to check that you're in custom mode to set it up
#endif
uint8_t h8_num = (value >> 8) & 0xff; uint8_t h8_num = (value >> 8) & 0xff;
uint8_t l8_num = value & 0xff; uint8_t l8_num = value & 0xff;
uint8_t send_data_len = 13; uint8_t send_data_len = 13;
@@ -871,8 +983,10 @@ void MR24HPC1Component::set_motion_to_rest_time(uint16_t value) {
} }
void MR24HPC1Component::set_custom_unman_time(uint16_t value) { void MR24HPC1Component::set_custom_unman_time(uint16_t value) {
#ifdef USE_SENSOR
if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0))
return; // You'll have to check that you're in custom mode to set it up return; // You'll have to check that you're in custom mode to set it up
#endif
uint32_t value_ms = value * 1000; uint32_t value_ms = value * 1000;
uint8_t h24_num = (value_ms >> 24) & 0xff; uint8_t h24_num = (value_ms >> 24) & 0xff;
uint8_t h16_num = (value_ms >> 16) & 0xff; uint8_t h16_num = (value_ms >> 16) & 0xff;

View File

@@ -70,7 +70,7 @@ void SN74HC595GPIOComponent::write_gpio() {
void SN74HC595SPIComponent::write_gpio() { void SN74HC595SPIComponent::write_gpio() {
for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) {
this->enable(); this->enable();
this->transfer_byte(output_byte); this->write_byte(output_byte);
this->disable(); this->disable();
} }
SN74HC595Component::write_gpio(); SN74HC595Component::write_gpio();

View File

@@ -47,6 +47,8 @@ def require_wake_loop_threadsafe() -> None:
This enables the shared UDP loopback socket mechanism (~208 bytes RAM). This enables the shared UDP loopback socket mechanism (~208 bytes RAM).
The socket is shared across all components that use this feature. The socket is shared across all components that use this feature.
This call is a no-op if networking is not enabled in the configuration.
IMPORTANT: This is for background thread context only, NOT ISR context. IMPORTANT: This is for background thread context only, NOT ISR context.
Socket operations are not safe to call from ISR handlers. Socket operations are not safe to call from ISR handlers.
@@ -56,8 +58,11 @@ def require_wake_loop_threadsafe() -> None:
async def to_code(config): async def to_code(config):
socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe()
""" """
# Only set up once (idempotent - multiple components can call this) # Only set up once (idempotent - multiple components can call this)
if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): if CORE.has_networking and not CORE.data.get(
KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False
):
CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
cg.add_define("USE_WAKE_LOOP_THREADSAFE") cg.add_define("USE_WAKE_LOOP_THREADSAFE")
# Consume 1 socket for the shared wake notification socket # Consume 1 socket for the shared wake notification socket

View File

@@ -188,7 +188,7 @@ class LWIPRawImpl : public Socket {
errno = EINVAL; errno = EINVAL;
return -1; 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 { std::string getpeername() override {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {

View File

@@ -1,6 +1,7 @@
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import spi from esphome.components import spi
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import ID, TimePeriod from esphome.core import ID, TimePeriod
@@ -14,7 +15,6 @@ CONF_SX126X_ID = "sx126x_id"
CONF_BANDWIDTH = "bandwidth" CONF_BANDWIDTH = "bandwidth"
CONF_BITRATE = "bitrate" CONF_BITRATE = "bitrate"
CONF_CODING_RATE = "coding_rate" CONF_CODING_RATE = "coding_rate"
CONF_CRC_ENABLE = "crc_enable"
CONF_CRC_INVERTED = "crc_inverted" CONF_CRC_INVERTED = "crc_inverted"
CONF_CRC_SIZE = "crc_size" CONF_CRC_SIZE = "crc_size"
CONF_CRC_POLYNOMIAL = "crc_polynomial" CONF_CRC_POLYNOMIAL = "crc_polynomial"
@@ -23,7 +23,6 @@ CONF_DEVIATION = "deviation"
CONF_DIO1_PIN = "dio1_pin" CONF_DIO1_PIN = "dio1_pin"
CONF_HW_VERSION = "hw_version" CONF_HW_VERSION = "hw_version"
CONF_MODULATION = "modulation" CONF_MODULATION = "modulation"
CONF_ON_PACKET = "on_packet"
CONF_PA_POWER = "pa_power" CONF_PA_POWER = "pa_power"
CONF_PA_RAMP = "pa_ramp" CONF_PA_RAMP = "pa_ramp"
CONF_PAYLOAD_LENGTH = "payload_length" CONF_PAYLOAD_LENGTH = "payload_length"
@@ -200,9 +199,13 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All( cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All(
cv.hex_int, cv.Range(min=0, max=0xFFFF) cv.hex_int, cv.Range(min=0, max=0xFFFF)
), ),
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_HW_VERSION): cv.one_of( cv.Required(CONF_HW_VERSION): cv.one_of(
"sx1261", "sx1262", "sx1268", "llcc68", lower=True "sx1261", "sx1262", "sx1268", "llcc68", lower=True
), ),

View File

@@ -12,12 +12,6 @@ void SX126xTransport::setup() {
this->parent_->register_listener(this); 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::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); } void SX126xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); }

View File

@@ -11,7 +11,6 @@ namespace sx126x {
class SX126xTransport : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener { class SX126xTransport : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener {
public: public:
void setup() override; void setup() override;
void update() override;
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) 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; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }

View File

@@ -1,6 +1,7 @@
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import spi from esphome.components import spi
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import ID from esphome.core import ID
@@ -16,11 +17,9 @@ CONF_BANDWIDTH = "bandwidth"
CONF_BITRATE = "bitrate" CONF_BITRATE = "bitrate"
CONF_BITSYNC = "bitsync" CONF_BITSYNC = "bitsync"
CONF_CODING_RATE = "coding_rate" CONF_CODING_RATE = "coding_rate"
CONF_CRC_ENABLE = "crc_enable"
CONF_DEVIATION = "deviation" CONF_DEVIATION = "deviation"
CONF_DIO0_PIN = "dio0_pin" CONF_DIO0_PIN = "dio0_pin"
CONF_MODULATION = "modulation" CONF_MODULATION = "modulation"
CONF_ON_PACKET = "on_packet"
CONF_PA_PIN = "pa_pin" CONF_PA_PIN = "pa_pin"
CONF_PA_POWER = "pa_power" CONF_PA_POWER = "pa_power"
CONF_PA_RAMP = "pa_ramp" CONF_PA_RAMP = "pa_ramp"
@@ -197,9 +196,13 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_BITSYNC): cv.boolean, cv.Optional(CONF_BITSYNC): cv.boolean,
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_MODULATION): cv.enum(MOD), cv.Required(CONF_MODULATION): cv.enum(MOD),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN), cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN),

View File

@@ -12,12 +12,6 @@ void SX127xTransport::setup() {
this->parent_->register_listener(this); 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::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); } void SX127xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); }

View File

@@ -11,7 +11,6 @@ namespace sx127x {
class SX127xTransport : public packet_transport::PacketTransport, public Parented<SX127x>, public SX127xListener { class SX127xTransport : public packet_transport::PacketTransport, public Parented<SX127x>, public SX127xListener {
public: public:
void setup() override; void setup() override;
void update() override;
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) 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; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }

View File

@@ -34,7 +34,15 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level]; severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level];
} }
int pri = this->facility_ * 8 + severity; int pri = this->facility_ * 8 + severity;
auto timestamp = this->time_->now().strftime("%b %e %H:%M:%S"); auto now = this->time_->now();
std::string timestamp;
if (now.is_valid()) {
timestamp = now.strftime("%b %e %H:%M:%S");
} else {
// RFC 5424: A syslog application MUST use the NILVALUE as TIMESTAMP if the syslog application is incapable of
// obtaining system time.
timestamp = "-";
}
size_t len = message_len; size_t len = message_len;
// remove color formatting // remove color formatting
if (this->strip_ && message[0] == 0x1B && len > 11) { if (this->strip_ && message[0] == 0x1B && len > 11) {

View File

@@ -39,6 +39,7 @@ enum TemplateAlarmControlPanelRestoreMode {
ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
}; };
#ifdef USE_BINARY_SENSOR
struct SensorDataStore { struct SensorDataStore {
bool last_chime_state; bool last_chime_state;
}; };
@@ -49,7 +50,6 @@ struct SensorInfo {
uint8_t store_index; uint8_t store_index;
}; };
#ifdef USE_BINARY_SENSOR
struct AlarmSensor { struct AlarmSensor {
binary_sensor::BinarySensor *sensor; binary_sensor::BinarySensor *sensor;
SensorInfo info; SensorInfo info;
@@ -139,6 +139,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
FixedVector<AlarmSensor> sensors_; FixedVector<AlarmSensor> sensors_;
// a list of automatically bypassed sensors // a list of automatically bypassed sensors
std::vector<uint8_t> bypassed_sensor_indicies_; std::vector<uint8_t> bypassed_sensor_indicies_;
// Per sensor data store
std::vector<SensorDataStore> sensor_data_;
uint8_t next_store_index_ = 0;
#endif #endif
TemplateAlarmControlPanelRestoreMode restore_mode_{}; TemplateAlarmControlPanelRestoreMode restore_mode_{};
@@ -154,14 +157,11 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
uint32_t trigger_time_; uint32_t trigger_time_;
// a list of codes // a list of codes
std::vector<std::string> codes_; std::vector<std::string> codes_;
// Per sensor data store
std::vector<SensorDataStore> sensor_data_;
// requires a code to arm // requires a code to arm
bool requires_code_to_arm_ = false; bool requires_code_to_arm_ = false;
bool supports_arm_home_ = false; bool supports_arm_home_ = false;
bool supports_arm_night_ = false; bool supports_arm_night_ = false;
bool sensors_ready_ = false; bool sensors_ready_ = false;
uint8_t next_store_index_ = 0;
// check if the code is valid // check if the code is valid
bool is_code_valid_(optional<std::string> code); bool is_code_valid_(optional<std::string> code);

View File

@@ -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. * 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. * @param byte The byte to write.

View File

@@ -23,7 +23,6 @@ static const uint8_t CONTROL_BYTE = 0x7D;
class UARTTransport : public packet_transport::PacketTransport, public UARTDevice { class UARTTransport : public packet_transport::PacketTransport, public UARTDevice {
public: public:
void loop() override; void loop() override;
void update() override;
float get_setup_priority() const override { return setup_priority::PROCESSOR; } float get_setup_priority() const override { return setup_priority::PROCESSOR; }
protected: protected:

View File

@@ -9,6 +9,7 @@
#include "esphome/core/gpio.h" #include "esphome/core/gpio.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include "soc/gpio_num.h" #include "soc/gpio_num.h"
#include "soc/uart_pins.h"
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
@@ -139,6 +140,22 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return; 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) { auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) { if (!pin) {
return; return;
@@ -154,10 +171,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
setup_pin_if_needed(this->tx_pin_); 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; uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV; invert |= UART_SIGNAL_TXD_INV;

View File

@@ -8,29 +8,14 @@ namespace udp {
static const char *const TAG = "udp_transport"; 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() { void UDPTransport::setup() {
PacketTransport::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_()) { if (!this->providers_.empty() || this->is_encrypted_()) {
this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); }); 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); } void UDPTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->send_packet(buf); }
} // namespace udp } // namespace udp
} // namespace esphome } // namespace esphome

View File

@@ -12,14 +12,12 @@ namespace udp {
class UDPTransport : public packet_transport::PacketTransport, public Parented<UDPComponent> { class UDPTransport : public packet_transport::PacketTransport, public Parented<UDPComponent> {
public: public:
void setup() override; void setup() override;
void update() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
protected: protected:
void send_packet(const std::vector<uint8_t> &buf) const override; void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override; bool should_send() override;
bool should_broadcast_{false};
size_t get_max_packet_size() override { return MAX_PACKET_SIZE; } size_t get_max_packet_size() override { return MAX_PACKET_SIZE; }
}; };

View File

@@ -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() { void AsyncWebServer::begin() {
if (this->server_) { if (this->server_) {
this->end(); this->end();
@@ -136,8 +124,11 @@ void AsyncWebServer::begin() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = this->port_; config.server_port = this->port_;
config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; 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) // Always enable LRU purging to handle socket exhaustion gracefully.
config.lru_purge_enable = this->lru_purge_enable_; // 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 // Use custom close function that shuts down before closing to prevent lwIP race conditions
config.close_fn = AsyncWebServer::safe_close_with_shutdown; config.close_fn = AsyncWebServer::safe_close_with_shutdown;
if (httpd_start(&this->server_, &config) == ESP_OK) { if (httpd_start(&this->server_, &config) == ESP_OK) {

View File

@@ -199,13 +199,11 @@ class AsyncWebServer {
return *handler; return *handler;
} }
void set_lru_purge_enable(bool enable);
httpd_handle_t get_server() { return this->server_; } httpd_handle_t get_server() { return this->server_; }
protected: protected:
uint16_t port_{}; uint16_t port_{};
httpd_handle_t server_{}; httpd_handle_t server_{};
bool lru_purge_enable_{false};
static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_handler(httpd_req_t *r);
static esp_err_t request_post_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r);
esp_err_t request_handler_(AsyncWebServerRequest *request) const; esp_err_t request_handler_(AsyncWebServerRequest *request) const;

View File

@@ -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 /// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown
static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; 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) { static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
switch (phase) { switch (phase) {
case WiFiRetryPhase::INITIAL_CONNECT: case WiFiRetryPhase::INITIAL_CONNECT:
@@ -1035,7 +1050,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
void WiFiComponent::check_scanning_finished() { void WiFiComponent::check_scanning_finished() {
if (!this->scan_done_) { if (!this->scan_done_) {
if (millis() - this->action_started_ > 30000) { if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
ESP_LOGE(TAG, "Scan timeout"); ESP_LOGE(TAG, "Scan timeout");
this->retry_connect(); this->retry_connect();
} }
@@ -1184,8 +1199,9 @@ void WiFiComponent::check_connecting_finished() {
} }
uint32_t now = millis(); uint32_t now = millis();
if (now - this->action_started_ > 30000) { if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Connection timeout"); ESP_LOGW(TAG, "Connection timeout, aborting connection attempt");
this->wifi_disconnect_();
this->retry_connect(); this->retry_connect();
return; return;
} }
@@ -1405,6 +1421,10 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
// without disrupting the captive portal/improv connection // without disrupting the captive portal/improv connection
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
this->restart_adapter(); 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 // Clear scan flag - we're starting a new retry cycle
this->did_scan_this_cycle_ = false; this->did_scan_this_cycle_ = false;

View File

@@ -528,6 +528,16 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
for (auto *listener : global_wifi_component->connect_state_listeners_) { for (auto *listener : global_wifi_component->connect_state_listeners_) {
listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid()); listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid());
} }
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = global_wifi_component->get_selected_sta_();
config && config->get_manual_ip().has_value()) {
for (auto *listener : global_wifi_component->ip_state_listeners_) {
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(),
global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1));
}
}
#endif
#endif #endif
break; break;
} }

View File

@@ -483,6 +483,12 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
s_sta_connected = false; s_sta_connected = false;
s_sta_connect_error = false; s_sta_connect_error = false;
s_sta_connect_not_found = false; s_sta_connect_not_found = false;
// Reset IP address flags - ensures we don't report connected before DHCP completes
// (IP_EVENT_STA_LOST_IP doesn't always fire on disconnect)
this->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
this->num_ipv6_addresses_ = 0;
#endif
err = esp_wifi_connect(); err = esp_wifi_connect();
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -720,6 +726,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
ESP_LOGV(TAG, "STA stop"); ESP_LOGV(TAG, "STA stop");
s_sta_started = false; s_sta_started = false;
s_sta_connecting = false;
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
const auto &it = data->data.sta_authmode_change; const auto &it = data->data.sta_authmode_change;
@@ -738,6 +745,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
for (auto *listener : this->connect_state_listeners_) { for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
} }
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif #endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) {

View File

@@ -291,6 +291,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
} }
case ESPHOME_EVENT_ID_WIFI_STA_STOP: { case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
ESP_LOGV(TAG, "STA stop"); ESP_LOGV(TAG, "STA stop");
s_sta_connecting = false;
break; break;
} }
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
@@ -304,6 +305,14 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
for (auto *listener : this->connect_state_listeners_) { for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
} }
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif #endif
break; break;
} }
@@ -322,7 +331,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 // wifi_sta_connect_status_() to return IDLE. The main loop then sees
// "Unknown connection status 0" (wifi_component.cpp check_connecting_finished) // "Unknown connection status 0" (wifi_component.cpp check_connecting_finished)
// and calls retry_connect(), aborting a connection that may succeed moments later. // 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) { if (it.ssid_len == 0 && s_sta_connecting) {
ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)", ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)",
get_disconnect_reason_str(it.reason)); get_disconnect_reason_str(it.reason));
@@ -527,7 +536,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
#endif // USE_WIFI_AP #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 WiFiComponent::wifi_bssid() {
bssid_t bssid{}; bssid_t bssid{};

View File

@@ -259,6 +259,15 @@ void WiFiComponent::wifi_loop_() {
for (auto *listener : this->connect_state_listeners_) { for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
} }
// For static IP configurations, notify IP listeners immediately as the IP is already configured
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
s_sta_had_ip = true;
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif #endif
} else if (!is_connected && s_sta_was_connected) { } else if (!is_connected && s_sta_was_connected) {
// Just disconnected // Just disconnected

View File

@@ -16,7 +16,12 @@ class WiFiSignalSensor : public sensor::Sensor, public PollingComponent {
#ifdef USE_WIFI_LISTENERS #ifdef USE_WIFI_LISTENERS
void setup() override { wifi::global_wifi_component->add_connect_state_listener(this); } void setup() override { wifi::global_wifi_component->add_connect_state_listener(this); }
#endif #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; void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }

View File

@@ -71,17 +71,20 @@ void WTS01Sensor::process_packet_() {
} }
// Extract temperature value // Extract temperature value
int8_t temp = this->buffer_[6]; const uint8_t raw = this->buffer_[6];
int32_t sign = 1;
// Handle negative temperatures // WTS01 encodes sign in bit 7, magnitude in bits 0-6
if (temp < 0) { const bool negative = (raw & 0x80) != 0;
sign = -1; const uint8_t magnitude = raw & 0x7F;
const float decimal = static_cast<float>(this->buffer_[7]) / 100.0f;
float temperature = static_cast<float>(magnitude) + decimal;
if (negative) {
temperature = -temperature;
} }
// Calculate temperature (temp + decimal/100)
float temperature = static_cast<float>(temp) + (sign * static_cast<float>(this->buffer_[7]) / 100.0f);
ESP_LOGV(TAG, "Received new temperature: %.2f°C", temperature); ESP_LOGV(TAG, "Received new temperature: %.2f°C", temperature);
this->publish_state(temperature); this->publish_state(temperature);

View File

@@ -1010,14 +1010,14 @@ def validate_config(
result.add_error(err) result.add_error(err)
return result return result
CORE.raw_config = config
# 1.1. Merge packages # 1.1. Merge packages
if CONF_PACKAGES in config: if CONF_PACKAGES in config:
from esphome.components.packages import merge_packages from esphome.components.packages import merge_packages
config = merge_packages(config) config = merge_packages(config)
CORE.raw_config = config
# 1.2. Resolve !extend and !remove and check for REPLACEME # 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 # After this step, there will not be any Extend or Remove values in the config anymore
try: try:

View File

@@ -71,6 +71,7 @@ from esphome.const import (
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
SCHEDULER_DONT_RUN,
TYPE_GIT, TYPE_GIT,
TYPE_LOCAL, TYPE_LOCAL,
VALID_SUBSTITUTIONS_CHARACTERS, VALID_SUBSTITUTIONS_CHARACTERS,
@@ -894,7 +895,7 @@ def time_period_in_minutes_(value):
def update_interval(value): def update_interval(value):
if value == "never": if value == "never":
return 4294967295 # uint32_t max return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN)
return positive_time_period_milliseconds(value) return positive_time_period_milliseconds(value)
@@ -2009,7 +2010,7 @@ def polling_component_schema(default_update_interval):
if default_update_interval is None: if default_update_interval is None:
return COMPONENT_SCHEMA.extend( return COMPONENT_SCHEMA.extend(
{ {
Required(CONF_UPDATE_INTERVAL): default_update_interval, Required(CONF_UPDATE_INTERVAL): update_interval,
} }
) )
assert isinstance(default_update_interval, str) assert isinstance(default_update_interval, str)

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum from esphome.enum import StrEnum
__version__ = "2025.12.0b2" __version__ = "2025.12.7"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -703,6 +703,25 @@ class EsphomeCore:
def config_filename(self) -> str: def config_filename(self) -> str:
return self.config_path.name return self.config_path.name
def has_at_least_one_component(self, *components: str) -> bool:
"""
Are any of the given components configured?
:param components: component names
:return: true if so
"""
if self.config is None:
raise ValueError("Config has not been loaded yet")
return any(component in self.config for component in components)
@property
def has_networking(self) -> bool:
"""
Is a network component configured?
:return: true if so
"""
return self.has_at_least_one_component("wifi", "ethernet", "openthread")
def relative_config_path(self, *path: str | Path) -> Path: def relative_config_path(self, *path: str | Path) -> Path:
path_ = Path(*path).expanduser() path_ = Path(*path).expanduser()
return self.config_dir / path_ return self.config_dir / path_

View File

@@ -1,16 +1,16 @@
#pragma once #pragma once
// Platform-agnostic macros for PROGMEM string handling // 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 ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings
// On other platforms: Use plain strings (no PROGMEM)
#ifdef USE_ESP32 #ifdef USE_ESP8266
#define ESPHOME_F(string_literal) (string_literal) // ESP8266 uses Arduino macros
#define ESPHOME_PGM_P const char *
#define ESPHOME_strncpy_P strncpy
#else
// ESP8266 and other Arduino platforms use Arduino macros
#define ESPHOME_F(string_literal) F(string_literal) #define ESPHOME_F(string_literal) F(string_literal)
#define ESPHOME_PGM_P PGM_P #define ESPHOME_PGM_P PGM_P
#define ESPHOME_strncpy_P strncpy_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 #endif

View File

@@ -164,8 +164,24 @@ def websocket_method(name):
return wrap 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 @websocket_class
class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandler):
"""Base class for ESPHome websocket commands.""" """Base class for ESPHome websocket commands."""
def __init__( def __init__(
@@ -183,18 +199,6 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
# use Popen() with a reading thread instead # use Popen() with a reading thread instead
self._use_popen = os.name == "nt" 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: def open(self, *args: str, **kwargs: str) -> None:
"""Handle new WebSocket connection.""" """Handle new WebSocket connection."""
# Ensure messages from the subprocess are sent immediately # Ensure messages from the subprocess are sent immediately
@@ -601,7 +605,7 @@ DASHBOARD_SUBSCRIBER = DashboardSubscriber()
@websocket_class @websocket_class
class DashboardEventsWebSocket(tornado.websocket.WebSocketHandler): class DashboardEventsWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandler):
"""WebSocket handler for real-time dashboard events.""" """WebSocket handler for real-time dashboard events."""
_event_listeners: list[Callable[[], None]] | None = None _event_listeners: list[Callable[[], None]] | None = None

View File

@@ -322,8 +322,8 @@ def perform_ota(
hash_func, nonce_size, hash_name = _AUTH_METHODS[auth] hash_func, nonce_size, hash_name = _AUTH_METHODS[auth]
perform_auth(sock, password, hash_func, nonce_size, hash_name) perform_auth(sock, password, hash_func, nonce_size, hash_name)
# Set higher timeout during upload # Timeout must match device-side OTA_SOCKET_TIMEOUT_DATA to prevent premature failures
sock.settimeout(30.0) sock.settimeout(90.0)
upload_size = len(upload_contents) upload_size = len(upload_contents)
upload_size_encoded = [ upload_size_encoded = [

View File

@@ -27,3 +27,7 @@ dependencies:
version: "1.7.6~1" version: "1.7.6~1"
rules: rules:
- if: "target in [esp32s2, esp32s3, esp32p4]" - if: "target in [esp32s2, esp32s3, esp32p4]"
esphome/esp-hub75:
version: 0.1.7
rules:
- if: "target in [esp32, esp32s2, esp32s3, esp32p4]"

View File

@@ -99,14 +99,11 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool: def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
if ( # ESP32 uses CMake for both Arduino and ESP-IDF frameworks
return (
old.loaded_integrations != new.loaded_integrations old.loaded_integrations != new.loaded_integrations
or old.loaded_platforms != new.loaded_platforms or old.loaded_platforms != new.loaded_platforms
) and new.core_platform == PLATFORM_ESP32: ) and new.core_platform == PLATFORM_ESP32
from esphome.components.esp32 import FRAMEWORK_ESP_IDF
return new.framework == FRAMEWORK_ESP_IDF
return False
def update_storage_json() -> None: def update_storage_json() -> None:

View File

@@ -156,7 +156,6 @@ lib_deps =
esphome/ESP32-audioI2S@2.3.0 ; i2s_audio esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
droscy/esp_wireguard@0.4.2 ; wireguard droscy/esp_wireguard@0.4.2 ; wireguard
esphome/esp-audio-libs@2.0.1 ; audio esphome/esp-audio-libs@2.0.1 ; audio
esphome/esp-hub75@0.1.6 ; hub75
build_flags = build_flags =
${common:arduino.build_flags} ${common:arduino.build_flags}
@@ -180,7 +179,6 @@ lib_deps =
droscy/esp_wireguard@0.4.2 ; wireguard droscy/esp_wireguard@0.4.2 ; wireguard
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
esphome/esp-audio-libs@2.0.1 ; audio esphome/esp-audio-libs@2.0.1 ; audio
esphome/esp-hub75@0.1.6 ; hub75
build_flags = build_flags =
${common:idf.build_flags} ${common:idf.build_flags}
-Wno-nonnull-compare -Wno-nonnull-compare

View File

@@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages 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 import resolve_extend_remove
from esphome.config_helpers import Extend, Remove from esphome.config_helpers import Extend, Remove
import esphome.config_validation as cv import esphome.config_validation as cv
@@ -33,6 +34,7 @@ from esphome.const import (
CONF_VARS, CONF_VARS,
CONF_WIFI, CONF_WIFI,
) )
from esphome.core import CORE
from esphome.util import OrderedDict from esphome.util import OrderedDict
# Test strings # Test strings
@@ -991,3 +993,35 @@ def test_package_merge_invalid(invalid_package) -> None:
with pytest.raises(cv.Invalid): with pytest.raises(cv.Invalid):
merge_packages(config) 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

View File

@@ -1,13 +1,26 @@
cc1101: cc1101:
id: transceiver id: transceiver
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
gdo0_pin: ${gdo0_pin}
frequency: 433.92MHz frequency: 433.92MHz
if_frequency: 153kHz if_frequency: 153kHz
filter_bandwidth: 203kHz filter_bandwidth: 203kHz
channel: 0 channel: 0
channel_spacing: 200kHz channel_spacing: 200kHz
symbol_rate: 5000 symbol_rate: 4800
modulation_type: ASK/OOK 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: button:
- platform: template - platform: template
@@ -18,3 +31,7 @@ button:
- cc1101.begin_rx: transceiver - cc1101.begin_rx: transceiver
- cc1101.set_idle: transceiver - cc1101.set_idle: transceiver
- cc1101.reset: 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};

View File

@@ -1,8 +1,8 @@
substitutions: substitutions:
cs_pin: GPIO5 cs_pin: GPIO5
gdo0_pin: GPIO4
packages: packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml 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 <<: !include common.yaml

View File

@@ -1,8 +1,8 @@
substitutions: substitutions:
cs_pin: GPIO5 cs_pin: GPIO5
gdo0_pin: GPIO4
packages: packages:
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml 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 <<: !include common.yaml

View File

@@ -62,7 +62,7 @@ packet_transport:
sensors: sensors:
- temp_sensor - temp_sensor
providers: providers:
- name: test_provider - name: test-provider
encryption: encryption:
key: "0123456789abcdef0123456789abcdef" key: "0123456789abcdef0123456789abcdef"
@@ -71,6 +71,6 @@ sensor:
id: temp_sensor id: temp_sensor
- platform: packet_transport - platform: packet_transport
provider: test_provider provider: test-provider
remote_id: temp_sensor remote_id: temp_sensor
id: remote_temp id: remote_temp

View File

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

View File

@@ -4,6 +4,7 @@ from esphome.core import CORE
def test_require_wake_loop_threadsafe__first_call() -> None: def test_require_wake_loop_threadsafe__first_call() -> None:
"""Test that first call sets up define and consumes socket.""" """Test that first call sets up define and consumes socket."""
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe()
# Verify CORE.data was updated # Verify CORE.data was updated
@@ -17,6 +18,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None:
"""Test that subsequent calls are idempotent.""" """Test that subsequent calls are idempotent."""
# Set up initial state as if already called # Set up initial state as if already called
CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
CORE.config = {"ethernet": True}
# Call again - should not raise or fail # Call again - should not raise or fail
socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe()
@@ -31,6 +33,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None:
def test_require_wake_loop_threadsafe__multiple_calls() -> None: def test_require_wake_loop_threadsafe__multiple_calls() -> None:
"""Test that multiple calls only set up once.""" """Test that multiple calls only set up once."""
# Call three times # Call three times
CORE.config = {"openthread": True}
socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe()
socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe()
socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe()
@@ -40,3 +43,35 @@ def test_require_wake_loop_threadsafe__multiple_calls() -> None:
# Verify the define was added (only once, but we can just check it exists) # Verify the define was added (only once, but we can just check it exists)
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
def test_require_wake_loop_threadsafe__no_networking() -> None:
"""Test that wake loop is NOT configured when no networking is configured."""
# Set up config without any networking components
CORE.config = {"esphome": {"name": "test"}, "logger": {}}
# Call require_wake_loop_threadsafe
socket.require_wake_loop_threadsafe()
# Verify CORE.data flag was NOT set (since has_networking returns False)
assert socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED not in CORE.data
# Verify the define was NOT added
assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -> None:
"""Test that no socket is consumed when no networking is configured."""
# Set up config without any networking components
CORE.config = {"logger": {}}
# Track initial socket consumer state
initial_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {})
# Call require_wake_loop_threadsafe
socket.require_wake_loop_threadsafe()
# Verify no socket was consumed
consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {})
assert "socket.wake_loop_threadsafe" not in consumers
assert consumers == initial_consumers

View File

@@ -1567,3 +1567,90 @@ async def test_dashboard_yaml_loading_with_packages_and_secrets(
# If we get here, secret resolution worked! # If we get here, secret resolution worked!
assert "esphome" in config assert "esphome" in config
assert config["esphome"]["name"] == "test-download-secrets" 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()

View File

@@ -718,3 +718,65 @@ class TestEsphomeCore:
# Even though "web_server" is in loaded_integrations due to the platform, # Even though "web_server" is in loaded_integrations due to the platform,
# web_port must return None because the full web_server component is not configured # web_port must return None because the full web_server component is not configured
assert target.web_port is None assert target.web_port is None
def test_has_at_least_one_component__none_configured(self, target):
"""Test has_at_least_one_component returns False when none of the components are configured."""
target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}}
assert target.has_at_least_one_component("wifi", "ethernet") is False
def test_has_at_least_one_component__one_configured(self, target):
"""Test has_at_least_one_component returns True when one component is configured."""
target.config = {const.CONF_WIFI: {}, "logger": {}}
assert target.has_at_least_one_component("wifi", "ethernet") is True
def test_has_at_least_one_component__multiple_configured(self, target):
"""Test has_at_least_one_component returns True when multiple components are configured."""
target.config = {
const.CONF_WIFI: {},
const.CONF_ETHERNET: {},
"logger": {},
}
assert (
target.has_at_least_one_component("wifi", "ethernet", "bluetooth") is True
)
def test_has_at_least_one_component__single_component(self, target):
"""Test has_at_least_one_component works with a single component."""
target.config = {const.CONF_MQTT: {}}
assert target.has_at_least_one_component("mqtt") is True
assert target.has_at_least_one_component("wifi") is False
def test_has_at_least_one_component__config_not_loaded(self, target):
"""Test has_at_least_one_component raises ValueError when config is not loaded."""
target.config = None
with pytest.raises(ValueError, match="Config has not been loaded yet"):
target.has_at_least_one_component("wifi")
def test_has_networking__with_wifi(self, target):
"""Test has_networking returns True when wifi is configured."""
target.config = {const.CONF_WIFI: {}}
assert target.has_networking is True
def test_has_networking__with_ethernet(self, target):
"""Test has_networking returns True when ethernet is configured."""
target.config = {const.CONF_ETHERNET: {}}
assert target.has_networking is True
def test_has_networking__with_openthread(self, target):
"""Test has_networking returns True when openthread is configured."""
target.config = {const.CONF_OPENTHREAD: {}}
assert target.has_networking is True
def test_has_networking__without_networking(self, target):
"""Test has_networking returns False when no networking component is configured."""
target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}}
assert target.has_networking is False

View File

@@ -9,6 +9,13 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
)
from esphome.core import EsphomeError from esphome.core import EsphomeError
from esphome.storage_json import StorageJSON from esphome.storage_json import StorageJSON
from esphome.writer import ( from esphome.writer import (
@@ -21,6 +28,7 @@ from esphome.writer import (
clean_build, clean_build,
clean_cmake_cache, clean_cmake_cache,
storage_should_clean, storage_should_clean,
storage_should_update_cmake_cache,
update_storage_json, update_storage_json,
write_cpp, write_cpp,
write_gitignore, write_gitignore,
@@ -164,6 +172,86 @@ def test_storage_edge_case_from_empty_integrations(
assert storage_should_clean(old, new) is False assert storage_should_clean(old, new) is False
# Tests for storage_should_update_cmake_cache
@pytest.mark.parametrize("framework", ["arduino", "esp-idf"])
def test_storage_should_update_cmake_cache_when_integration_added_esp32(
create_storage: Callable[..., StorageJSON],
framework: str,
) -> None:
"""Test cmake cache update triggered when integration added on ESP32."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework=framework,
)
new = create_storage(
loaded_integrations=["api", "wifi", "restart"],
core_platform=PLATFORM_ESP32,
framework=framework,
)
assert storage_should_update_cmake_cache(old, new) is True
def test_storage_should_update_cmake_cache_when_platform_changed_esp32(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test cmake cache update triggered when platforms change on ESP32."""
old = create_storage(
loaded_integrations=["api", "wifi"],
loaded_platforms={"sensor"},
core_platform=PLATFORM_ESP32,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi"],
loaded_platforms={"sensor", "binary_sensor"},
core_platform=PLATFORM_ESP32,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is True
def test_storage_should_not_update_cmake_cache_when_nothing_changes(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test cmake cache not updated when nothing changes."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is False
@pytest.mark.parametrize(
"core_platform",
[PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX],
)
def test_storage_should_not_update_cmake_cache_for_non_esp32(
create_storage: Callable[..., StorageJSON],
core_platform: str,
) -> None:
"""Test cmake cache not updated for non-ESP32 platforms."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=core_platform,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi", "restart"],
core_platform=core_platform,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is False
@patch("esphome.writer.clean_build") @patch("esphome.writer.clean_build")
@patch("esphome.writer.StorageJSON") @patch("esphome.writer.StorageJSON")
@patch("esphome.writer.storage_path") @patch("esphome.writer.storage_path")