Compare commits

..

7 Commits

Author SHA1 Message Date
J. Nick Koston 0fbc4e85be Address review: reference WDT_FEED_INTERVAL_MS in slow-path comment 2026-04-14 08:13:06 -10:00
J. Nick Koston 3af7e9a0db Merge remote-tracking branch 'upstream/dev' into pr-15656
# Conflicts:
#	esphome/core/application.cpp
#	esphome/core/application.h
2026-04-14 08:12:21 -10:00
J. Nick Koston 5926ca5369 [core] Split feed_wdt into hot and cold entries
Separate Application::feed_wdt() into two entry points so the hot path
callers stop paying for the time==0 check they never trigger:

- feed_wdt_with_time(time): inline, hot path. Rate-limit check in 3
  Xtensa instructions (load + sub + branch). [[unlikely]] tells the
  compiler the slow branch is rare so the common path stays
  fall-through.
- feed_wdt(): cold, out of line. Fetches millis() and forwards through
  the same rate limit. Used by setup loops, upload helpers, yield(),
  and any other non-hot caller.

feed_wdt_slow_() is now pure worker code — 11 bytes. It just calls
arch_feed_wdt(), updates last_wdt_feed_, and runs the status LED
re-dispatch. Both entries have already confirmed the rate limit was
exceeded before calling.

Hot call sites updated:
- Application::loop() per-component feed
- Scheduler::execute_item_() after each scheduled item runs
- Application::teardown_components() inner loop (already has 'now')
2026-04-11 15:19:35 -10:00
J. Nick Koston dc5626eb85 [core] Invert feed_wdt condition so slow-path call is inside the if
Cleaner than the early-return form — the action (calling feed_wdt_slow_)
reads as the body of the conditional instead of falling through past a
guard clause. Logically identical and compiles to the same code.
2026-04-11 14:57:01 -10:00
J. Nick Koston 715f0ca6f7 [core] Extract WDT_FEED_INTERVAL_MS constexpr
Replace the magic 3 in both the inline feed_wdt check and the slow path
with a named constexpr so the rate-limit threshold is defined once.
2026-04-11 14:54:02 -10:00
J. Nick Koston a70ec9ec06 [scheduler] Feed watchdog after each scheduled item, drop top-of-loop feed
The main loop used to feed the watchdog unconditionally right after
Scheduler::call() returned, regardless of whether the scheduler had any
actual work to do. On an idle device this meant every outer loop
iteration paid the inline rate-limit check (load + sub + branch) for no
benefit.

Move the feed into Scheduler::execute_item_() so it fires only after a
scheduled callback actually runs, and covers both the main heap path
and the defer queue path (both go through execute_item_). This also
bounds the max feed gap during a burst of back-to-back scheduled items
by max(item_runtime) instead of sum(item_runtime).

The top-of-loop feed in Application::before_loop_tasks_() is now
unnecessary — when Scheduler::call does no work, the only elapsed time
is the sleep wake plus a few instructions, and when it does have work,
it fed the wdt as it went.
2026-04-11 14:51:59 -10:00
J. Nick Koston ddbf6f2347 [core] Inline feed_wdt hot path with out-of-line slow path
Split Application::feed_wdt() into an ALWAYS_INLINE wrapper that checks
the 3ms rate limit against last_wdt_feed_ and a feed_wdt_slow_() callee
that performs the actual arch_feed_wdt() + status LED re-dispatch.

Callers on the hot path (loop_task before/after each component) that
already have a millis() timestamp in hand now pay only a load + sub +
branch on the no-op path instead of a full call8 / entry / retw.

Moves the rate-limit state from a function-local static to a class
member (last_wdt_feed_) so the inline can access it.
2026-04-11 14:32:49 -10:00
126 changed files with 1437 additions and 3226 deletions
+1 -1
View File
@@ -1 +1 @@
075ed2142432dc59883bb52db8ac11270f952851d6400deae080f5468c7cb592
dc8ad5472d9fb44ce1ca29a0601afd65705642799a2819704dfc8459fbaf9815
+1 -1
View File
@@ -12,7 +12,7 @@
"--privileged",
"-e",
"GIT_EDITOR=code --wait"
// uncomment and edit the path in order to pass through local USB serial to the container
// uncomment and edit the path in order to pass though local USB serial to the conatiner
// , "--device=/dev/ttyACM0"
],
"appPort": 6052,
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -8,4 +8,4 @@ on:
jobs:
lock:
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
uses: esphome/workflows/.github/workflows/lock.yml@main
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.11
rev: v0.15.10
hooks:
# Run the linter.
- id: ruff
-1
View File
@@ -569,7 +569,6 @@ def wrap_to_code(name, comp):
@functools.wraps(comp.to_code)
async def wrapped(conf):
cg.add(cg.ComponentMarker(name))
cg.add(cg.LineComment(f"{name}:"))
if comp.config_schema is not None:
conf_str = yaml_util.dump(conf)
+4 -3
View File
@@ -199,10 +199,11 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
return cv.Schema([schema])(value)
except cv.Invalid as err2:
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
raise err from None
# pylint: disable=raise-missing-from
raise err
if "Unable to find action" in str(err):
raise err2 from None
raise cv.MultipleInvalid([err, err2]) from None
raise err2
raise cv.MultipleInvalid([err, err2])
elif isinstance(value, dict):
if CONF_THEN in value:
return [schema(value)]
+6 -72
View File
@@ -151,8 +151,8 @@ class ConfigBundleCreator:
def __init__(self, config: dict[str, Any]) -> None:
self._config = config
self._config_dir = Path(CORE.config_dir).resolve()
self._config_path = Path(CORE.config_path).resolve()
self._config_dir = CORE.config_dir
self._config_path = CORE.config_path
self._files: list[BundleFile] = []
self._seen_paths: set[Path] = set()
self._secrets_paths: set[Path] = set()
@@ -258,36 +258,21 @@ class ConfigBundleCreator:
def _discover_yaml_includes(self) -> None:
"""Discover YAML files loaded during config parsing.
Deliberately uses a fresh re-parse and force-loads every deferred
``IncludeFile`` to include *all* potentially-reachable includes,
even branches not selected by the local substitutions. Bundles are
meant to be compiled on another system where command-line
substitution overrides may choose a different branch — e.g.
``!include network/${eth_model}/config.yaml`` must ship every
candidate so the remote build can pick any one.
Entries with unresolved substitution variables in the filename
path are skipped with a warning (they cannot be resolved without
the substitution pass).
We track files by wrapping _load_yaml_internal. The config has already
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
re-load just to discover the file list.
Secrets files are tracked separately so we can filter them to
only include the keys this config actually references.
"""
# Must be a fresh parse: IncludeFile.load() caches its result in
# _content, and we discover files by listening for loader calls. On
# an already-parsed tree the cache is populated, .load() returns
# without calling the loader, the listener never fires, and the
# referenced files would be silently dropped from the bundle.
with yaml_util.track_yaml_loads() as loaded_files:
try:
data = yaml_util.load_yaml(self._config_path)
yaml_util.load_yaml(self._config_path)
except EsphomeError:
_LOGGER.debug(
"Bundle: re-loading YAML for include discovery failed, "
"proceeding with partial file list"
)
else:
_force_load_include_files(data)
for fpath in loaded_files:
if fpath == self._config_path.resolve():
@@ -623,57 +608,6 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
tar.addfile(info, io.BytesIO(data))
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
resolved during the substitution pass. During bundle discovery we need
the referenced files to actually load so the ``track_yaml_loads``
listener fires for them.
``IncludeFile`` instances with unresolved substitution variables in the
filename cannot be loaded — we skip and warn about those.
"""
if _seen is None:
_seen = set()
if isinstance(obj, yaml_util.IncludeFile):
if id(obj) in _seen:
return
_seen.add(id(obj))
if obj.has_unresolved_expressions():
_LOGGER.warning(
"Bundle: cannot resolve !include %s (referenced from %s) "
"with substitutions in path",
obj.file,
obj.parent_file,
)
return
try:
loaded = obj.load()
except EsphomeError as err:
_LOGGER.warning(
"Bundle: failed to load !include %s (referenced from %s): %s",
obj.file,
obj.parent_file,
err,
)
return
_force_load_include_files(loaded, _seen)
elif isinstance(obj, dict):
if id(obj) in _seen:
return
_seen.add(id(obj))
for value in obj.values():
_force_load_include_files(value, _seen)
elif isinstance(obj, (list, tuple)):
if id(obj) in _seen:
return
_seen.add(id(obj))
for item in obj:
_force_load_include_files(item, _seen)
def _resolve_include_path(include_path: Any) -> Path | None:
"""Resolve an include path to absolute, skipping system includes."""
if isinstance(include_path, str) and include_path.startswith("<"):
-2
View File
@@ -10,10 +10,8 @@
# pylint: disable=unused-import
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
ComponentMarker,
Expression,
FlashStringLiteral,
IIFEUnsafeStatement,
LineComment,
LogStringLiteral,
MockObj,
+2 -2
View File
@@ -83,7 +83,7 @@ def angle_to_position(value, min=-360, max=360):
value = angle(min=min, max=max)(value)
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
except cv.Invalid as e:
raise cv.Invalid(f"When using angle, {e.error_message}") from e
raise cv.Invalid(f"When using angle, {e.error_message}")
def percent_to_position(value):
@@ -164,7 +164,7 @@ def has_valid_range_config():
except cv.Invalid as e:
raise cv.Invalid(
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
) from e
)
return validator
+1 -1
View File
@@ -116,7 +116,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
) from e
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type == "wav":
+5 -12
View File
@@ -332,9 +332,8 @@ def parse_multi_click_timing_str(value):
try:
state = cv.boolean(parts[0])
except cv.Invalid:
raise cv.Invalid(
f"First word must either be ON or OFF, not {parts[0]}"
) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}")
if parts[1] != "for":
raise cv.Invalid(f"Second word must be 'for', got {parts[1]}")
@@ -351,9 +350,7 @@ def parse_multi_click_timing_str(value):
try:
length = cv.positive_time_period_milliseconds(parts[4])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}")
return {CONF_STATE: state, key: str(length)}
if parts[3] != "to":
@@ -362,16 +359,12 @@ def parse_multi_click_timing_str(value):
try:
min_length = cv.positive_time_period_milliseconds(parts[2])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing minimum length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
try:
max_length = cv.positive_time_period_milliseconds(parts[4])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing maximum length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
return {
CONF_STATE: state,
-5
View File
@@ -65,8 +65,3 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
+1 -1
View File
@@ -63,7 +63,7 @@ void BM8563::read_time() {
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+1 -3
View File
@@ -171,9 +171,7 @@ async def to_code_base(config):
with open(path, encoding="utf-8") as f:
bsec2_iaq_config = f.read()
except Exception as e:
raise core.EsphomeError(
f"Could not open binary configuration file {path}: {e}"
) from e
raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}")
# Convert retrieved BSEC2 config to an array of ints
rhs = [int(x) for x in bsec2_iaq_config.split(",")]
+1 -1
View File
@@ -44,7 +44,7 @@ void DS1307Component::read_time() {
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+1 -1
View File
@@ -1222,7 +1222,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
*ESP32_CHIP_REVISIONS, string=True
*ESP32_CHIP_REVISIONS
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
@@ -78,14 +78,6 @@ def ota_esphome_final_validate(config):
else:
new_ota_conf.append(ota_conf)
if len(merged_ota_esphome_configs_by_port) > 1:
raise cv.Invalid(
f"Only a single port is supported for '{CONF_OTA}' "
f"'{CONF_PLATFORM}: {CONF_ESPHOME}'. Got ports "
f"{sorted(merged_ota_esphome_configs_by_port.keys())}. Consolidate "
f"onto a single port; configs sharing a port are merged automatically."
)
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
full_conf[CONF_OTA] = new_ota_conf
@@ -155,8 +147,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_PASSWORD")
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
await cg.register_component(var, config)
await ota_to_code(var, config)
+6 -33
View File
@@ -15,9 +15,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#ifdef USE_LWIP_FAST_SELECT
#include "esphome/core/lwip_fast_select.h"
#endif
#include <cerrno>
#include <cstdio>
@@ -31,17 +28,6 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
// Single-instance pointer — multi-port configs are rejected in final_validate.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static ESPHomeOTAComponent *global_esphome_ota_component = nullptr;
// Called from any context (LwIP TCP/IP task, RP2040 user-IRQ).
extern "C" void esphome_wake_ota_component_any_context() {
if (global_esphome_ota_component != nullptr) {
global_esphome_ota_component->enable_loop_soon_any_context();
}
}
void ESPHomeOTAComponent::setup() {
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->server_ == nullptr) {
@@ -79,14 +65,6 @@ void ESPHomeOTAComponent::setup() {
this->server_failed_(LOG_STR("listen"));
return;
}
// loop() self-disables on its first idle tick; no explicit disable_loop() needed here.
global_esphome_ota_component = this;
#ifdef USE_LWIP_FAST_SELECT
// Filter fast-select wakes to this listener only. If the sock lookup returns nullptr,
// no wakes fire and loop() falls back to the self-disable safety net.
esphome_fast_select_set_ota_listener_sock(esphome_lwip_get_sock(this->server_->get_fd()));
#endif
}
void ESPHomeOTAComponent::dump_config() {
@@ -103,15 +81,13 @@ void ESPHomeOTAComponent::dump_config() {
}
void ESPHomeOTAComponent::loop() {
// Self-disabling idle loop. Runs when a wake path marks us pending-enable (fast-select
// listener filter, raw-TCP accept_fn_, or host select), finds no work, and goes back
// to sleep. cleanup_connection_() deliberately leaves the loop enabled for one more
// iteration so a connection queued mid-session is still caught here.
if (this->client_ == nullptr && !this->server_->ready()) {
this->disable_loop();
return;
// Skip handle_handshake_() call if no client connected and no incoming connections
// This optimization reduces idle loop overhead when OTA is not active
// Note: No need to check server_ for null as the component is marked failed in setup()
// if server_ creation fails
if (this->client_ != nullptr || this->server_->ready()) {
this->handle_handshake_();
}
this->handle_handshake_();
}
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
@@ -590,9 +566,6 @@ void ESPHomeOTAComponent::cleanup_connection_() {
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
#endif
// Intentionally no disable_loop() — letting loop() run one more iteration catches
// any connection that queued on the listener mid-session (otherwise the wake flag,
// set while we were in LOOP state, would be lost to enable_pending_loops_()).
}
void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
int reset_pin_{-1};
int phy_addr_spi_{-1};
int clock_speed_;
spi_host_device_t interface_{SPI2_HOST};
spi_host_device_t interface_{SPI3_HOST};
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
uint32_t polling_interval_{0};
#endif
+1 -1
View File
@@ -325,7 +325,7 @@ def download_gfont(value):
raise cv.Invalid(
f"Could not download font at {url}, please check the fonts exists "
f"at google fonts ({e})"
) from e
)
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
if match is None:
raise cv.Invalid(
@@ -60,73 +60,6 @@ CONFIG_SCHEMA = (
)
def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
"""Check if pin is shared exclusively with deep_sleep (wakeup pin)."""
pin_key = (CORE.target_platform, CORE.target_platform, pin_num)
pin_users = pins.PIN_SCHEMA_REGISTRY.pins_used.get(pin_key, [])
if len(pin_users) != 2:
return False
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
def _final_validate(config):
use_interrupt = config[CONF_USE_INTERRUPT]
if not use_interrupt:
return config
pin_num = config[CONF_PIN][CONF_NUMBER]
# Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt
# attachment — only internal/native GPIO pins do.
if pins.PIN_SCHEMA_REGISTRY.get_key(config[CONF_PIN]) != CORE.target_platform:
_LOGGER.info(
"GPIO binary_sensor '%s': Pin is not an internal GPIO, "
"falling back to polling mode.",
config.get(CONF_NAME, config[CONF_ID]),
)
config[CONF_USE_INTERRUPT] = False
return config
# GPIO16 on ESP8266 doesn't support interrupts through attachInterrupt().
if CORE.is_esp8266 and pin_num == 16:
_LOGGER.warning(
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
"Falling back to polling mode (same as in ESPHome <2025.7). "
"The sensor will work exactly as before, but other pins have better "
"performance with interrupts.",
config.get(CONF_NAME, config[CONF_ID]),
)
config[CONF_USE_INTERRUPT] = False
return config
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes.
# Exception: deep_sleep wakeup pins are compatible with interrupts when
# the pin is only shared between this sensor and deep_sleep (count == 2).
if config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
if not _pin_shared_only_with_deep_sleep(pin_num):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared "
"with other components. The sensor will use polling mode for "
"compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
pin_num,
)
config[CONF_USE_INTERRUPT] = False
else:
_LOGGER.debug(
"GPIO binary_sensor '%s': Pin %s is shared with deep_sleep, "
"keeping interrupts enabled.",
config.get(CONF_NAME, config[CONF_ID]),
pin_num,
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
@@ -134,7 +67,36 @@ async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
if config[CONF_USE_INTERRUPT]:
# Check for ESP8266 GPIO16 interrupt limitation
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
# the Arduino attachInterrupt() function. This is the only known GPIO pin
# across all supported platforms that has this limitation, so we handle it
# here instead of in the platform-specific code.
use_interrupt = config[CONF_USE_INTERRUPT]
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
_LOGGER.warning(
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
"Falling back to polling mode (same as in ESPHome <2025.7). "
"The sensor will work exactly as before, but other pins have better "
"performance with interrupts.",
config.get(CONF_NAME, config[CONF_ID]),
)
use_interrupt = False
# Check if pin is shared with other components (allow_other_uses)
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
"The sensor will use polling mode for compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
config[CONF_PIN][CONF_NUMBER],
)
use_interrupt = False
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
else:
cg.add(var.set_use_interrupt(False))
# Only generate call when disabling interrupts (default is true)
cg.add(var.set_use_interrupt(use_interrupt))
@@ -46,6 +46,11 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
}
void GPIOBinarySensor::setup() {
if (this->store_.use_interrupt_ && !this->pin_->is_internal()) {
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
this->store_.use_interrupt_ = false;
}
if (this->store_.use_interrupt_) {
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
this->store_.setup(internal_pin, this);
@@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
)
INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
def _validate_esp32_variant(config):
+1 -1
View File
@@ -283,7 +283,7 @@ async def to_code(config):
try:
return Image.open(path)
except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}") from e
raise core.EsphomeError(f"Could not load image file {path}: {e}")
# make a wide horizontal combined image.
images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]]
@@ -1,7 +1,5 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace ili9xxx {
@@ -229,10 +229,6 @@ void ILI9XXXDisplay::update() {
}
void ILI9XXXDisplay::display_() {
// buffer may be null if allocation failed
if (this->buffer_ == nullptr) {
return;
}
// check if something was displayed
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
return;
+7 -10
View File
@@ -28,6 +28,7 @@ from esphome.const import (
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.final_validate import full_config
_LOGGER = logging.getLogger(__name__)
@@ -675,16 +676,12 @@ def _final_validate(config):
:param config:
:return:
"""
config = config.copy()
for c in config:
if byte_order := c.get(CONF_BYTE_ORDER):
if byte_order == "BIG_ENDIAN":
_LOGGER.warning(
"The image '%s' is configured with big-endian byte order, little-endian is expected",
c.get(CONF_FILE),
)
else:
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
fv = full_config.get()
if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config):
config = config.copy()
for c in config:
if not c.get(CONF_BYTE_ORDER):
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config
+1 -1
View File
@@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const {
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
-17
View File
@@ -1,6 +1,5 @@
import json
import logging
from pathlib import Path
import esphome.codegen as cg
import esphome.config_validation as cv
@@ -25,7 +24,6 @@ from esphome.const import (
)
from esphome.core import CORE
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.helpers import copy_file_if_changed
from esphome.storage_json import StorageJSON
from . import gpio # noqa
@@ -467,11 +465,6 @@ async def component_to_code(config):
# it for project source files only. GCC uses the last -O flag.
build_src_flags += " -Os"
cg.add_platformio_option("build_src_flags", build_src_flags)
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
# On other families, patch_linker.py routes .sram.text into the right
# RAM-executable output section and prints a post-link placement summary.
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"])
# dummy version code
cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)"))
# decrease web server stack size (16k words -> 4k words)
@@ -556,13 +549,3 @@ async def component_to_code(config):
_configure_lwip(config)
await cg.register_component(var, config)
# Called by writer.py
def copy_files() -> None:
script_dir = Path(__file__).parent
patch_linker_file = script_dir / "patch_linker.py.script"
copy_file_if_changed(
patch_linker_file,
CORE.relative_build_path("patch_linker.py"),
)
@@ -79,11 +79,6 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
'''
BASE_CODE_BOARDS = '''
@@ -1,171 +0,0 @@
# pylint: disable=E0602
Import("env") # noqa
import os
import re
import subprocess
# ESPHome marks ISR code IRAM_ATTR, which on LibreTiny maps to a per-family
# section routed into RAM-executable memory (see esphome/core/hal.h).
#
# This script is NOT loaded on BK72xx (IRAM_ATTR is a no-op there; the SDK
# masks FIQ+IRQ around flash writes). On the remaining families:
# - RTL8710B: hal.h uses section(".image2.ram.text"); stock linker consumes it.
# - RTL8720C: hal.h uses section(".sram.text"); stock linker consumes it.
# - LN882H: stock linker has no glob for ".sram.text", so we inject
# KEEP(*(.sram.text*)) into ".flash_copysection" (> RAM0 AT> FLASH).
#
# All families also get a post-link summary showing where IRAM_ATTR landed.
_MARKER = "/* esphome .sram.text */"
# Strong assignments (not PROVIDE) so the symbols are always emitted in the
# ELF; PROVIDE symbols with no references can be garbage-collected.
_KEEP_LINE = (
" __esphome_sram_text_start = .; "
"KEEP(*(.sram.text*)) "
"__esphome_sram_text_end = .; "
+ _MARKER + "\n"
)
_LN_COPY = re.compile(r"(\.flash_copysection\s*:\s*\{\s*\n)")
def _detect(env):
prefix = "USE_LIBRETINY_VARIANT_"
# CPPDEFINES may hold strings or (name, value) tuples; BUILD_FLAGS holds
# the raw "-DNAME" strings. PlatformIO populates both, but the exact order
# vs. extra_scripts varies, so check both to be robust.
for token in env.get("CPPDEFINES", []):
if isinstance(token, (list, tuple)):
token = token[0]
if isinstance(token, str) and token.startswith(prefix):
return token[len(prefix):]
for flag in env.get("BUILD_FLAGS", []):
if isinstance(flag, str) and "-D" + prefix in flag:
name = flag.split("-D", 1)[1].split("=", 1)[0].strip()
if name.startswith(prefix):
return name[len(prefix):]
return None
KNOWN_VARIANTS = frozenset({
"LN882H",
"RTL8710B",
"RTL8720C",
})
def _inject_keep(host_section):
"""Return a patcher that injects _KEEP_LINE at the top of `host_section`."""
def patch(content):
if _MARKER in content:
return content
return host_section.sub(r"\1" + _KEEP_LINE, content, count=1)
return patch
# Variants not listed here intentionally have no .ld patcher:
# - RTL8710B: hal.h uses section(".image2.ram.text") which the stock linker
# already routes into .ram_image2.text (> BD_RAM).
# - RTL8720C: stock linker already consumes *(.sram.text*).
# - BK72xx (all): SDK masks FIQ+IRQ around flash writes, IRAM_ATTR is no-op.
_PATCHERS_BY_VARIANT = {
"LN882H": (_inject_keep(_LN_COPY),),
}
def _patchers_for(variant):
return _PATCHERS_BY_VARIANT.get(variant, ())
def _pre_link(target, source, env):
build_dir = env.subst("$BUILD_DIR")
ld_files = [f for f in os.listdir(build_dir) if f.endswith(".ld")]
patched = 0
for name in ld_files:
path = os.path.join(build_dir, name)
with open(path, "r", encoding="utf-8") as fh:
original = fh.read()
if _MARKER in original:
patched += 1
continue
content = original
for fn in _patchers:
content = fn(content)
if content != original:
with open(path, "w", encoding="utf-8") as fh:
fh.write(content)
print("ESPHome: patched {} for IRAM_ATTR placement".format(name))
patched += 1
if not patched:
raise RuntimeError(
"ESPHome: no .ld in {} was patched for IRAM_ATTR. Update the "
"regex in patch_linker.py.script (_PATCHERS_BY_VARIANT).".format(
build_dir
)
)
# Substrings matched against demangled names as a fallback on RTL8720C,
# where we cannot inject __esphome_sram_text_start/end markers.
_FALLBACK_SUBSTRINGS = ("wake_loop_any_context", "wake_loop_isrsafe",
"enable_loop_soon_any_context")
def _post_link(target, source, env):
"""Print where IRAM_ATTR ended up so users can confirm at a glance."""
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
if not os.path.isfile(elf):
return
nm = env.subst("$NM")
try:
out = subprocess.check_output(
[nm, "--defined-only", "--demangle", elf], text=True
)
except (OSError, subprocess.CalledProcessError) as exc:
print("ESPHome: IRAM_ATTR summary unavailable (nm failed: {})".format(exc))
return
start = end = None
fallback = []
for line in out.splitlines():
parts = line.split(maxsplit=2)
if len(parts) != 3:
continue
addr_str, _kind, name = parts
if name == "__esphome_sram_text_start":
start = int(addr_str, 16)
elif name == "__esphome_sram_text_end":
end = int(addr_str, 16)
elif "veneer" not in name and any(s in name for s in _FALLBACK_SUBSTRINGS):
fallback.append(int(addr_str, 16))
print("ESPHome: IRAM_ATTR placement summary ({}):".format(_variant))
if start is not None and end is not None:
print(" .sram.text: {} bytes at 0x{:08x} - 0x{:08x}".format(end - start, start, end))
elif fallback:
lo, hi = min(fallback), max(fallback)
print(" IRAM symbols at 0x{:08x} - 0x{:08x} (approx {} bytes)".format(lo, hi, hi - lo))
else:
print(" no IRAM_ATTR symbols found")
if (_variant := _detect(env)) is None:
raise RuntimeError(
"ESPHome: could not determine LibreTiny variant from build flags. "
"patch_linker.py needs USE_LIBRETINY_VARIANT_* to route IRAM_ATTR "
"into SRAM; without it, ISR handlers would silently end up in flash."
)
if _variant not in KNOWN_VARIANTS:
raise RuntimeError(
"ESPHome: unknown LibreTiny variant {!r}; patch_linker.py does not "
"know how to route IRAM_ATTR into SRAM for this family. Update "
"patch_linker.py.script before shipping firmware.".format(_variant)
)
if _patchers := _patchers_for(_variant):
# LibreTiny writes the processed .ld templates into $BUILD_DIR during its
# own builder setup, which may run after this script. Register the patch
# as a pre-link action so it executes once the linker scripts exist.
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", _pre_link)
# Post-link summary for every family that reaches this script.
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", _post_link)
+6 -57
View File
@@ -58,12 +58,6 @@ void AddressableLightTransformer::start() {
// our transition will handle brightness, disable brightness in correction.
this->light_.correction_.set_local_brightness(255);
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
// Uniformity scan is deferred to the first apply() call. start() can run before the underlying
// LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE
// triggering a transition), and reading through ESPColorView would deref a null buffer.
this->uniform_start_scanned_ = false;
this->uniform_start_is_uniform_ = false;
}
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
@@ -103,57 +97,12 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
// non-linear when applying small deltas.
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
// Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the
// frame buffer is valid. When every LED already has the same color (the common case: plain
// turn_on/turn_off on a uniform strip), interpolate math-only against a single start color.
// Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip
// quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27
// round to stored 0, freezing progress).
if (!this->uniform_start_scanned_) {
this->uniform_start_scanned_ = true;
if (this->light_.size() > 0) {
Color first = this->light_[0].get();
bool uniform = true;
for (int32_t i = 1; i < this->light_.size(); i++) {
if (this->light_[i].get() != first) {
uniform = false;
break;
}
}
if (uniform) {
this->uniform_start_color_ = first;
this->uniform_start_is_uniform_ = true;
}
}
}
if (this->uniform_start_is_uniform_) {
// All LEDs started at the same color: compute the interpolated value once and write it to
// every LED. No read-back, so each LED's stored byte advances through every gamma threshold
// as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
//
// Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be
// overwritten on the next apply() here. The fallback path below would have respected them
// via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we
// support, so this is acceptable.
// lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
const Color &start = this->uniform_start_color_;
int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining);
uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining);
uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining);
uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining);
for (auto led : this->light_) {
led.set_rgbw(r, g, b, w);
}
} else {
int32_t scale =
int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
this->last_transition_progress_ = smoothed_progress;
this->light_.schedule_show();
@@ -115,9 +115,6 @@ class AddressableLightTransformer : public LightTransformer {
AddressableLight &light_;
float last_transition_progress_{0.0f};
Color target_color_{};
Color uniform_start_color_{};
bool uniform_start_scanned_{false};
bool uniform_start_is_uniform_{false};
};
} // namespace esphome::light
-5
View File
@@ -65,8 +65,3 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
+1 -3
View File
@@ -44,7 +44,6 @@ from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from esphome.writer import clean_build
from esphome.yaml_util import load_yaml
from . import defines as df, helpers, lv_validation as lvalid, widgets
@@ -452,8 +451,7 @@ async def to_code(configs):
df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1")
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
clean_build(clear_pio_cache=False)
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
cg.add_build_flag("-DLV_CONF_H=1")
# handle windows paths in a way that doesn't break the generated C++
lv_conf_h_path = Path(lv_conf_h_file).as_posix()
+5 -6
View File
@@ -76,17 +76,16 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
}
#endif
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
#if LV_USE_IMAGE
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
#endif // LV_USE_IMAGE
// Shortcut / overload, so that the source of an image can easily be updated
// from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); }
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
}
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
}
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
+3 -3
View File
@@ -195,7 +195,7 @@ def model_schema(config):
"big_endian", "little_endian", lower=True
),
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
model.option(CONF_DRAW_ROUNDING, 1): power_of_two,
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
*pixel_modes, lower=True
),
@@ -297,9 +297,9 @@ def _final_validate(config):
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
fraction = min(20000.0, buffer_size // 4) / buffer_size
fraction = min(20000.0, buffer_size // 16) / buffer_size
config[CONF_BUFFER_SIZE] = 1.0 / next(
(x for x in range(2, 8) if fraction >= 1 / x), 8
x for x in range(2, 17) if fraction >= 1 / x
)
+7 -7
View File
@@ -546,12 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING;
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) {
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
this->start_line_ += this->get_height_internal() / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal());
this->end_line_ =
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
if (this->auto_clear_enabled_) {
this->clear();
}
@@ -573,13 +574,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
// Some chips require that the drawing window be aligned on certain boundaries
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
this->x_high_ = round_buffer(this->x_high_ + 1) - 1;
this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1);
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_,
round_buffer(this->get_width_internal()) - w - this->x_low_);
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
// invalidate watermarks
this->x_low_ = this->get_width_internal();
this->y_low_ = this->get_height_internal();
@@ -15,7 +15,7 @@ from esphome.components.mipi import (
import esphome.config_validation as cv
from .amoled import CO5300
from .ili import ILI9488_A, ST7789V
from .ili import ILI9488_A
from .jc import AXS15231
DriverChip(
@@ -243,15 +243,3 @@ ST7789P.extend(
),
),
)
ST7789V.extend(
"WAVESHARE-ESP32-C6-LCD-1.47",
width=172,
height=320,
offset_width=34,
invert_colors=True,
data_rate="40MHz",
reset_pin=21,
cs_pin=14,
dc_pin={"number": 15, "ignore_strapping_warning": True},
)
@@ -7,7 +7,7 @@ namespace esphome::mitsubishi_cn105 {
static const char *const TAG = "mitsubishi_cn105.climate";
static constexpr std::array MODE_MAP{
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_HEAT_COOL},
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT},
std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY},
std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL},
@@ -76,13 +76,23 @@ void MitsubishiCN105Climate::loop() {
climate::ClimateTraits MitsubishiCN105Climate::traits() {
climate::ClimateTraits traits;
for (const auto &p : MODE_MAP) {
traits.add_supported_mode(p.second);
}
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_DRY,
climate::CLIMATE_MODE_FAN_ONLY,
climate::CLIMATE_MODE_AUTO,
});
for (const auto &p : FAN_MODE_MAP) {
traits.add_supported_fan_mode(p.second);
}
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_MIDDLE,
climate::CLIMATE_FAN_HIGH,
});
traits.set_visual_min_temperature(16.0f);
traits.set_visual_max_temperature(31.0f);
+34 -74
View File
@@ -36,9 +36,8 @@ bool Nextion::send_command_(const std::string &command) {
}
#ifdef USE_NEXTION_COMMAND_SPACING
const uint32_t now = App.get_loop_component_start_time();
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send(now)) {
ESP_LOGN(TAG, "Command spacing: delaying '%s'", command.c_str());
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) {
ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
return false;
}
#endif // USE_NEXTION_COMMAND_SPACING
@@ -49,16 +48,6 @@ bool Nextion::send_command_(const std::string &command) {
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
this->write_array(to_send, sizeof(to_send));
#ifdef USE_NEXTION_COMMAND_SPACING
// Mark sent immediately after writing to UART. The pacer enforces inter-command
// spacing from the transmit side. Marking on ACK (0x01) would leave last_command_time_
// at zero indefinitely, making can_send() always return true and spacing a no-op.
// ignore_is_setup_ commands (setup/init sequence) bypass spacing intentionally.
if (!this->connection_state_.ignore_is_setup_) {
this->command_pacer_.mark_sent(now);
}
#endif // USE_NEXTION_COMMAND_SPACING
return true;
}
@@ -264,8 +253,11 @@ bool Nextion::send_command(const char *command) {
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
return false;
this->add_no_result_to_queue_with_command_("command", command);
return true;
if (this->send_command_(command)) {
this->add_no_result_to_queue_("command");
return true;
}
return false;
}
bool Nextion::send_command_printf(const char *format, ...) {
@@ -282,8 +274,11 @@ bool Nextion::send_command_printf(const char *format, ...) {
return false;
}
this->add_no_result_to_queue_with_command_("command_printf", buffer);
return true;
if (this->send_command_(buffer)) {
this->add_no_result_to_queue_("command_printf");
return true;
}
return false;
}
#ifdef NEXTION_PROTOCOL_LOG
@@ -354,43 +349,25 @@ void Nextion::loop() {
}
#ifdef USE_NEXTION_COMMAND_SPACING
// Try to send any pending commands if spacing allows
this->process_pending_in_queue_();
#ifdef USE_NEXTION_WAVEFORM
if (!this->waveform_queue_.empty()) {
this->check_pending_waveform_();
}
#endif // USE_NEXTION_WAVEFORM
#endif // USE_NEXTION_COMMAND_SPACING
}
#ifdef USE_NEXTION_COMMAND_SPACING
void Nextion::process_pending_in_queue_() {
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
size_t commands_sent = 0;
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) {
return;
}
for (auto *item : this->nextion_queue_) {
if (item == nullptr || item->pending_command.empty()) {
continue; // Already sent, waiting for ACK — skip, don't stop
// Check if first item in queue has a pending command
auto *front_item = this->nextion_queue_.front();
if (front_item && !front_item->pending_command.empty()) {
if (this->send_command_(front_item->pending_command)) {
// Command sent successfully, clear the pending command
front_item->pending_command.clear();
ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str());
}
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (++commands_sent > this->max_commands_per_loop_) {
ESP_LOGV(TAG, "Pending cmds: loop limit reached, deferring");
break;
}
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
const uint32_t now = App.get_loop_component_start_time();
if (!this->command_pacer_.can_send(now)) {
break; // Spacing not elapsed, stop for this loop iteration
}
if (!this->send_command_(item->pending_command)) {
break; // Unexpected send failure, stop
}
item->pending_command.clear();
ESP_LOGVV(TAG, "Pending cmd sent: %s", item->component->get_variable_name().c_str());
}
}
#endif // USE_NEXTION_COMMAND_SPACING
@@ -493,6 +470,10 @@ void Nextion::process_nextion_commands_() {
this->setup_callback_.call();
}
}
#ifdef USE_NEXTION_COMMAND_SPACING
this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent
ESP_LOGN(TAG, "Command spacing: marked command sent");
#endif
break;
case 0x02: // invalid Component ID or name was used
ESP_LOGW(TAG, "Invalid component ID/name");
@@ -1098,18 +1079,10 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
}
/**
* @brief Send a command and enqueue it for response tracking.
* @brief
*
* Callers are responsible for checking is_sleeping() before calling this
* method. The sleep guard is deliberately absent here because some callers
* (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly
* sleep-safe and must bypass it.
*
* If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready,
* the command is saved in the queue entry for retry rather than dropped.
*
* @param variable_name Name of the variable or component associated with the command.
* @param command The raw command string to send.
* @param variable_name Variable name for the queue
* @param command
*/
void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) {
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty())
@@ -1290,22 +1263,9 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
std::string command = "get " + component->get_variable_name_to_send();
#ifdef USE_NEXTION_COMMAND_SPACING
// Always enqueue first so the response handler is present when the command
// is eventually sent. Store the command for retry if spacing blocked it;
// process_pending_in_queue_() will transmit it when the pacer allows.
nextion_queue->pending_command = command;
this->nextion_queue_.push_back(nextion_queue);
if (this->send_command_(command)) {
nextion_queue->pending_command.clear();
}
#else // USE_NEXTION_COMMAND_SPACING
if (this->send_command_(command)) {
this->nextion_queue_.push_back(nextion_queue);
} else {
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
}
#endif // USE_NEXTION_COMMAND_SPACING
}
#ifdef USE_NEXTION_WAVEFORM
@@ -1349,10 +1309,10 @@ void Nextion::check_pending_waveform_() {
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
// If spacing or setup state blocks the send, leave the entry at the front
// of waveform_queue_ for retry on the next loop iteration via
// check_pending_waveform_(). Only pop on a successful send.
this->send_command_(command);
if (!this->send_command_(command)) {
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop();
}
}
#endif // USE_NEXTION_WAVEFORM
+5 -10
View File
@@ -55,20 +55,15 @@ class NextionCommandPacer {
uint8_t get_spacing() const { return spacing_ms_; }
/**
* @brief Check if enough time has passed to send the next command.
* @param now Current timestamp in milliseconds (use App.get_loop_component_start_time()
* for consistency with the rest of the queue timing).
* @return true if the spacing interval has elapsed since the last command was sent.
* @brief Check if enough time has passed to send next command
* @return true if enough time has passed since last command
*/
bool can_send(uint32_t now) const { return (now - last_command_time_) >= spacing_ms_; }
bool can_send() const { return (millis() - last_command_time_) >= spacing_ms_; }
/**
* @brief Record the transmit timestamp for the most recently sent command.
* @param now Current timestamp in milliseconds, as returned by
* App.get_loop_component_start_time(). Must use the same clock
* source as can_send() to avoid unsigned underflow.
* @brief Mark a command as sent, updating the timing
*/
void mark_sent(uint32_t now) { last_command_time_ = now; }
void mark_sent() { last_command_time_ = millis(); }
private:
uint8_t spacing_ms_;
+1 -1
View File
@@ -44,7 +44,7 @@ void PCF85063Component::read_time() {
.year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+1 -1
View File
@@ -44,7 +44,7 @@ void PCF8563Component::read_time() {
.year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+2 -2
View File
@@ -100,7 +100,7 @@ void QMC5883LComponent::update() {
// ROL_PNT in setup and reading 7 bytes starting at the status register.
// If status and all three axes are desired, using ROL_PNT saves you 3 bytes.
// But simply not reading status saves you 4 bytes always and is much simpler.
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) {
err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1);
if (err != i2c::ERROR_OK) {
char buf[32];
@@ -165,7 +165,7 @@ void QMC5883LComponent::update() {
temp = int16_t(raw_temp) * 0.01f;
}
ESP_LOGV(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
temp, status);
if (this->x_sensor_ != nullptr)
-5
View File
@@ -65,8 +65,3 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
@@ -32,101 +32,40 @@ void RuntimeStatsCollector::log_stats_() {
" Period stats (last %" PRIu32 "ms): %zu active components",
this->log_interval_, count);
// Sum component time so we can derive main-loop overhead
// (active loop time minus time attributable to component loop()s).
// Period sum iterates the active-in-period subset; total sum must iterate
// all components since total_active_time_us_ includes iterations where
// currently-idle components previously ran.
uint64_t period_component_sum_us = 0;
if (count == 0) {
return;
}
// Sort by period runtime (descending)
std::sort(sorted, sorted + count, compare_period_time);
// Log top components by period runtime
for (size_t i = 0; i < count; i++) {
period_component_sum_us += sorted[i]->runtime_stats_.period_time_us;
}
uint64_t total_component_sum_us = 0;
for (auto *component : components) {
total_component_sum_us += component->runtime_stats_.total_time_us;
}
if (count > 0) {
// Sort by period runtime (descending)
std::sort(sorted, sorted + count, compare_period_time);
// Log top components by period runtime
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
}
}
// Main-loop overhead for the period: active wall time minus component time.
// active = sum of per-iteration loop time excluding yield/sleep.
if (this->period_active_count_ > 0) {
uint64_t active = this->period_active_time_us_;
uint64_t overhead = active > period_component_sum_us ? active - period_component_sum_us : 0;
// Use double for µs→ms conversion so multi-day uptimes (where total
// microsecond counters exceed float's ~7-digit mantissa) keep resolution.
ESP_LOGI(TAG,
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
"overhead_total=%.1fms",
this->period_active_count_,
static_cast<double>(active) / static_cast<double>(this->period_active_count_) / 1000.0,
static_cast<double>(this->period_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
static_cast<double>(overhead) / 1000.0);
uint64_t before = this->period_before_time_us_;
uint64_t tail = this->period_tail_time_us_;
uint64_t accounted = before + tail;
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
static_cast<double>(inter) / 1000.0);
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
}
// Log total stats since boot (only for active components - idle ones haven't changed)
ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
if (count > 0) {
// Re-sort by total runtime for all-time stats
std::sort(sorted, sorted + count, compare_total_time);
// Re-sort by total runtime for all-time stats
std::sort(sorted, sorted + count, compare_total_time);
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
}
}
if (this->total_active_count_ > 0) {
uint64_t active = this->total_active_time_us_;
uint64_t overhead = active > total_component_sum_us ? active - total_component_sum_us : 0;
ESP_LOGI(TAG,
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
"overhead_total=%.1fms",
this->total_active_count_,
static_cast<double>(active) / static_cast<double>(this->total_active_count_) / 1000.0,
static_cast<double>(this->total_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
static_cast<double>(overhead) / 1000.0);
uint64_t before = this->total_before_time_us_;
uint64_t tail = this->total_tail_time_us_;
uint64_t accounted = before + tail;
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
static_cast<double>(inter) / 1000.0);
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
}
// Reset period stats
for (auto *component : components) {
component->runtime_stats_.reset_period();
}
this->period_active_count_ = 0;
this->period_active_time_us_ = 0;
this->period_active_max_us_ = 0;
this->period_before_time_us_ = 0;
this->period_tail_time_us_ = 0;
}
bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) {
@@ -29,31 +29,6 @@ class RuntimeStatsCollector {
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
// Record the wall time of one main loop iteration excluding the yield/sleep.
// Called once per loop from Application::loop().
// active_us = total time between loop start and just before yield.
// before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop).
// tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix.
// Residual overhead at log time = active Σ(component) before tail,
// which captures per-iteration inter-component bookkeeping (set_current_component,
// WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls,
// the for-loop itself).
void record_loop_active(uint32_t active_us, uint32_t before_us, uint32_t tail_us) {
this->period_active_count_++;
this->period_active_time_us_ += active_us;
if (active_us > this->period_active_max_us_)
this->period_active_max_us_ = active_us;
this->total_active_count_++;
this->total_active_time_us_ += active_us;
if (active_us > this->total_active_max_us_)
this->total_active_max_us_ = active_us;
this->period_before_time_us_ += before_us;
this->total_before_time_us_ += before_us;
this->period_tail_time_us_ += tail_us;
this->total_tail_time_us_ += tail_us;
}
protected:
void log_stats_();
// Static comparators — member functions have friend access, lambdas do not
@@ -62,22 +37,6 @@ class RuntimeStatsCollector {
uint32_t log_interval_;
uint32_t next_log_time_{0};
// Main loop active-time stats (wall time per iteration, excluding yield/sleep).
// Counters are uint64_t — at sub-millisecond loop times a uint32_t can wrap in
// a few weeks of uptime, which is well within ESPHome device lifetimes.
uint64_t period_active_count_{0};
uint64_t period_active_time_us_{0};
uint32_t period_active_max_us_{0};
uint64_t total_active_count_{0};
uint64_t total_active_time_us_{0};
uint32_t total_active_max_us_{0};
// Split of overhead sections — accumulated per iteration.
uint64_t period_before_time_us_{0};
uint64_t total_before_time_us_{0};
uint64_t period_tail_time_us_{0};
uint64_t total_tail_time_us_{0};
};
} // namespace runtime_stats
+1 -1
View File
@@ -81,7 +81,7 @@ void RX8130Component::read_time() {
.year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+1 -4
View File
@@ -87,10 +87,7 @@ async def to_code(config):
config[CONF_REBOOT_TIMEOUT],
config[CONF_BOOT_IS_GOOD_AFTER],
)
# Wrap in IIFEUnsafeStatement so cpp_main_section emits this
# component's block flat rather than inside an IIFE lambda —
# the `return` must exit setup() itself, not just the lambda.
cg.add(cg.IIFEUnsafeStatement(RawExpression(f"if ({condition}) return")))
cg.add(RawExpression(f"if ({condition}) return"))
CORE.data[CONF_SAFE_MODE] = {}
CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True
+1 -1
View File
@@ -84,7 +84,7 @@ def get_firmware(value):
req = requests.get(url, timeout=30)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download firmware file ({url}): {e}") from e
raise cv.Invalid(f"Could not download firmware file ({url}): {e}")
h = hashlib.new("sha256")
h.update(req.content)
@@ -11,10 +11,6 @@
#include "esphome/core/wake.h"
#include "esphome/core/log.h"
#ifdef USE_OTA_PLATFORM_ESPHOME
extern "C" void esphome_wake_ota_component_any_context();
#endif
#ifdef USE_ESP8266
#include <coredecls.h> // For esp_schedule()
#elif defined(USE_RP2040)
@@ -858,10 +854,6 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) {
tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn);
tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn);
LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
#ifdef USE_OTA_PLATFORM_ESPHOME
// Must run before wake_loop_any_context() so flags are visible when the main task wakes.
esphome_wake_ota_component_any_context();
#endif
// Wake the main loop immediately so it can accept the new connection.
esphome::wake_loop_any_context();
return ERR_OK;
@@ -173,7 +173,7 @@ def _read_audio_file_and_type(file_config):
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
) from e
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type in ("wav"):
+10 -10
View File
@@ -45,8 +45,8 @@ MODELS = {
presets={
CONF_HEIGHT: 240,
CONF_WIDTH: 135,
CONF_OFFSET_HEIGHT: 40,
CONF_OFFSET_WIDTH: 52,
CONF_OFFSET_HEIGHT: 52,
CONF_OFFSET_WIDTH: 40,
CONF_CS_PIN: "GPIO5",
CONF_DC_PIN: "GPIO16",
CONF_RESET_PIN: "GPIO23",
@@ -68,8 +68,8 @@ MODELS = {
presets={
CONF_HEIGHT: 280,
CONF_WIDTH: 240,
CONF_OFFSET_HEIGHT: 20,
CONF_OFFSET_WIDTH: 0,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 20,
}
),
"ADAFRUIT_S2_TFT_FEATHER_240X135": model_spec(
@@ -77,8 +77,8 @@ MODELS = {
presets={
CONF_HEIGHT: 240,
CONF_WIDTH: 135,
CONF_OFFSET_HEIGHT: 40,
CONF_OFFSET_WIDTH: 52,
CONF_OFFSET_HEIGHT: 52,
CONF_OFFSET_WIDTH: 40,
CONF_CS_PIN: "GPIO7",
CONF_DC_PIN: "GPIO39",
CONF_RESET_PIN: "GPIO40",
@@ -89,8 +89,8 @@ MODELS = {
presets={
CONF_HEIGHT: 320,
CONF_WIDTH: 170,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 35,
CONF_OFFSET_HEIGHT: 35,
CONF_OFFSET_WIDTH: 0,
CONF_ROTATION: 270,
CONF_CS_PIN: "GPIO10",
CONF_DC_PIN: "GPIO13",
@@ -102,8 +102,8 @@ MODELS = {
presets={
CONF_HEIGHT: 320,
CONF_WIDTH: 172,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 34,
CONF_OFFSET_HEIGHT: 34,
CONF_OFFSET_WIDTH: 0,
CONF_ROTATION: 90,
CONF_CS_PIN: "GPIO21",
CONF_DC_PIN: "GPIO22",
+4 -15
View File
@@ -7,11 +7,6 @@ namespace status_led {
static const char *const TAG = "status_led";
static constexpr uint32_t ERROR_PERIOD_MS = 250;
static constexpr uint32_t ERROR_ON_MS = 150;
static constexpr uint32_t WARNING_PERIOD_MS = 1500;
static constexpr uint32_t WARNING_ON_MS = 250;
StatusLED *global_status_led = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
StatusLED::StatusLED(GPIOPin *pin) : pin_(pin) { global_status_led = this; }
@@ -24,18 +19,12 @@ void StatusLED::dump_config() {
LOG_PIN(" Pin: ", this->pin_);
}
void StatusLED::loop() {
const uint32_t app_state = App.get_app_state();
// Use millis() rather than App.get_loop_component_start_time() because this loop is also
// dispatched from Application::feed_wdt() during long blocking operations, where the cached
// per-component timestamp doesn't advance and would freeze the blink pattern.
const uint32_t now = millis();
if ((app_state & STATUS_LED_ERROR) != 0u) {
this->pin_->digital_write(now % ERROR_PERIOD_MS < ERROR_ON_MS);
} else if ((app_state & STATUS_LED_WARNING) != 0u) {
this->pin_->digital_write(now % WARNING_PERIOD_MS < WARNING_ON_MS);
if ((App.get_app_state() & STATUS_LED_ERROR) != 0u) {
this->pin_->digital_write(millis() % 250u < 150u);
} else if ((App.get_app_state() & STATUS_LED_WARNING) != 0u) {
this->pin_->digital_write(millis() % 1500u < 250u);
} else {
this->pin_->digital_write(false);
this->disable_loop();
}
}
float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; }
+4 -6
View File
@@ -35,9 +35,8 @@ def validate_acceleration(value):
try:
value = float(value)
except ValueError:
raise cv.Invalid(
f"Expected acceleration as floating point number, got {value}"
) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(f"Expected acceleration as floating point number, got {value}")
if value <= 0:
raise cv.Invalid("Acceleration must be larger than 0 steps/s^2!")
@@ -56,9 +55,8 @@ def validate_speed(value):
try:
value = float(value)
except ValueError:
raise cv.Invalid(
f"Expected speed as floating point number, got {value}"
) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(f"Expected speed as floating point number, got {value}")
if value <= 0:
raise cv.Invalid("Speed must be larger than 0 steps/s!")
+1 -1
View File
@@ -188,7 +188,7 @@ def _expand_substitutions(
f"\nRelevant context:\n{err.context_trace_str()}"
f"\nSee {'->'.join(str(x) for x in path)}",
path,
) from err
)
else:
if isinstance(orig_value, ESPHomeDataBase):
value = _restore_data_base(value, orig_value)
+2 -2
View File
@@ -200,11 +200,11 @@ CONFIG_SCHEMA = (
cv.hex_int, cv.Range(min=0, max=0xFFFF)
),
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.int_range(min=0, max=100000)
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_HW_VERSION): cv.one_of(
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
+2 -2
View File
@@ -197,11 +197,11 @@ CONFIG_SCHEMA = (
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_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.int_range(min=0, max=100000)
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_MODULATION): cv.enum(MOD),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
+4 -2
View File
@@ -109,7 +109,8 @@ def _parse_cron_int(value, special_mapping, message):
try:
return int(value)
except ValueError:
raise cv.Invalid(message.format(value)) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(message.format(value))
def _parse_cron_part(part, min_value, max_value, special_mapping):
@@ -133,9 +134,10 @@ def _parse_cron_part(part, min_value, max_value, special_mapping):
try:
repeat_n = int(repeat)
except ValueError:
# pylint: disable=raise-missing-from
raise cv.Invalid(
f"Repeat for '/' time expression must be an integer, got {repeat}"
) from None
)
return set(range(offset_n, max_value + 1, repeat_n))
if "-" in part:
data = part.split("-")
-7
View File
@@ -347,13 +347,6 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) {
}
return pos - start_pos;
}
void TM1637Display::set_brightness(float brightness) {
auto intensity = clamp(brightness, 0.f, 1.f) * 7;
this->set_on(intensity > 0);
this->set_intensity(intensity);
}
uint8_t TM1637Display::print(const char *str) { return this->print(0, str); }
void TM1637Display::set_buffer(const uint8_t *data, uint8_t length) {
-3
View File
@@ -50,9 +50,6 @@ class TM1637Display : public PollingComponent {
/// Set raw buffer bytes from data array up to length bytes.
void set_buffer(const uint8_t *data, uint8_t length);
/// Set the display brightness. Accepts a value between 0.0 and 1.0; 0 will turn off
/// the display and 1.0 will set it to the maximum brightness.
void set_brightness(float brightness);
void set_intensity(uint8_t intensity) { this->intensity_ = intensity; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_length(uint8_t length) { this->length_ = length; }
@@ -75,9 +75,6 @@ class ListEntitiesIterator final : public ComponentIterator {
#ifdef USE_VALVE
bool on_valve(valve::Valve *obj) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *obj) override { return true; }
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) override;
#endif
@@ -308,7 +308,6 @@ bool CompactString::operator==(const StringRef &other) const {
/// │ - Roaming fail (RECONNECTING on other AP): counter preserved │
/// └──────────────────────────────────────────────────────────────────────┘
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
if (phase == WiFiRetryPhase::INITIAL_CONNECT)
@@ -327,7 +326,6 @@ static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
return LOG_STR("RESTARTING");
return LOG_STR("UNKNOWN");
}
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
bool WiFiComponent::went_through_explicit_hidden_phase_() const {
// If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
+3 -3
View File
@@ -67,7 +67,7 @@ def _validate_load_certificate(value):
contents = read_relative_config_path(value)
return wrapped_load_pem_x509_certificate(contents)
except ValueError as err:
raise cv.Invalid(f"Invalid certificate: {err}") from err
raise cv.Invalid(f"Invalid certificate: {err}")
def validate_certificate(value):
@@ -86,9 +86,9 @@ def _validate_load_private_key(key, cert_pw):
except ValueError as e:
raise cv.Invalid(
f"There was an error with the EAP 'password:' provided for 'key' {e}"
) from e
)
except TypeError as e:
raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}") from e
raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}")
def _check_private_key_cert_match(key, cert):
+2 -2
View File
@@ -53,7 +53,7 @@ def _cidr_network(value):
try:
ipaddress.ip_network(value, strict=False)
except ValueError as err:
raise cv.Invalid(f"Invalid network in CIDR notation: {err}") from err
raise cv.Invalid(f"Invalid network in CIDR notation: {err}")
return value
@@ -137,7 +137,7 @@ async def to_code(config):
# the '+1' modifier is relative to the device's own address that will
# be automatically added to the provided list.
cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}")
cg.add_library("droscy/esp_wireguard", "0.4.5")
cg.add_library("droscy/esp_wireguard", "0.4.4")
await cg.register_component(var, config)
@@ -1,35 +1,28 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import CONF_ID, Framework
from esphome.core import CORE
from esphome.const import CONF_ESPHOME, CONF_ID, CONF_NAME, Framework
import esphome.final_validate as fv
zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server")
BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component)
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
CONF_ACCEPT = "accept"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLEServer),
cv.Optional(
CONF_ON_NUMERIC_COMPARISON_REQUEST
): automation.validate_automation({}),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_framework(Framework.ZEPHYR),
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_NUMERIC_COMPARISON_REQUEST,
"add_passkey_callback",
[(cg.uint32, "passkey")],
),
)
def _final_validate(_):
full_config = fv.full_config.get()
zephyr_add_prj_conf("BT_DEVICE_NAME", full_config[CONF_ESPHOME][CONF_NAME])
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
@@ -37,39 +30,5 @@ async def to_code(config):
zephyr_add_prj_conf("BT", True)
zephyr_add_prj_conf("BT_PERIPHERAL", True)
zephyr_add_prj_conf("BT_RX_STACK_SIZE", 1536)
zephyr_add_prj_conf("BT_DEVICE_NAME", CORE.name)
# zephyr_add_prj_conf("BT_LL_SW_SPLIT", True)
await cg.register_component(var, config)
if config.get(CONF_ON_NUMERIC_COMPARISON_REQUEST):
zephyr_add_prj_conf("BT_SMP", True)
zephyr_add_prj_conf("BT_SETTINGS", True)
zephyr_add_prj_conf("BT_SMP_SC_ONLY", True)
zephyr_add_prj_conf("BT_KEYS_OVERWRITE_OLDEST", True)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
BLENumericComparisonReplyAction = zephyr_ble_server_ns.class_(
"BLENumericComparisonReplyAction", automation.Action
)
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEServer),
cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean),
}
)
@automation.register_action(
"ble_server.numeric_comparison_reply",
BLENumericComparisonReplyAction,
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA,
synchronous=True,
)
async def numeric_comparison_reply_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent)
templ = await cg.templatable(config[CONF_ACCEPT], args, cg.bool_)
cg.add(var.set_accept(templ))
return var
@@ -3,34 +3,32 @@
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/settings/settings.h>
#include <zephyr/bluetooth/conn.h>
namespace esphome::zephyr_ble_server {
static const char *const TAG = "zephyr_ble_server";
static k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
BLEServer *global_ble_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static struct k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)
static const bt_data AD[] = {
static const struct bt_data AD[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};
static const bt_data SD[] = {
static const struct bt_data SD[] = {
#ifdef USE_OTA
BT_DATA_BYTES(BT_DATA_UUID128_ALL, 0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86, 0xd3, 0x4c, 0xb7, 0x1d, 0x1d,
0xdc, 0x53, 0x8d),
#endif
};
const bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN;
const struct bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN;
static void advertise(k_work *work) {
static void advertise(struct k_work *work) {
int rc = bt_le_adv_stop();
if (rc) {
ESP_LOGE(TAG, "Advertising failed to stop (rc %d)", rc);
@@ -44,276 +42,57 @@ static void advertise(k_work *work) {
ESP_LOGI(TAG, "Advertising successfully started");
}
void BLEServer::connected(bt_conn *conn, uint8_t err) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
static void connected(struct bt_conn *conn, uint8_t err) {
if (err) {
ESP_LOGE(TAG, "Failed to connect to %s (%u)", addr, err);
return;
ESP_LOGE(TAG, "Connection failed (err 0x%02x)", err);
} else {
ESP_LOGI(TAG, "Connected");
}
ESP_LOGI(TAG, "Connected %s", addr);
#ifdef CONFIG_BT_SMP
if (bt_conn_set_security(conn, BT_SECURITY_L4)) {
ESP_LOGE(TAG, "Failed to set security");
}
#endif
conn = bt_conn_ref(conn);
global_ble_server->defer([conn]() { global_ble_server->conn_ = conn; });
}
void BLEServer::disconnected(bt_conn *conn, uint8_t reason) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
ESP_LOGI(TAG, "Disconnected from %s (reason 0x%02x)", addr, reason);
global_ble_server->defer([]() {
if (global_ble_server->conn_) {
bt_conn_unref(global_ble_server->conn_);
global_ble_server->conn_ = nullptr;
}
});
static void disconnected(struct bt_conn *conn, uint8_t reason) {
ESP_LOGI(TAG, "Disconnected (reason 0x%02x)", reason);
k_work_submit(&advertise_work);
}
#ifdef CONFIG_BT_SMP
static void identity_resolved(bt_conn *conn, const bt_addr_le_t *rpa, const bt_addr_le_t *identity) {
char addr_identity[BT_ADDR_LE_STR_LEN];
char addr_rpa[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(identity, addr_identity, sizeof(addr_identity));
bt_addr_le_to_str(rpa, addr_rpa, sizeof(addr_rpa));
ESP_LOGD(TAG, "Identity resolved %s -> %s", addr_rpa, addr_identity);
}
static void security_changed(bt_conn *conn, bt_security_t level, bt_security_err err) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
if (!err) {
ESP_LOGD(TAG, "Security changed: %s level %u", addr, level);
static void bt_ready(int err) {
if (err != 0) {
ESP_LOGE(TAG, "Bluetooth failed to initialise: %d", err);
} else {
ESP_LOGE(TAG, "Security failed: %s level %u err %d", addr, level, err);
k_work_submit(&advertise_work);
}
}
static void pairing_complete(bt_conn *conn, bool bonded) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
ESP_LOGD(TAG, "Pairing completed: %s, bonded: %d", addr, bonded);
}
static void pairing_failed(bt_conn *conn, bt_security_err reason) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
ESP_LOGE(TAG, "Pairing failed conn: %s, reason %d", addr, reason);
bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
static void bond_deleted(uint8_t id, const bt_addr_le_t *peer) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(peer, addr, sizeof(addr));
ESP_LOGD(TAG, "Bond deleted for %s, id %u", addr, id);
}
static void auth_passkey_display(bt_conn *conn, unsigned int passkey) {
char addr[BT_ADDR_LE_STR_LEN];
char passkey_str[7];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
snprintk(passkey_str, 7, "%06u", passkey);
ESP_LOGI(TAG, "Passkey for %s: %s", addr, passkey_str);
}
static void conn_addr_str(bt_conn *conn, char *addr, size_t len) {
struct bt_conn_info info;
if (bt_conn_get_info(conn, &info) < 0) {
addr[0] = '\0';
return;
}
switch (info.type) {
case BT_CONN_TYPE_LE:
bt_addr_le_to_str(info.le.dst, addr, len);
break;
default:
ESP_LOGE(TAG, "Not implemented");
addr[0] = '\0';
break;
}
}
static void auth_cancel(bt_conn *conn) {
char addr[BT_ADDR_LE_STR_LEN];
conn_addr_str(conn, addr, sizeof(addr));
ESP_LOGI(TAG, "Pairing cancelled: %s", addr);
}
void BLEServer::auth_passkey_confirm(bt_conn *conn, unsigned int passkey) {
char addr[BT_ADDR_LE_STR_LEN];
char passkey_str[7];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
snprintk(passkey_str, 7, "%06u", passkey);
ESP_LOGI(TAG, "Confirm passkey for %s: %s", addr, passkey_str);
global_ble_server->defer([passkey]() { global_ble_server->passkey_cb_(passkey); });
}
static void auth_pairing_confirm(bt_conn *conn) {
/* Automatically confirm pairing request from the device side. */
auto err = bt_conn_auth_pairing_confirm(conn);
if (err) {
ESP_LOGE(TAG, "Can't confirm pairing (err: %d)", err);
return;
}
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
ESP_LOGI(TAG, "Pairing confirmed: %s", addr);
}
#endif
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};
void BLEServer::setup() {
global_ble_server = this;
int err = 0;
k_work_init(&advertise_work, advertise);
resume_();
}
static bt_conn_cb conn_callbacks = {
.connected = connected,
.disconnected = disconnected,
#ifdef CONFIG_BT_SMP
.identity_resolved = identity_resolved,
.security_changed = security_changed,
#endif
};
void BLEServer::loop() {
if (this->suspended_) {
resume_();
this->suspended_ = false;
}
}
bt_conn_cb_register(&conn_callbacks);
#ifdef CONFIG_BT_SMP
static struct bt_conn_auth_info_cb conn_auth_info_callbacks = {
.pairing_complete = pairing_complete, .pairing_failed = pairing_failed, .bond_deleted = bond_deleted};
err = bt_conn_auth_info_cb_register(&conn_auth_info_callbacks);
if (err) {
ESP_LOGE(TAG, "Failed to register authorization info callbacks.");
}
static struct bt_conn_auth_cb auth_cb = {
.passkey_display = auth_passkey_display,
.passkey_confirm = auth_passkey_confirm,
.cancel = auth_cancel,
.pairing_confirm = auth_pairing_confirm,
};
err = bt_conn_auth_cb_register(&auth_cb);
if (err) {
ESP_LOGE(TAG, "Failed to set auth handlers (%d)", err);
}
#endif
// callback cannot be used to start scanning due to race conditions with BT_SETTINGS
err = bt_enable(nullptr);
if (err) {
ESP_LOGE(TAG, "Bluetooth enable failed: %d", err);
void BLEServer::resume_() {
int rc = bt_enable(bt_ready);
if (rc != 0) {
ESP_LOGE(TAG, "Bluetooth enable failed: %d", rc);
return;
}
#ifdef CONFIG_BT_SETTINGS
err = settings_load();
if (err) {
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
}
#endif
k_work_submit(&advertise_work);
}
#ifdef ESPHOME_LOG_HAS_DEBUG
static const char *role_str(uint8_t role) {
switch (role) {
case BT_CONN_ROLE_CENTRAL:
return "Central";
case BT_CONN_ROLE_PERIPHERAL:
return "Peripheral";
}
return "Unknown";
}
static void connection_info(bt_conn *conn, void *user_data) {
char addr[BT_ADDR_LE_STR_LEN];
struct bt_conn_info info;
if (bt_conn_get_info(conn, &info) < 0) {
ESP_LOGE(TAG, "Unable to get info: conn %p", conn);
return;
}
switch (info.type) {
case BT_CONN_TYPE_LE:
bt_addr_le_to_str(info.le.dst, addr, sizeof(addr));
ESP_LOGD(TAG, " %u [LE][%s] %s: Interval %u latency %u timeout %u security L%u", info.id, role_str(info.role),
addr, info.le.interval, info.le.latency, info.le.timeout, info.security.level);
break;
default:
ESP_LOGE(TAG, "Not implemented");
break;
}
}
#ifdef CONFIG_BT_BONDABLE
static void bond_info(const struct bt_bond_info *info, void *user_data) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(&info->addr, addr, sizeof(addr));
ESP_LOGD(TAG, " Bond remote identity: %s", addr);
}
#endif
#endif
void BLEServer::dump_config() {
ESP_LOGCONFIG(TAG,
"ble server:\n"
" connected: %s\n"
" name: %s\n"
" appearance: %u\n"
" ready: %s\n"
#ifdef CONFIG_BT_SMP
" security manager: YES",
#else
" security manager: NO",
#endif
YESNO(this->conn_), bt_get_name(), bt_get_appearance(), YESNO(bt_is_ready()));
#ifdef ESPHOME_LOG_HAS_DEBUG
bt_conn_foreach(BT_CONN_TYPE_ALL, connection_info, nullptr);
#ifdef CONFIG_BT_BONDABLE
bt_foreach_bond(BT_ID_DEFAULT, bond_info, nullptr);
#endif
#endif
}
void BLEServer::numeric_comparison_reply(bool accept) {
if (this->conn_ == nullptr) {
ESP_LOGE(TAG, "Not connected");
return;
}
ESP_LOGD(TAG, "Numeric comparison %s", accept ? "accepted" : "rejected");
if (accept) {
bt_conn_auth_passkey_confirm(this->conn_);
} else {
bt_conn_auth_cancel(this->conn_);
}
void BLEServer::on_shutdown() {
struct k_work_sync sync;
k_work_cancel_sync(&advertise_work, &sync);
bt_disable();
this->suspended_ = true;
}
} // namespace esphome::zephyr_ble_server
@@ -1,36 +1,18 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/component.h"
#include <zephyr/bluetooth/conn.h>
#include "esphome/core/automation.h"
namespace esphome::zephyr_ble_server {
class BLEServer : public Component {
public:
void setup() override;
void dump_config() override;
template<typename F> void add_passkey_callback(F &&callback) { this->passkey_cb_.add(std::forward<F>(callback)); }
void numeric_comparison_reply(bool accept);
void loop() override;
void on_shutdown() override;
protected:
static void connected(bt_conn *conn, uint8_t err);
static void disconnected(bt_conn *conn, uint8_t reason);
static void auth_passkey_confirm(bt_conn *conn, unsigned int passkey);
bt_conn *conn_{};
CallbackManager<void(uint32_t)> passkey_cb_;
};
template<typename... Ts> class BLENumericComparisonReplyAction : public Action<Ts...> {
public:
explicit BLENumericComparisonReplyAction(BLEServer *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(bool, accept)
void play(const Ts &...x) override { this->parent_->numeric_comparison_reply(this->accept_.value(x...)); }
protected:
BLEServer *parent_;
void resume_();
bool suspended_ = false;
};
} // namespace esphome::zephyr_ble_server
+20 -33
View File
@@ -544,9 +544,8 @@ def int_(value):
try:
return int(value, base)
except ValueError:
raise Invalid(
f"Expected integer, but cannot parse {value} as an integer"
) from None
# pylint: disable=raise-missing-from
raise Invalid(f"Expected integer, but cannot parse {value} as an integer")
def int_range(min=None, max=None, min_included=True, max_included=True):
@@ -845,7 +844,8 @@ def time_period_str_colon(value):
try:
parsed = [int(x) for x in value.split(":")]
except ValueError:
raise Invalid(TIME_PERIOD_ERROR.format(value)) from None
# pylint: disable=raise-missing-from
raise Invalid(TIME_PERIOD_ERROR.format(value))
if len(parsed) == 2:
hour, minute = parsed
@@ -943,26 +943,7 @@ def time_period_in_minutes_(value):
def update_interval(value):
if value == "never":
return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN)
result = positive_time_period_milliseconds(value)
# 0ms was historically (mis)used as a pseudo-loop() mechanism for
# PollingComponents. Under the hood it calls set_interval(0), which
# causes Scheduler::call() to spin (WDT reset in the field). Coerce
# to 1ms so existing configs keep working at ~1kHz instead of
# spinning. Don't hard-fail so configs don't break on upgrade;
# authors should migrate to HighFrequencyLoopRequester (C++) for
# true run-every-loop behaviour.
if result.total_milliseconds == 0:
_LOGGER.warning(
"update_interval of 0ms is not supported - coercing to 1ms. "
"A literal 0ms schedule would spin the main loop (the scheduled "
"item would always be due, so the scheduler would never yield "
"back) and trigger a watchdog reset. Set update_interval to a "
"non-zero value such as 1ms or higher. (Custom C++ components "
"that need true run-every-loop behaviour should override loop() "
"with HighFrequencyLoopRequester instead.)"
)
return TimePeriodMilliseconds(milliseconds=1)
return result
return positive_time_period_milliseconds(value)
time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict)
@@ -1066,7 +1047,8 @@ def date_time(date: bool, time: bool):
try:
date_obj = datetime.strptime(value, format)
except ValueError as err:
raise Invalid(f"Invalid {exc_message}: {err}") from err
# pylint: disable=raise-missing-from
raise Invalid(f"Invalid {exc_message}: {err}")
return_value = {}
if date:
@@ -1096,9 +1078,8 @@ def mac_address(value):
try:
parts_int.append(int(part, 16))
except ValueError:
raise Invalid(
"MAC Address parts must be hexadecimal values from 00 to FF"
) from None
# pylint: disable=raise-missing-from
raise Invalid("MAC Address parts must be hexadecimal values from 00 to FF")
return core.MACAddress(*parts_int)
@@ -1115,7 +1096,8 @@ def bind_key(value, *, name="Bind key"):
try:
parts_int.append(int(part, 16))
except ValueError:
raise Invalid(f"{name} must be hex values from 00 to FF") from None
# pylint: disable=raise-missing-from
raise Invalid(f"{name} must be hex values from 00 to FF")
return "".join(f"{part:02X}" for part in parts_int)
@@ -1443,7 +1425,8 @@ def mqtt_qos(value):
try:
value = int(value)
except (TypeError, ValueError):
raise Invalid(f"MQTT Quality of Service must be integer, got {value}") from None
# pylint: disable=raise-missing-from
raise Invalid(f"MQTT Quality of Service must be integer, got {value}")
return one_of(0, 1, 2)(value)
@@ -1535,7 +1518,8 @@ def _parse_percentage(value: object) -> float:
else:
value = float(value)
except ValueError:
raise Invalid("invalid number") from None
# pylint: disable=raise-missing-from
raise Invalid("invalid number")
try:
if not has_percent_sign and (value > 1 or value < -1):
raise Invalid(
@@ -1543,7 +1527,9 @@ def _parse_percentage(value: object) -> float:
"outside -1.0 to 1.0. Please put a percent sign after the number!"
)
except TypeError:
raise Invalid("Expected percentage or float") from None
raise Invalid( # pylint: disable=raise-missing-from
"Expected percentage or float"
)
return float(value)
@@ -1716,7 +1702,8 @@ def dimensions(value):
try:
width, height = int(value[0]), int(value[1])
except ValueError:
raise Invalid("Width and height dimensions must be integers") from None
# pylint: disable=raise-missing-from
raise Invalid("Width and height dimensions must be integers")
if width <= 0 or height <= 0:
raise Invalid("Width and height must at least be 1")
return [width, height]
+7 -177
View File
@@ -1,6 +1,5 @@
from collections import defaultdict
from contextlib import contextmanager
from dataclasses import dataclass, field
import logging
import math
import os
@@ -532,126 +531,6 @@ class Library:
return self
# Cap on the number of statements in a single IIFE chunk when a
# component's to_code body is sub-split. Picks a frame-size sweet spot
# on esp32-s3 — large enough that most components fit in one chunk and
# small enough that heavy sensor platforms (many filter registrations)
# don't produce a chunk with a very large spill frame.
IIFE_MAX_STATEMENTS = 50
@dataclass
class _ComponentGroup:
"""A contiguous run of statements emitted by one component's to_code."""
lines: list[str] = field(default_factory=list)
# True when the group contains a statement that must affect setup()'s
# own control flow (e.g. safe_mode's `return`). Emit the group flat,
# bypassing IIFE wrapping entirely.
unsafe: bool = False
# True when the group contains a statement that may declare a
# function-local whose lifetime extends past the current statement
# (scope-brace RawStatement, direct RawExpression, typed
# AssignmentExpression). Wrap the group in a single IIFE without
# sub-splitting so the declaration and any later references stay
# in the same lambda.
no_split: bool = False
def _emits_bare_local(exp: "Statement") -> bool:
"""True if ``exp`` emits a scope brace or bare-raw construct that may
declare a function-local whose lifetime extends past the current
statement. Components that emit any such statement must not be
sub-split later references within the same ``to_code`` would land
in a different IIFE and fail to compile.
The detection is intentionally safety-biased: false negatives cause
silent broken C++, false positives just keep a component in one
slightly larger IIFE. Any ``cg.add(RawExpression(...))`` disables
sub-splitting for its group regardless of whether the raw text
actually references a local, because the chunker can't introspect
arbitrary raw text."""
from esphome.cpp_generator import (
AssignmentExpression,
ExpressionStatement,
RawExpression,
RawStatement,
)
# Scope braces from cg.with_local_variable() or inline scope blocks
# (e.g. time's tz pattern). Content-aware so RawStatements emitted
# for "call(); // comment" (entity_helpers) don't false-positive.
if isinstance(exp, RawStatement) and str(exp).strip() in ("{", "}"):
return True
# cg.add(RawExpression(...)) — bare raw text, e.g.
# `time::ParsedTimezone tz{}` or `tz.field = ...`. CORE.add wraps
# a passed Expression in an ExpressionStatement; when the inner is
# a RawExpression the author is emitting uninterpreted text that
# may reference a local declared elsewhere in the same block. A
# RawExpression passed as a CallExpression argument does NOT land
# here (its ExpressionStatement's .expression is the CallExpression),
# so value-pass patterns like `var.set_program(RawExpression("&foo"))`
# continue to sub-split normally.
if isinstance(exp, ExpressionStatement) and isinstance(
exp.expression, RawExpression
):
return True
# cg.variable(id, rhs) — emits ``Type id = rhs;`` as a function-local.
return (
isinstance(exp, ExpressionStatement)
and isinstance(exp.expression, AssignmentExpression)
and exp.expression.type is not None
)
def _wrap_in_iifes(lines: list[str], max_statements: int | None) -> list[str]:
"""Wrap ``lines`` in ``[]() {...}();`` IIFEs of up to ``max_statements``
each, or in a single IIFE when ``max_statements`` is ``None``. Never
splits inside a brace-balanced block (e.g. the ``{`` / ``}`` pair from
``cg.with_local_variable()``), so an IIFE may exceed the cap when a
block straddles it. Comment-only chunks pass through verbatim.
No ``noinline`` attribute GCC's inliner re-folds small chunks freely,
keeping flash small without regressing peak stack."""
out: list[str] = []
chunk: list[str] = []
depth: int = 0
# Once depth goes negative we stop trusting the brace count and
# keep everything remaining in one final IIFE. A later ``{`` could
# arithmetically bring depth back to 0, but by that point the brace
# tracking is already unreliable — re-enabling mid-stream splits
# could land between a declaration and its use.
poisoned: bool = False
def flush() -> None:
if not chunk:
return
if all(line.lstrip().startswith("//") for line in chunk):
out.extend(chunk)
else:
out.append("[]() {")
out.extend(chunk)
out.append("}();")
chunk.clear()
for line in lines:
chunk.append(line)
# Count { and } per line so inline control flow (e.g. `if (cond) {`)
# and balanced inline lambdas are tracked correctly.
depth += line.count("{") - line.count("}")
if depth < 0:
poisoned = True
if (
not poisoned
and max_statements is not None
and depth == 0
and len(chunk) >= max_statements
):
flush()
flush()
return out
# pylint: disable=too-many-public-methods
class EsphomeCore:
def __init__(self):
@@ -1123,64 +1002,15 @@ class EsphomeCore:
self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1
@property
def cpp_main_section(self) -> str:
from esphome.cpp_generator import (
ComponentMarker,
IIFEUnsafeStatement,
statement,
)
def cpp_main_section(self):
from esphome.cpp_generator import statement
# Split main_statements at ComponentMarker sentinels and wrap each
# component's group in an IIFE, sub-splitting at 50 statements so
# a single heavy component (e.g. a sensor platform with many
# filter registrations) can't blow the peak chunk frame.
#
# Two escape hatches control whether a component's group is safe
# to sub-split:
#
# - IIFEUnsafeStatement (e.g. safe_mode's setup-scope `return`):
# the whole group must stay at setup() scope so the statement
# affects setup()'s control flow, not the lambda's. Emit flat.
#
# - Any statement that may declare a function-local: a bare
# ``{`` / ``}`` RawStatement (from ``cg.with_local_variable``,
# time's inline tz block, etc.), a direct ``RawExpression``
# passed to ``cg.add`` (raw bare-local or field-assignment
# emission like ``time::ParsedTimezone tz`` followed by
# ``tz.field = ...``), or a typed ``AssignmentExpression``
# (``cg.variable`` emitting ``Type id = rhs;``). Each signals
# "this group's body may contain bare names whose scope is the
# enclosing IIFE"; wrap the whole group in one IIFE with no
# sub-split so the declaration and any later references stay
# together.
prefix: list[str] = []
components: list[_ComponentGroup] = []
current: list[str] = prefix
group: _ComponentGroup | None = None
main_code = []
for exp in self.main_statements:
if isinstance(exp, ComponentMarker):
group = _ComponentGroup()
components.append(group)
current = group.lines
continue
if group is not None:
if isinstance(exp, IIFEUnsafeStatement):
group.unsafe = True
if _emits_bare_local(exp):
group.no_split = True
current.append(str(statement(exp)).rstrip())
if not components:
return "\n".join(prefix) + "\n\n"
pieces: list[str] = list(prefix)
for g in components:
if g.unsafe:
pieces.extend(g.lines)
else:
cap = None if g.no_split else IIFE_MAX_STATEMENTS
pieces.extend(_wrap_in_iifes(g.lines, max_statements=cap))
return "\n".join(pieces) + "\n\n"
text = str(statement(exp))
text = text.rstrip()
main_code.append(text)
return "\n".join(main_code) + "\n\n"
@property
def cpp_global_section(self):
+1 -15
View File
@@ -225,21 +225,7 @@ void HOT Application::feed_wdt_slow_(uint32_t time) {
this->last_wdt_feed_ = time;
#ifdef USE_STATUS_LED
if (status_led::global_status_led != nullptr) {
auto *sl = status_led::global_status_led;
uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK;
if (sl_state == COMPONENT_STATE_LOOP_DONE) {
// status_led only transitions to LOOP_DONE from inside its own loop() (after the
// first idle-path dispatch), so its pin is already initialized by pre_setup() and
// its setup() has already run. Re-dispatch only if an error or warning bit has been
// set since; otherwise skip entirely.
if ((this->app_state_ & STATUS_LED_MASK) == 0)
return;
sl->enable_loop();
} else if (sl_state != COMPONENT_STATE_LOOP) {
// CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle.
return;
}
sl->loop();
status_led::global_status_led->call();
}
#endif
}
+336 -64
View File
@@ -39,7 +39,78 @@
#include "esphome/components/runtime_stats/runtime_stats.h"
#endif
#include "esphome/core/wake.h"
#include "esphome/core/entity_includes.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_FAN
#include "esphome/components/fan/fan.h"
#endif
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_state.h"
#endif
#ifdef USE_COVER
#include "esphome/components/cover/cover.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_DATETIME_DATE
#include "esphome/components/datetime/date_entity.h"
#endif
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_entity.h"
#endif
#ifdef USE_DATETIME_DATETIME
#include "esphome/components/datetime/datetime_entity.h"
#endif
#ifdef USE_TEXT
#include "esphome/components/text/text.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_LOCK
#include "esphome/components/lock/lock.h"
#endif
#ifdef USE_VALVE
#include "esphome/components/valve/valve.h"
#endif
#ifdef USE_MEDIA_PLAYER
#include "esphome/components/media_player/media_player.h"
#endif
#ifdef USE_ALARM_CONTROL_PANEL
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_SERIAL_PROXY
#include "esphome/components/serial_proxy/serial_proxy.h"
#endif
#ifdef USE_EVENT
#include "esphome/components/event/event.h"
#endif
#ifdef USE_UPDATE
#include "esphome/components/update/update_entity.h"
#endif
namespace esphome::socket {
#ifdef USE_HOST
@@ -119,16 +190,93 @@ class Application {
void set_current_component(Component *component) { this->current_component_ = component; }
Component *get_current_component() { return this->current_component_; }
// Entity register methods (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
void register_##singular(type *obj) { this->plural##_.push_back(obj); }
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
ENTITY_TYPE_(type, singular, plural, count, upper)
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
void register_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
this->binary_sensors_.push_back(binary_sensor);
}
#endif
#ifdef USE_SENSOR
void register_sensor(sensor::Sensor *sensor) { this->sensors_.push_back(sensor); }
#endif
#ifdef USE_SWITCH
void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); }
#endif
#ifdef USE_BUTTON
void register_button(button::Button *button) { this->buttons_.push_back(button); }
#endif
#ifdef USE_TEXT_SENSOR
void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
#endif
#ifdef USE_FAN
void register_fan(fan::Fan *state) { this->fans_.push_back(state); }
#endif
#ifdef USE_COVER
void register_cover(cover::Cover *cover) { this->covers_.push_back(cover); }
#endif
#ifdef USE_CLIMATE
void register_climate(climate::Climate *climate) { this->climates_.push_back(climate); }
#endif
#ifdef USE_LIGHT
void register_light(light::LightState *light) { this->lights_.push_back(light); }
#endif
#ifdef USE_NUMBER
void register_number(number::Number *number) { this->numbers_.push_back(number); }
#endif
#ifdef USE_DATETIME_DATE
void register_date(datetime::DateEntity *date) { this->dates_.push_back(date); }
#endif
#ifdef USE_DATETIME_TIME
void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); }
#endif
#ifdef USE_DATETIME_DATETIME
void register_datetime(datetime::DateTimeEntity *datetime) { this->datetimes_.push_back(datetime); }
#endif
#ifdef USE_TEXT
void register_text(text::Text *text) { this->texts_.push_back(text); }
#endif
#ifdef USE_SELECT
void register_select(select::Select *select) { this->selects_.push_back(select); }
#endif
#ifdef USE_LOCK
void register_lock(lock::Lock *a_lock) { this->locks_.push_back(a_lock); }
#endif
#ifdef USE_VALVE
void register_valve(valve::Valve *valve) { this->valves_.push_back(valve); }
#endif
#ifdef USE_MEDIA_PLAYER
void register_media_player(media_player::MediaPlayer *media_player) { this->media_players_.push_back(media_player); }
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void register_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
this->alarm_control_panels_.push_back(a_alarm_control_panel);
}
#endif
#ifdef USE_WATER_HEATER
void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); }
#endif
#ifdef USE_INFRARED
void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); }
#endif
#ifdef USE_SERIAL_PROXY
void register_serial_proxy(serial_proxy::SerialProxy *proxy) {
@@ -137,6 +285,14 @@ class Application {
}
#endif
#ifdef USE_EVENT
void register_event(event::Event *event) { this->events_.push_back(event); }
#endif
#ifdef USE_UPDATE
void register_update(update::UpdateEntity *update) { this->updates_.push_back(update); }
#endif
/// Reserve space for components to avoid memory fragmentation
/// Set up all the registered components. Call this at the end of your setup() function.
@@ -300,22 +456,108 @@ class Application {
#ifdef USE_AREAS
const auto &get_areas() { return this->areas_; }
#endif
// Entity getter methods (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
auto &get_##plural() const { return this->plural##_; } \
GET_ENTITY_METHOD(type, singular, plural)
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
ENTITY_TYPE_(type, singular, plural, count, upper)
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
auto &get_binary_sensors() const { return this->binary_sensors_; }
GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors)
#endif
#ifdef USE_SWITCH
auto &get_switches() const { return this->switches_; }
GET_ENTITY_METHOD(switch_::Switch, switch, switches)
#endif
#ifdef USE_BUTTON
auto &get_buttons() const { return this->buttons_; }
GET_ENTITY_METHOD(button::Button, button, buttons)
#endif
#ifdef USE_SENSOR
auto &get_sensors() const { return this->sensors_; }
GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors)
#endif
#ifdef USE_TEXT_SENSOR
auto &get_text_sensors() const { return this->text_sensors_; }
GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors)
#endif
#ifdef USE_FAN
auto &get_fans() const { return this->fans_; }
GET_ENTITY_METHOD(fan::Fan, fan, fans)
#endif
#ifdef USE_COVER
auto &get_covers() const { return this->covers_; }
GET_ENTITY_METHOD(cover::Cover, cover, covers)
#endif
#ifdef USE_LIGHT
auto &get_lights() const { return this->lights_; }
GET_ENTITY_METHOD(light::LightState, light, lights)
#endif
#ifdef USE_CLIMATE
auto &get_climates() const { return this->climates_; }
GET_ENTITY_METHOD(climate::Climate, climate, climates)
#endif
#ifdef USE_NUMBER
auto &get_numbers() const { return this->numbers_; }
GET_ENTITY_METHOD(number::Number, number, numbers)
#endif
#ifdef USE_DATETIME_DATE
auto &get_dates() const { return this->dates_; }
GET_ENTITY_METHOD(datetime::DateEntity, date, dates)
#endif
#ifdef USE_DATETIME_TIME
auto &get_times() const { return this->times_; }
GET_ENTITY_METHOD(datetime::TimeEntity, time, times)
#endif
#ifdef USE_DATETIME_DATETIME
auto &get_datetimes() const { return this->datetimes_; }
GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes)
#endif
#ifdef USE_TEXT
auto &get_texts() const { return this->texts_; }
GET_ENTITY_METHOD(text::Text, text, texts)
#endif
#ifdef USE_SELECT
auto &get_selects() const { return this->selects_; }
GET_ENTITY_METHOD(select::Select, select, selects)
#endif
#ifdef USE_LOCK
auto &get_locks() const { return this->locks_; }
GET_ENTITY_METHOD(lock::Lock, lock, locks)
#endif
#ifdef USE_VALVE
auto &get_valves() const { return this->valves_; }
GET_ENTITY_METHOD(valve::Valve, valve, valves)
#endif
#ifdef USE_MEDIA_PLAYER
auto &get_media_players() const { return this->media_players_; }
GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players)
#endif
#ifdef USE_ALARM_CONTROL_PANEL
auto &get_alarm_control_panels() const { return this->alarm_control_panels_; }
GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels)
#endif
#ifdef USE_WATER_HEATER
auto &get_water_heaters() const { return this->water_heaters_; }
GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters)
#endif
#ifdef USE_INFRARED
auto &get_infrareds() const { return this->infrareds_; }
GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds)
#endif
#ifdef USE_SERIAL_PROXY
auto &get_serial_proxies() const { return this->serial_proxies_; }
#endif
#ifdef USE_EVENT
auto &get_events() const { return this->events_; }
GET_ENTITY_METHOD(event::Event, event, events)
#endif
#ifdef USE_UPDATE
auto &get_updates() const { return this->updates_; }
GET_ENTITY_METHOD(update::UpdateEntity, update, updates)
#endif
Scheduler scheduler;
/// Register/unregister a socket to be monitored for read events.
@@ -337,12 +579,9 @@ class Application {
/// @see esphome::wake_loop_threadsafe() in wake.h for platform details.
void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); }
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
/// Wake from ISR (ESP32 and LibreTiny).
#ifdef USE_ESP32
/// Wake from ISR (ESP32 only).
static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); }
#elif defined(USE_ESP8266)
/// Wake from ISR (ESP8266). No task_woken arg — no FreeRTOS. Caller must be IRAM_ATTR.
static void IRAM_ATTR ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { esphome::wake_loop_isrsafe(); }
#endif
/// Wake from any context (ISR, thread, callback).
@@ -504,19 +743,79 @@ class Application {
#ifdef USE_AREAS
StaticVector<Area *, ESPHOME_AREA_COUNT> areas_{};
#endif
// Entity StaticVector fields (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) StaticVector<type *, count> plural##_{};
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
ENTITY_TYPE_(type, singular, plural, count, upper)
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
StaticVector<binary_sensor::BinarySensor *, ESPHOME_ENTITY_BINARY_SENSOR_COUNT> binary_sensors_{};
#endif
#ifdef USE_SWITCH
StaticVector<switch_::Switch *, ESPHOME_ENTITY_SWITCH_COUNT> switches_{};
#endif
#ifdef USE_BUTTON
StaticVector<button::Button *, ESPHOME_ENTITY_BUTTON_COUNT> buttons_{};
#endif
#ifdef USE_EVENT
StaticVector<event::Event *, ESPHOME_ENTITY_EVENT_COUNT> events_{};
#endif
#ifdef USE_SENSOR
StaticVector<sensor::Sensor *, ESPHOME_ENTITY_SENSOR_COUNT> sensors_{};
#endif
#ifdef USE_TEXT_SENSOR
StaticVector<text_sensor::TextSensor *, ESPHOME_ENTITY_TEXT_SENSOR_COUNT> text_sensors_{};
#endif
#ifdef USE_FAN
StaticVector<fan::Fan *, ESPHOME_ENTITY_FAN_COUNT> fans_{};
#endif
#ifdef USE_COVER
StaticVector<cover::Cover *, ESPHOME_ENTITY_COVER_COUNT> covers_{};
#endif
#ifdef USE_CLIMATE
StaticVector<climate::Climate *, ESPHOME_ENTITY_CLIMATE_COUNT> climates_{};
#endif
#ifdef USE_LIGHT
StaticVector<light::LightState *, ESPHOME_ENTITY_LIGHT_COUNT> lights_{};
#endif
#ifdef USE_NUMBER
StaticVector<number::Number *, ESPHOME_ENTITY_NUMBER_COUNT> numbers_{};
#endif
#ifdef USE_DATETIME_DATE
StaticVector<datetime::DateEntity *, ESPHOME_ENTITY_DATE_COUNT> dates_{};
#endif
#ifdef USE_DATETIME_TIME
StaticVector<datetime::TimeEntity *, ESPHOME_ENTITY_TIME_COUNT> times_{};
#endif
#ifdef USE_DATETIME_DATETIME
StaticVector<datetime::DateTimeEntity *, ESPHOME_ENTITY_DATETIME_COUNT> datetimes_{};
#endif
#ifdef USE_SELECT
StaticVector<select::Select *, ESPHOME_ENTITY_SELECT_COUNT> selects_{};
#endif
#ifdef USE_TEXT
StaticVector<text::Text *, ESPHOME_ENTITY_TEXT_COUNT> texts_{};
#endif
#ifdef USE_LOCK
StaticVector<lock::Lock *, ESPHOME_ENTITY_LOCK_COUNT> locks_{};
#endif
#ifdef USE_VALVE
StaticVector<valve::Valve *, ESPHOME_ENTITY_VALVE_COUNT> valves_{};
#endif
#ifdef USE_MEDIA_PLAYER
StaticVector<media_player::MediaPlayer *, ESPHOME_ENTITY_MEDIA_PLAYER_COUNT> media_players_{};
#endif
#ifdef USE_ALARM_CONTROL_PANEL
StaticVector<alarm_control_panel::AlarmControlPanel *, ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT>
alarm_control_panels_{};
#endif
#ifdef USE_WATER_HEATER
StaticVector<water_heater::WaterHeater *, ESPHOME_ENTITY_WATER_HEATER_COUNT> water_heaters_{};
#endif
#ifdef USE_INFRARED
StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{};
#endif
#ifdef USE_SERIAL_PROXY
StaticVector<serial_proxy::SerialProxy *, SERIAL_PROXY_COUNT> serial_proxies_{};
#endif
#ifdef USE_UPDATE
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
#endif
};
/// Global storage of Application pointer - only one Application can exist.
@@ -578,25 +877,10 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_
}
inline void ESPHOME_ALWAYS_INLINE Application::loop() {
#ifdef USE_RUNTIME_STATS
// Capture the start of the active (non-sleeping) portion of this iteration.
// Used to derive main-loop overhead = active time Σ(component time)
// before/tail splits recorded below.
uint32_t loop_active_start_us = micros();
// Snapshot the cumulative component-recorded time so we can subtract the
// slice that the scheduler spends inside its own WarnIfComponentBlockingGuard
// (scheduler.cpp) — that time is already counted in per-component stats,
// so charging it again to "before" would double-count.
uint64_t loop_recorded_snap = ComponentRuntimeStats::global_recorded_us;
#endif
// Get the initial loop time at the start
uint32_t last_op_end_time = millis();
this->before_loop_tasks_(last_op_end_time);
#ifdef USE_RUNTIME_STATS
uint32_t loop_before_end_us = micros();
uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap;
#endif
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
this->current_loop_index_++) {
@@ -615,24 +899,12 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
this->feed_wdt_with_time(last_op_end_time);
}
#ifdef USE_RUNTIME_STATS
uint32_t loop_tail_start_us = micros();
#endif
this->after_loop_tasks_();
#ifdef USE_RUNTIME_STATS
// Process any pending runtime stats printing after all components have run
// This ensures stats printing doesn't affect component timing measurements
if (global_runtime_stats != nullptr) {
uint32_t loop_now_us = micros();
// Subtract scheduled-component time from the "before" bucket so it is
// not double-counted (it is already attributed to per-component stats).
uint32_t loop_before_wall_us = loop_before_end_us - loop_active_start_us;
uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us
? loop_before_wall_us - static_cast<uint32_t>(loop_before_scheduled_us)
: 0;
global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us,
loop_now_us - loop_tail_start_us);
global_runtime_stats->process_pending_stats(last_op_end_time);
}
#endif
-12
View File
@@ -62,18 +62,6 @@ template<typename T, typename... X> class TemplatableFn {
!std::convertible_to<std::invoke_result_t<F, X...>, T> ||
!std::default_initializable<F>) = delete;
// Reject raw (non-callable) values with a helpful diagnostic pointing at the Python-side fix.
// TemplatableFn stores only a function pointer (4 bytes), so constants must be wrapped in a
// stateless lambda by codegen. External components hitting this error should use
// `cg.templatable(value, args, type)` in their Python __init__.py before passing to the setter.
template<typename V> TemplatableFn(V) requires(!std::invocable<V, X...>) && (!std::convertible_to<V, T (*)(X...)>) {
static_assert(sizeof(V) == 0, "Missing cg.templatable(...) in Python codegen for this TEMPLATABLE_VALUE "
"field. The wrapper was always required; it worked by accident because the old "
"TemplatableValue implicitly converted raw constants. TemplatableFn cannot. See "
"https://developers.esphome.io/blog/2026/04/09/"
"templatablefn-4-byte-templatable-storage-for-trivially-copyable-types/");
}
bool has_value() const { return this->f_ != nullptr; }
T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; }
+1 -3
View File
@@ -205,9 +205,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
} else {
// For delays with arguments, capture by value to preserve argument values
// Arguments must be copied because original references may be invalid after delay
// `mutable` is required so captured copies of non-const reference args (e.g. std::string&)
// are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&`
auto f = [this, x...]() mutable { this->play_next_(x...); };
auto f = [this, x...]() { this->play_next_(x...); };
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL,
nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION),
this->delay_.value(x...), std::move(f),
-4
View File
@@ -506,10 +506,6 @@ void PollingComponent::stop_poller() {
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
#ifdef USE_RUNTIME_STATS
uint64_t ComponentRuntimeStats::global_recorded_us = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif
void __attribute__((noinline, cold))
WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) {
bool should_warn;
-8
View File
@@ -116,13 +116,6 @@ struct ComponentRuntimeStats {
uint64_t total_time_us{0};
uint32_t total_max_time_us{0};
// Cumulative sum of every record_time() duration since boot, across all
// components. Used by Application::loop() to snapshot time spent inside
// WarnIfComponentBlockingGuard (including guards constructed by the
// scheduler at scheduler.cpp) so main-loop overhead accounting can
// subtract scheduled-callback time from the before_loop_tasks_ wall time.
static uint64_t global_recorded_us; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void record_time(uint32_t duration_us) {
this->period_count++;
this->period_time_us += duration_us;
@@ -132,7 +125,6 @@ struct ComponentRuntimeStats {
this->total_time_us += duration_us;
if (duration_us > this->total_max_time_us)
this->total_max_time_us = duration_us;
global_recorded_us += duration_us;
}
void reset_period() {
this->period_count = 0;
+140 -12
View File
@@ -33,18 +33,53 @@ void ComponentIterator::advance() {
}
break;
// Entity iterator cases (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
case IteratorState::upper: \
this->process_platform_item_(App.get_##plural(), &ComponentIterator::on_##singular); \
break;
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
ENTITY_TYPE_(type, singular, plural, count, upper)
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
case IteratorState::BINARY_SENSOR:
this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor);
break;
#endif
#ifdef USE_COVER
case IteratorState::COVER:
this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover);
break;
#endif
#ifdef USE_FAN
case IteratorState::FAN:
this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan);
break;
#endif
#ifdef USE_LIGHT
case IteratorState::LIGHT:
this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light);
break;
#endif
#ifdef USE_SENSOR
case IteratorState::SENSOR:
this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor);
break;
#endif
#ifdef USE_SWITCH
case IteratorState::SWITCH:
this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch);
break;
#endif
#ifdef USE_BUTTON
case IteratorState::BUTTON:
this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button);
break;
#endif
#ifdef USE_TEXT_SENSOR
case IteratorState::TEXT_SENSOR:
this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor);
break;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
case IteratorState::SERVICE:
@@ -62,6 +97,96 @@ void ComponentIterator::advance() {
} break;
#endif
#ifdef USE_CLIMATE
case IteratorState::CLIMATE:
this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate);
break;
#endif
#ifdef USE_NUMBER
case IteratorState::NUMBER:
this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number);
break;
#endif
#ifdef USE_DATETIME_DATE
case IteratorState::DATETIME_DATE:
this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date);
break;
#endif
#ifdef USE_DATETIME_TIME
case IteratorState::DATETIME_TIME:
this->process_platform_item_(App.get_times(), &ComponentIterator::on_time);
break;
#endif
#ifdef USE_DATETIME_DATETIME
case IteratorState::DATETIME_DATETIME:
this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime);
break;
#endif
#ifdef USE_TEXT
case IteratorState::TEXT:
this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text);
break;
#endif
#ifdef USE_SELECT
case IteratorState::SELECT:
this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select);
break;
#endif
#ifdef USE_LOCK
case IteratorState::LOCK:
this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock);
break;
#endif
#ifdef USE_VALVE
case IteratorState::VALVE:
this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve);
break;
#endif
#ifdef USE_MEDIA_PLAYER
case IteratorState::MEDIA_PLAYER:
this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player);
break;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
case IteratorState::ALARM_CONTROL_PANEL:
this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel);
break;
#endif
#ifdef USE_WATER_HEATER
case IteratorState::WATER_HEATER:
this->process_platform_item_(App.get_water_heaters(), &ComponentIterator::on_water_heater);
break;
#endif
#ifdef USE_INFRARED
case IteratorState::INFRARED:
this->process_platform_item_(App.get_infrareds(), &ComponentIterator::on_infrared);
break;
#endif
#ifdef USE_EVENT
case IteratorState::EVENT:
this->process_platform_item_(App.get_events(), &ComponentIterator::on_event);
break;
#endif
#ifdef USE_UPDATE
case IteratorState::UPDATE:
this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update);
break;
#endif
case IteratorState::MAX:
if (this->on_end()) {
this->state_ = IteratorState::NONE;
@@ -78,4 +203,7 @@ bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return
#ifdef USE_CAMERA
bool ComponentIterator::on_camera(camera::Camera *camera) { return true; }
#endif
#ifdef USE_MEDIA_PLAYER
bool ComponentIterator::on_media_player(media_player::MediaPlayer *media_player) { return true; }
#endif
} // namespace esphome
+138 -18
View File
@@ -28,21 +28,80 @@ class ComponentIterator {
void advance();
bool completed() const { return this->state_ == IteratorState::NONE; }
virtual bool on_begin();
// Pure virtual entity callbacks (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) virtual bool on_##singular(type *obj) = 0;
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
ENTITY_TYPE_(type, singular, plural, count, upper)
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
// Non-entity and non-pure-virtual callbacks (have default implementations)
#ifdef USE_BINARY_SENSOR
virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0;
#endif
#ifdef USE_COVER
virtual bool on_cover(cover::Cover *cover) = 0;
#endif
#ifdef USE_FAN
virtual bool on_fan(fan::Fan *fan) = 0;
#endif
#ifdef USE_LIGHT
virtual bool on_light(light::LightState *light) = 0;
#endif
#ifdef USE_SENSOR
virtual bool on_sensor(sensor::Sensor *sensor) = 0;
#endif
#ifdef USE_SWITCH
virtual bool on_switch(switch_::Switch *a_switch) = 0;
#endif
#ifdef USE_BUTTON
virtual bool on_button(button::Button *button) = 0;
#endif
#ifdef USE_TEXT_SENSOR
virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
virtual bool on_service(api::UserServiceDescriptor *service);
#endif
#ifdef USE_CAMERA
virtual bool on_camera(camera::Camera *camera);
#endif
#ifdef USE_CLIMATE
virtual bool on_climate(climate::Climate *climate) = 0;
#endif
#ifdef USE_NUMBER
virtual bool on_number(number::Number *number) = 0;
#endif
#ifdef USE_DATETIME_DATE
virtual bool on_date(datetime::DateEntity *date) = 0;
#endif
#ifdef USE_DATETIME_TIME
virtual bool on_time(datetime::TimeEntity *time) = 0;
#endif
#ifdef USE_DATETIME_DATETIME
virtual bool on_datetime(datetime::DateTimeEntity *datetime) = 0;
#endif
#ifdef USE_TEXT
virtual bool on_text(text::Text *text) = 0;
#endif
#ifdef USE_SELECT
virtual bool on_select(select::Select *select) = 0;
#endif
#ifdef USE_LOCK
virtual bool on_lock(lock::Lock *a_lock) = 0;
#endif
#ifdef USE_VALVE
virtual bool on_valve(valve::Valve *valve) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual bool on_media_player(media_player::MediaPlayer *media_player);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) = 0;
#endif
#ifdef USE_WATER_HEATER
virtual bool on_water_heater(water_heater::WaterHeater *water_heater) = 0;
#endif
#ifdef USE_INFRARED
virtual bool on_infrared(infrared::Infrared *infrared) = 0;
#endif
#ifdef USE_EVENT
virtual bool on_event(event::Event *event) = 0;
#endif
#ifdef USE_UPDATE
virtual bool on_update(update::UpdateEntity *update) = 0;
#endif
virtual bool on_end();
@@ -52,19 +111,80 @@ class ComponentIterator {
enum class IteratorState : uint8_t {
NONE = 0,
BEGIN,
// Entity iterator states (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) upper,
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) upper,
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
BINARY_SENSOR,
#endif
#ifdef USE_COVER
COVER,
#endif
#ifdef USE_FAN
FAN,
#endif
#ifdef USE_LIGHT
LIGHT,
#endif
#ifdef USE_SENSOR
SENSOR,
#endif
#ifdef USE_SWITCH
SWITCH,
#endif
#ifdef USE_BUTTON
BUTTON,
#endif
#ifdef USE_TEXT_SENSOR
TEXT_SENSOR,
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
SERVICE,
#endif
#ifdef USE_CAMERA
CAMERA,
#endif
#ifdef USE_CLIMATE
CLIMATE,
#endif
#ifdef USE_NUMBER
NUMBER,
#endif
#ifdef USE_DATETIME_DATE
DATETIME_DATE,
#endif
#ifdef USE_DATETIME_TIME
DATETIME_TIME,
#endif
#ifdef USE_DATETIME_DATETIME
DATETIME_DATETIME,
#endif
#ifdef USE_TEXT
TEXT,
#endif
#ifdef USE_SELECT
SELECT,
#endif
#ifdef USE_LOCK
LOCK,
#endif
#ifdef USE_VALVE
VALVE,
#endif
#ifdef USE_MEDIA_PLAYER
MEDIA_PLAYER,
#endif
#ifdef USE_ALARM_CONTROL_PANEL
ALARM_CONTROL_PANEL,
#endif
#ifdef USE_WATER_HEATER
WATER_HEATER,
#endif
#ifdef USE_INFRARED
INFRARED,
#endif
#ifdef USE_EVENT
EVENT,
#endif
#ifdef USE_UPDATE
UPDATE,
#endif
MAX,
};
+130 -9
View File
@@ -1,19 +1,140 @@
#pragma once
#include "esphome/core/entity_includes.h"
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_FAN
#include "esphome/components/fan/fan.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_state.h"
#endif
#ifdef USE_COVER
#include "esphome/components/cover/cover.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_DATETIME_DATE
#include "esphome/components/datetime/date_entity.h"
#endif
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_entity.h"
#endif
#ifdef USE_DATETIME_DATETIME
#include "esphome/components/datetime/datetime_entity.h"
#endif
#ifdef USE_TEXT
#include "esphome/components/text/text.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_LOCK
#include "esphome/components/lock/lock.h"
#endif
#ifdef USE_VALVE
#include "esphome/components/valve/valve.h"
#endif
#ifdef USE_MEDIA_PLAYER
#include "esphome/components/media_player/media_player.h"
#endif
#ifdef USE_ALARM_CONTROL_PANEL
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_EVENT
#include "esphome/components/event/event.h"
#endif
#ifdef USE_UPDATE
#include "esphome/components/update/update_entity.h"
#endif
namespace esphome {
class Controller {
public:
// Controller virtual methods (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) virtual void on_##callback(type *obj){};
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){};
#endif
#ifdef USE_FAN
virtual void on_fan_update(fan::Fan *obj){};
#endif
#ifdef USE_LIGHT
virtual void on_light_update(light::LightState *obj){};
#endif
#ifdef USE_SENSOR
virtual void on_sensor_update(sensor::Sensor *obj){};
#endif
#ifdef USE_SWITCH
virtual void on_switch_update(switch_::Switch *obj){};
#endif
#ifdef USE_COVER
virtual void on_cover_update(cover::Cover *obj){};
#endif
#ifdef USE_TEXT_SENSOR
virtual void on_text_sensor_update(text_sensor::TextSensor *obj){};
#endif
#ifdef USE_CLIMATE
virtual void on_climate_update(climate::Climate *obj){};
#endif
#ifdef USE_NUMBER
virtual void on_number_update(number::Number *obj){};
#endif
#ifdef USE_DATETIME_DATE
virtual void on_date_update(datetime::DateEntity *obj){};
#endif
#ifdef USE_DATETIME_TIME
virtual void on_time_update(datetime::TimeEntity *obj){};
#endif
#ifdef USE_DATETIME_DATETIME
virtual void on_datetime_update(datetime::DateTimeEntity *obj){};
#endif
#ifdef USE_TEXT
virtual void on_text_update(text::Text *obj){};
#endif
#ifdef USE_SELECT
virtual void on_select_update(select::Select *obj){};
#endif
#ifdef USE_LOCK
virtual void on_lock_update(lock::Lock *obj){};
#endif
#ifdef USE_VALVE
virtual void on_valve_update(valve::Valve *obj){};
#endif
#ifdef USE_MEDIA_PLAYER
virtual void on_media_player_update(media_player::MediaPlayer *obj){};
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){};
#endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_update(water_heater::WaterHeater *obj){};
#endif
#ifdef USE_EVENT
virtual void on_event(event::Event *obj){};
#endif
#ifdef USE_UPDATE
virtual void on_update(update::UpdateEntity *obj){};
#endif
};
} // namespace esphome
+2
View File
@@ -6,6 +6,8 @@ namespace esphome {
StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controllers;
void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); }
} // namespace esphome
#endif // USE_CONTROLLER_REGISTRY
+308 -18
View File
@@ -4,13 +4,139 @@
#ifdef USE_CONTROLLER_REGISTRY
#include "esphome/core/entity_includes.h"
#include "esphome/core/helpers.h"
// Forward declarations
namespace esphome {
class Controller;
#ifdef USE_BINARY_SENSOR
namespace binary_sensor {
class BinarySensor;
}
#endif
#ifdef USE_FAN
namespace fan {
class Fan;
}
#endif
#ifdef USE_LIGHT
namespace light {
class LightState;
}
#endif
#ifdef USE_SENSOR
namespace sensor {
class Sensor;
}
#endif
#ifdef USE_SWITCH
namespace switch_ {
class Switch;
}
#endif
#ifdef USE_COVER
namespace cover {
class Cover;
}
#endif
#ifdef USE_TEXT_SENSOR
namespace text_sensor {
class TextSensor;
}
#endif
#ifdef USE_CLIMATE
namespace climate {
class Climate;
}
#endif
#ifdef USE_NUMBER
namespace number {
class Number;
}
#endif
#ifdef USE_DATETIME_DATE
namespace datetime {
class DateEntity;
}
#endif
#ifdef USE_DATETIME_TIME
namespace datetime {
class TimeEntity;
}
#endif
#ifdef USE_DATETIME_DATETIME
namespace datetime {
class DateTimeEntity;
}
#endif
#ifdef USE_TEXT
namespace text {
class Text;
}
#endif
#ifdef USE_SELECT
namespace select {
class Select;
}
#endif
#ifdef USE_LOCK
namespace lock {
class Lock;
}
#endif
#ifdef USE_VALVE
namespace valve {
class Valve;
}
#endif
#ifdef USE_MEDIA_PLAYER
namespace media_player {
class MediaPlayer;
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
namespace alarm_control_panel {
class AlarmControlPanel;
}
#endif
#ifdef USE_WATER_HEATER
namespace water_heater {
class WaterHeater;
}
#endif
#ifdef USE_EVENT
namespace event {
class Event;
}
#endif
#ifdef USE_UPDATE
namespace update {
class UpdateEntity;
}
#endif
/** Global registry for Controllers to receive entity state updates.
*
* This singleton registry allows Controllers (APIServer, WebServer) to receive
@@ -34,17 +160,91 @@ class ControllerRegistry {
* Controllers should call this in their setup() method.
* Typically only APIServer and WebServer register.
*/
static void register_controller(Controller *controller) { controllers.push_back(controller); }
static void register_controller(Controller *controller);
// Notify method declarations (generated from entity_types.h)
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
static void notify_##callback(type *obj);
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
static void notify_binary_sensor_update(binary_sensor::BinarySensor *obj);
#endif
#ifdef USE_FAN
static void notify_fan_update(fan::Fan *obj);
#endif
#ifdef USE_LIGHT
static void notify_light_update(light::LightState *obj);
#endif
#ifdef USE_SENSOR
static void notify_sensor_update(sensor::Sensor *obj);
#endif
#ifdef USE_SWITCH
static void notify_switch_update(switch_::Switch *obj);
#endif
#ifdef USE_COVER
static void notify_cover_update(cover::Cover *obj);
#endif
#ifdef USE_TEXT_SENSOR
static void notify_text_sensor_update(text_sensor::TextSensor *obj);
#endif
#ifdef USE_CLIMATE
static void notify_climate_update(climate::Climate *obj);
#endif
#ifdef USE_NUMBER
static void notify_number_update(number::Number *obj);
#endif
#ifdef USE_DATETIME_DATE
static void notify_date_update(datetime::DateEntity *obj);
#endif
#ifdef USE_DATETIME_TIME
static void notify_time_update(datetime::TimeEntity *obj);
#endif
#ifdef USE_DATETIME_DATETIME
static void notify_datetime_update(datetime::DateTimeEntity *obj);
#endif
#ifdef USE_TEXT
static void notify_text_update(text::Text *obj);
#endif
#ifdef USE_SELECT
static void notify_select_update(select::Select *obj);
#endif
#ifdef USE_LOCK
static void notify_lock_update(lock::Lock *obj);
#endif
#ifdef USE_VALVE
static void notify_valve_update(valve::Valve *obj);
#endif
#ifdef USE_MEDIA_PLAYER
static void notify_media_player_update(media_player::MediaPlayer *obj);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj);
#endif
#ifdef USE_WATER_HEATER
static void notify_water_heater_update(water_heater::WaterHeater *obj);
#endif
#ifdef USE_EVENT
static void notify_event(event::Event *obj);
#endif
#ifdef USE_UPDATE
static void notify_update(update::UpdateEntity *obj);
#endif
protected:
static StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> controllers;
@@ -65,18 +265,108 @@ namespace esphome {
// notify_frontend_(), eliminating an unnecessary function-call frame.
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
inline void ControllerRegistry::notify_##callback(type *obj) { \
#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \
inline void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \
for (auto *controller : controllers) { \
controller->on_##callback(obj); \
controller->on_##entity_name##_update(obj); \
} \
}
#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \
inline void ControllerRegistry::notify_##entity_name(entity_type *obj) { \
for (auto *controller : controllers) { \
controller->on_##entity_name(obj); \
} \
}
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor)
#endif
#ifdef USE_FAN
CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan)
#endif
#ifdef USE_LIGHT
CONTROLLER_REGISTRY_NOTIFY(light::LightState, light)
#endif
#ifdef USE_SENSOR
CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor)
#endif
#ifdef USE_SWITCH
CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch)
#endif
#ifdef USE_COVER
CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover)
#endif
#ifdef USE_TEXT_SENSOR
CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor)
#endif
#ifdef USE_CLIMATE
CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate)
#endif
#ifdef USE_NUMBER
CONTROLLER_REGISTRY_NOTIFY(number::Number, number)
#endif
#ifdef USE_DATETIME_DATE
CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date)
#endif
#ifdef USE_DATETIME_TIME
CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time)
#endif
#ifdef USE_DATETIME_DATETIME
CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime)
#endif
#ifdef USE_TEXT
CONTROLLER_REGISTRY_NOTIFY(text::Text, text)
#endif
#ifdef USE_SELECT
CONTROLLER_REGISTRY_NOTIFY(select::Select, select)
#endif
#ifdef USE_LOCK
CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock)
#endif
#ifdef USE_VALVE
CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve)
#endif
#ifdef USE_MEDIA_PLAYER
CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_ALARM_CONTROL_PANEL
CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
#endif
#ifdef USE_WATER_HEATER
CONTROLLER_REGISTRY_NOTIFY(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event)
#endif
#ifdef USE_UPDATE
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update)
#endif
#undef CONTROLLER_REGISTRY_NOTIFY
#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX
} // namespace esphome
#endif // USE_CONTROLLER_REGISTRY
-79
View File
@@ -1,79 +0,0 @@
#pragma once
// Shared entity component includes.
// Conditionally includes headers for all entity types based on USE_* defines.
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_COVER
#include "esphome/components/cover/cover.h"
#endif
#ifdef USE_FAN
#include "esphome/components/fan/fan.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_state.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_DATETIME_DATE
#include "esphome/components/datetime/date_entity.h"
#endif
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_entity.h"
#endif
#ifdef USE_DATETIME_DATETIME
#include "esphome/components/datetime/datetime_entity.h"
#endif
#ifdef USE_TEXT
#include "esphome/components/text/text.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_LOCK
#include "esphome/components/lock/lock.h"
#endif
#ifdef USE_VALVE
#include "esphome/components/valve/valve.h"
#endif
#ifdef USE_MEDIA_PLAYER
#include "esphome/components/media_player/media_player.h"
#endif
#ifdef USE_ALARM_CONTROL_PANEL
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_SERIAL_PROXY
#include "esphome/components/serial_proxy/serial_proxy.h"
#endif
#ifdef USE_EVENT
#include "esphome/components/event/event.h"
#endif
#ifdef USE_UPDATE
#include "esphome/components/update/update_entity.h"
#endif
-98
View File
@@ -1,98 +0,0 @@
// X-macro include file for entity type declarations.
// This file is included multiple times with different macro definitions.
//
// Both macros must be defined before including this file:
//
// ENTITY_TYPE_(type, singular, plural, count, upper)
// — entities without controller callbacks (button, infrared)
//
// ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback)
// — entities with controller callbacks
//
// Excluded from this list (handled manually):
// - devices/areas: not entities
// - serial_proxy: custom register logic, no by-key lookup
#ifndef ENTITY_TYPE_
#error "ENTITY_TYPE_(type, singular, plural, count, upper) must be defined before including entity_types.h"
#endif
#ifndef ENTITY_CONTROLLER_TYPE_
#error \
"ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) must be defined before including entity_types.h"
#endif
#ifdef USE_BINARY_SENSOR
ENTITY_CONTROLLER_TYPE_(binary_sensor::BinarySensor, binary_sensor, binary_sensors, ESPHOME_ENTITY_BINARY_SENSOR_COUNT,
BINARY_SENSOR, binary_sensor_update)
#endif
#ifdef USE_COVER
ENTITY_CONTROLLER_TYPE_(cover::Cover, cover, covers, ESPHOME_ENTITY_COVER_COUNT, COVER, cover_update)
#endif
#ifdef USE_FAN
ENTITY_CONTROLLER_TYPE_(fan::Fan, fan, fans, ESPHOME_ENTITY_FAN_COUNT, FAN, fan_update)
#endif
#ifdef USE_LIGHT
ENTITY_CONTROLLER_TYPE_(light::LightState, light, lights, ESPHOME_ENTITY_LIGHT_COUNT, LIGHT, light_update)
#endif
#ifdef USE_SENSOR
ENTITY_CONTROLLER_TYPE_(sensor::Sensor, sensor, sensors, ESPHOME_ENTITY_SENSOR_COUNT, SENSOR, sensor_update)
#endif
#ifdef USE_SWITCH
ENTITY_CONTROLLER_TYPE_(switch_::Switch, switch, switches, ESPHOME_ENTITY_SWITCH_COUNT, SWITCH, switch_update)
#endif
#ifdef USE_BUTTON
ENTITY_TYPE_(button::Button, button, buttons, ESPHOME_ENTITY_BUTTON_COUNT, BUTTON)
#endif
#ifdef USE_TEXT_SENSOR
ENTITY_CONTROLLER_TYPE_(text_sensor::TextSensor, text_sensor, text_sensors, ESPHOME_ENTITY_TEXT_SENSOR_COUNT,
TEXT_SENSOR, text_sensor_update)
#endif
#ifdef USE_CLIMATE
ENTITY_CONTROLLER_TYPE_(climate::Climate, climate, climates, ESPHOME_ENTITY_CLIMATE_COUNT, CLIMATE, climate_update)
#endif
#ifdef USE_NUMBER
ENTITY_CONTROLLER_TYPE_(number::Number, number, numbers, ESPHOME_ENTITY_NUMBER_COUNT, NUMBER, number_update)
#endif
#ifdef USE_DATETIME_DATE
ENTITY_CONTROLLER_TYPE_(datetime::DateEntity, date, dates, ESPHOME_ENTITY_DATE_COUNT, DATETIME_DATE, date_update)
#endif
#ifdef USE_DATETIME_TIME
ENTITY_CONTROLLER_TYPE_(datetime::TimeEntity, time, times, ESPHOME_ENTITY_TIME_COUNT, DATETIME_TIME, time_update)
#endif
#ifdef USE_DATETIME_DATETIME
ENTITY_CONTROLLER_TYPE_(datetime::DateTimeEntity, datetime, datetimes, ESPHOME_ENTITY_DATETIME_COUNT, DATETIME_DATETIME,
datetime_update)
#endif
#ifdef USE_TEXT
ENTITY_CONTROLLER_TYPE_(text::Text, text, texts, ESPHOME_ENTITY_TEXT_COUNT, TEXT, text_update)
#endif
#ifdef USE_SELECT
ENTITY_CONTROLLER_TYPE_(select::Select, select, selects, ESPHOME_ENTITY_SELECT_COUNT, SELECT, select_update)
#endif
#ifdef USE_LOCK
ENTITY_CONTROLLER_TYPE_(lock::Lock, lock, locks, ESPHOME_ENTITY_LOCK_COUNT, LOCK, lock_update)
#endif
#ifdef USE_VALVE
ENTITY_CONTROLLER_TYPE_(valve::Valve, valve, valves, ESPHOME_ENTITY_VALVE_COUNT, VALVE, valve_update)
#endif
#ifdef USE_MEDIA_PLAYER
ENTITY_CONTROLLER_TYPE_(media_player::MediaPlayer, media_player, media_players, ESPHOME_ENTITY_MEDIA_PLAYER_COUNT,
MEDIA_PLAYER, media_player_update)
#endif
#ifdef USE_ALARM_CONTROL_PANEL
ENTITY_CONTROLLER_TYPE_(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels,
ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT, ALARM_CONTROL_PANEL, alarm_control_panel_update)
#endif
#ifdef USE_WATER_HEATER
ENTITY_CONTROLLER_TYPE_(water_heater::WaterHeater, water_heater, water_heaters, ESPHOME_ENTITY_WATER_HEATER_COUNT,
WATER_HEATER, water_heater_update)
#endif
#ifdef USE_INFRARED
ENTITY_TYPE_(infrared::Infrared, infrared, infrareds, ESPHOME_ENTITY_INFRARED_COUNT, INFRARED)
#endif
#ifdef USE_EVENT
ENTITY_CONTROLLER_TYPE_(event::Event, event, events, ESPHOME_ENTITY_EVENT_COUNT, EVENT, event)
#endif
#ifdef USE_UPDATE
ENTITY_CONTROLLER_TYPE_(update::UpdateEntity, update, updates, ESPHOME_ENTITY_UPDATE_COUNT, UPDATE, update)
#endif
-72
View File
@@ -21,35 +21,6 @@
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
#define PROGMEM
#elif defined(USE_LIBRETINY)
// IRAM_ATTR places a function in executable RAM so it is callable from an
// ISR even while flash is busy (XIP stall, OTA, logger flash write).
// Each family uses a section its stock linker already routes to RAM:
// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the
// exception: its stock linker has no matching glob, so patch_linker.py
// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link.
//
// BK72xx (all variants) are left as a no-op: their SDK wraps flash
// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for
// the duration of every write, so no ISR fires while flash is stalled and
// the race IRAM_ATTR guards against cannot occur. The trade-off is that
// interrupts are delayed (not dropped) by up to ~20 ms during a sector
// erase, but that is an SDK-level choice and cannot be changed from this
// layer.
#if defined(USE_BK72XX)
#define IRAM_ATTR
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM).
#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text")))
#else
// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text.
// LN882H: patch_linker.py.script injects *(.sram.text*) into
// .flash_copysection (> RAM0 AT> FLASH).
#define IRAM_ATTR __attribute__((noinline, section(".sram.text")))
#endif
#define PROGMEM
#else
#define IRAM_ATTR
@@ -57,51 +28,8 @@
#endif
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#endif
#ifdef USE_BK72XX
// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so
// it is callable from Thumb code via interworking. The MRS CPSR instruction
// is ARM-only and user code here may be built in Thumb, so in_isr_context()
// defers to this port helper on BK72xx instead of reading CPSR inline.
extern "C" uint32_t platform_is_in_interrupt_context(void);
#endif
namespace esphome {
/// Returns true when executing inside an interrupt handler.
/// always_inline so callers placed in IRAM keep the detection in IRAM.
__attribute__((always_inline)) inline bool in_isr_context() {
#if defined(USE_ESP32)
return xPortInIsrContext() != 0;
#elif defined(USE_ESP8266)
// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
// non-zero both in a real ISR and when user code masks interrupts. The
// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule
// which is ISR-safe) so this helper is unused on this platform.
return false;
#elif defined(USE_RP2040)
uint32_t ipsr;
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
return ipsr != 0;
#elif defined(USE_BK72XX)
// BK72xx is ARM968E-S (ARM9); see extern declaration above.
return platform_is_in_interrupt_context() != 0;
#elif defined(USE_LIBRETINY)
// Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number;
// non-zero means we're in a handler.
uint32_t ipsr;
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
return ipsr != 0;
#else
// Host and any future platform without an ISR concept.
return false;
#endif
}
void yield();
uint32_t millis();
uint64_t millis_64();
-18
View File
@@ -157,17 +157,6 @@ _Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVEN
// Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task.
static netconn_callback s_original_callback = NULL;
#ifdef USE_OTA_PLATFORM_ESPHOME
static struct netconn *s_ota_listener_conn = NULL;
extern void esphome_wake_ota_component_any_context(void);
void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock) {
s_ota_listener_conn = (sock != NULL) ? sock->conn : NULL;
}
#else
void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock) { (void) sock; }
#endif
// Wrapper callback: calls original event_callback + notifies main loop task.
// Called from LwIP's TCP/IP thread when socket events occur (task context, not ISR).
static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt evt, u16_t len) {
@@ -182,13 +171,6 @@ static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt
// (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions
// already wake the main loop through the RCVPLUS path.
if (evt == NETCONN_EVT_RCVPLUS) {
#ifdef USE_OTA_PLATFORM_ESPHOME
// Mark OTA pending-enable only for events on its listen socket. MUST happen
// before xTaskNotifyGive so the flags are visible when the main task wakes.
if (conn == s_ota_listener_conn) {
esphome_wake_ota_component_any_context();
}
#endif
TaskHandle_t task = esphome_main_task_handle;
if (task != NULL) {
xTaskNotifyGive(task);
-6
View File
@@ -53,12 +53,6 @@ static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) {
/// The sock pointer must have been obtained from esphome_lwip_get_sock().
void esphome_lwip_hook_socket(struct lwip_sock *sock);
/// Set the listener netconn that the fast-select callback filters OTA wakes against.
/// After this is called, the OTA wake hook only fires for RCVPLUS events whose `conn`
/// matches this listener. Passing NULL disables OTA wakes (no event matches a NULL
/// listener) — correct behavior before install and after teardown.
void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock);
/// Set or clear TCP_NODELAY on a socket's tcp_pcb directly.
/// Must be called with the TCPIP core lock held (LwIPLock in C++).
/// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade,
+15 -4
View File
@@ -20,8 +20,7 @@ extern "C" {
extern TaskHandle_t esphome_main_task_handle;
/// Wake the main loop task from another FreeRTOS task. NOT ISR-safe.
/// always_inline so callers placed in IRAM do not reference a flash-resident copy.
__attribute__((always_inline)) static inline void esphome_main_task_notify() {
static inline void esphome_main_task_notify() {
TaskHandle_t task = esphome_main_task_handle;
if (task != NULL) {
xTaskNotifyGive(task);
@@ -29,14 +28,26 @@ __attribute__((always_inline)) static inline void esphome_main_task_notify() {
}
/// Wake the main loop task from an ISR. ISR-safe.
__attribute__((always_inline)) static inline void esphome_main_task_notify_from_isr(
BaseType_t *px_higher_priority_task_woken) {
static inline void esphome_main_task_notify_from_isr(BaseType_t *px_higher_priority_task_woken) {
TaskHandle_t task = esphome_main_task_handle;
if (task != NULL) {
vTaskNotifyGiveFromISR(task, px_higher_priority_task_woken);
}
}
#ifdef USE_ESP32
/// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext).
static inline void esphome_main_task_notify_any_context() {
if (xPortInIsrContext()) {
int px_higher_priority_task_woken = 0;
esphome_main_task_notify_from_isr(&px_higher_priority_task_woken);
portYIELD_FROM_ISR(px_higher_priority_task_woken);
} else {
esphome_main_task_notify();
}
}
#endif
#ifdef __cplusplus
}
#endif
-13
View File
@@ -144,19 +144,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
return;
}
// An interval of 0 means "fire every tick forever," which is misuse: the
// item would always be due, causing Scheduler::call() to spin and starve
// the main loop (WDT reset in the field). Coerce to 1ms so existing code
// using update_interval=0ms as a pseudo-loop() continues to work at ~1kHz,
// and warn so authors can migrate to HighFrequencyLoopRequester which is
// the intended mechanism for running fast in the main loop. Zero-delay
// timeouts (defer) remain legitimate one-shots and are not affected.
if (type == SchedulerItem::INTERVAL && delay == 0) [[unlikely]] {
ESP_LOGE(TAG, "[%s] set_interval(0) would spin main loop - coercing to 1ms (use HighFrequencyLoopRequester)",
component ? LOG_STR_ARG(component->get_component_log_str()) : LOG_STR_LITERAL("?"));
delay = 1;
}
// Take lock early to protect scheduler_item_pool_ access and retry-cancelled check
LockGuard guard{this->lock_};
+1 -1
View File
@@ -286,7 +286,7 @@ class Scheduler {
// Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now.
// On platforms with native 64-bit time, ignores now and uses millis_64() directly.
// On other platforms, extends now to 64-bit using rollover tracking.
uint64_t ESPHOME_ALWAYS_INLINE millis_64_from_(uint32_t now) {
uint64_t millis_64_from_(uint32_t now) {
#ifdef USE_NATIVE_64BIT_TIME
(void) now;
return millis_64();
+2 -6
View File
@@ -76,12 +76,8 @@ struct ESPTime {
/// @copydoc strftime(const std::string &format)
std::string strftime(const char *format);
/// Check if this ESPTime is valid (year >= 2019 and the requested fields are in range).
/// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields)
/// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields)
bool is_valid(bool check_day_of_week = true, bool check_day_of_year = true) const {
return this->year >= 2019 && this->fields_in_range(check_day_of_week, check_day_of_year);
}
/// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019)
bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); }
/// Check if time fields are in range.
/// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields)
+30 -12
View File
@@ -20,12 +20,6 @@ namespace esphome {
static const char *const TAG = "time_64";
#endif
#ifdef ESPHOME_THREAD_SINGLE
// Storage for Millis64Impl inline compute() — defined here so all TUs share one copy.
uint32_t Millis64Impl::last_millis_{0};
uint16_t Millis64Impl::millis_major_{0};
#else
uint64_t Millis64Impl::compute(uint32_t now) {
// Half the 32-bit range - used to detect rollovers vs normal time progression
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
@@ -50,25 +44,51 @@ uint64_t Millis64Impl::compute(uint32_t now) {
* to last_millis is provided by its release store and the corresponding acquire loads.
*/
static std::atomic<uint16_t> millis_major{0};
#else /* ESPHOME_THREAD_MULTI_NO_ATOMICS */
#elif !defined(ESPHOME_THREAD_SINGLE) /* ESPHOME_THREAD_MULTI_NO_ATOMICS */
static Mutex lock;
static uint32_t last_millis{0};
static uint16_t millis_major{0};
#else /* ESPHOME_THREAD_SINGLE */
static uint32_t last_millis{0};
static uint16_t millis_major{0};
#endif
// THREAD SAFETY NOTE:
// This function has two out-of-line implementations, based on the preprocessor flags:
// This function has three implementations, based on the precompiler flags
// - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, etc.)
// - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx)
// - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (LibreTiny RTL87xx/LN882x, etc.)
//
// The ESPHOME_THREAD_SINGLE path is inlined in time_64.h.
// Make sure all changes are synchronized if you edit this function.
//
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
// handles out-of-order timestamps between threads, but minimizing time differences
// helps maintain accuracy.
#if defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
#ifdef ESPHOME_THREAD_SINGLE
// Single-core platforms have no concurrency, so this is a simple implementation
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
uint16_t major = millis_major;
uint32_t last = last_millis;
// Check for rollover
if (now < last && (last - now) > HALF_MAX_UINT32) {
millis_major++;
major++;
last_millis = now;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
} else if (now > last) {
// Only update if time moved forward
last_millis = now;
}
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
// Without atomics, this implementation uses locks more aggressively:
// 1. Always locks when near the rollover boundary (within 10 seconds)
// 2. Always locks when detecting a large backwards jump
@@ -182,8 +202,6 @@ uint64_t Millis64Impl::compute(uint32_t now) {
#endif
}
#endif // !ESPHOME_THREAD_SINGLE
} // namespace esphome
#endif // !USE_NATIVE_64BIT_TIME
-32
View File
@@ -4,9 +4,6 @@
#ifndef USE_NATIVE_64BIT_TIME
#include <cstdint>
#include <limits>
#include "esphome/core/helpers.h"
namespace esphome {
@@ -19,36 +16,7 @@ class Millis64Impl {
friend uint64_t millis_64();
friend class Scheduler;
#ifdef ESPHOME_THREAD_SINGLE
// Storage defined in time_64.cpp — declared here so the inline body can access them.
static uint32_t last_millis_;
static uint16_t millis_major_;
static inline uint64_t ESPHOME_ALWAYS_INLINE compute(uint32_t now) {
// Half the 32-bit range - used to detect rollovers vs normal time progression
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
// Single-core platforms have no concurrency, so this is a simple implementation
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
uint16_t major = millis_major_;
uint32_t last = last_millis_;
// Check for rollover
if (now < last && (last - now) > HALF_MAX_UINT32) {
millis_major_++;
major++;
last_millis_ = now;
} else if (now > last) {
// Only update if time moved forward
last_millis_ = now;
}
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
}
#else
static uint64_t compute(uint32_t now);
#endif
};
} // namespace esphome
+3 -3
View File
@@ -12,12 +12,12 @@
namespace esphome {
// === ESP32 / LibreTiny — IRAM_ATTR entry points ===
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// === ESP32 — IRAM_ATTR entry points ===
#ifdef USE_ESP32
void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) {
esphome_main_task_notify_from_isr(px_higher_priority_task_woken);
}
void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); }
void IRAM_ATTR wake_loop_any_context() { esphome_main_task_notify_any_context(); }
#endif
// === ESP8266 / RP2040 ===
+9 -22
View File
@@ -28,27 +28,17 @@ extern volatile bool g_main_loop_woke;
// === ESP32 / LibreTiny (FreeRTOS) ===
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
/// Wake the main loop from any context (ISR or task).
/// always_inline so callers placed in IRAM keep the whole wake path in IRAM.
__attribute__((always_inline)) inline void wake_main_task_any_context() {
if (in_isr_context()) {
BaseType_t px_higher_priority_task_woken = pdFALSE;
esphome_main_task_notify_from_isr(&px_higher_priority_task_woken);
#ifdef portYIELD_FROM_ISR
portYIELD_FROM_ISR(px_higher_priority_task_woken);
#else
// ARM9 FreeRTOS port (BK72xx) does not define portYIELD_FROM_ISR; the IRQ
// exit sequence performs the context switch if one was requested.
(void) px_higher_priority_task_woken;
#endif
} else {
esphome_main_task_notify();
}
}
/// IRAM_ATTR entry points — defined in wake.cpp.
#ifdef USE_ESP32
/// IRAM_ATTR entry point — defined in wake.cpp.
void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken);
/// IRAM_ATTR entry point — defined in wake.cpp.
void wake_loop_any_context();
#else
/// LibreTiny: IRAM_ATTR is not functional and the FreeRTOS port does not
/// provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR, so ISR-safe wake
/// is not possible. xTaskNotifyGive is used as the best available option.
inline void wake_loop_any_context() { esphome_main_task_notify(); }
#endif
inline void wake_loop_threadsafe() { esphome_main_task_notify(); }
@@ -77,9 +67,6 @@ void wake_loop_any_context();
/// Non-ISR: always inline.
inline void wake_loop_threadsafe() { wake_loop_impl(); }
/// ISR-safe: no task_woken arg because ESP8266 has no FreeRTOS. Caller must be IRAM_ATTR.
inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); }
namespace internal {
inline void wakeable_delay(uint32_t ms) {
if (ms == 0) {
+3 -56
View File
@@ -434,48 +434,6 @@ class LineComment(Statement):
return "\n".join(parts)
class IIFEUnsafeStatement(Statement):
"""Statement that must not be placed inside an IIFE lambda when
``cpp_main_section`` chunks ``setup()``. Causes the containing
component's block to be emitted flat (no IIFE), so constructs that
rely on exiting ``setup()`` directly e.g. safe_mode's
``if (should_enter_safe_mode(...)) return;`` still work.
Accepts either a ``Statement`` or a bare ``Expression``; bare
expressions are wrapped so they terminate with a semicolon."""
__slots__ = ("inner",)
def __init__(self, inner: Expression | Statement) -> None:
self.inner = inner
def __str__(self) -> str:
return str(statement(self.inner))
class ComponentMarker(Statement):
"""Chunking-boundary sentinel. ``cpp_main_section`` wraps the
statements between two markers in an IIFE to shorten temporary
lifetimes and bound peak setup-time stack. Emits no C++ output.
Grouping is best-effort: ``flush_tasks`` can interleave coroutines
on ``await``, so a component's later statements may land in another
component's chunk. This is safe for the dominant codegen patterns
(placement-new into static storage, assignment to a file-scope
global); patterns that depend on function-local state within the
IIFE scope (cg.variable, with_local_variable, raw bare locals)
are kept together by the bare-local detection in cpp_main_section
so they aren't split across sibling lambdas."""
__slots__ = ("name",)
def __init__(self, name: str) -> None:
self.name = name
def __str__(self) -> str:
return f"// component-marker: {self.name}"
class ProgmemAssignmentExpression(AssignmentExpression):
__slots__ = ()
@@ -500,13 +458,7 @@ def progmem_array(id_, rhs) -> "MockObj":
rhs = safe_exp(rhs)
obj = MockObj(id_, ".")
assignment = ProgmemAssignmentExpression(id_.type, id_, rhs)
# Emit at file scope, not inside setup(). setup() is split into
# per-component IIFE lambdas; a function-local static declared in one
# lambda is not visible to statements in sibling lambdas that
# reference the same shared table (e.g. two lights sharing a gamma
# lookup). File-scope static constexpr is semantically identical for
# read-only lookup tables.
CORE.add_global(assignment)
CORE.add(assignment)
CORE.register_variable(id_, obj)
return obj
@@ -515,7 +467,7 @@ def static_const_array(id_, rhs) -> "MockObj":
rhs = safe_exp(rhs)
obj = MockObj(id_, ".")
assignment = StaticConstAssignmentExpression(id_.type, id_, rhs)
CORE.add_global(assignment)
CORE.add(assignment)
CORE.register_variable(id_, obj)
return obj
@@ -538,15 +490,10 @@ def literal(name: str) -> "MockObj":
def variable(
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register: bool = True
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True
) -> "MockObj":
"""Declare a new variable, not pointer type, in the code generation.
Emits a function-local declaration ``Type id = rhs;`` inside setup().
``cpp_main_section`` detects typed ``AssignmentExpression`` and
disables sub-chunking for the component's group, so later references
to the local within the same ``to_code`` stay visible.
:param id_: The ID used to declare the variable.
:param rhs: The expression to place on the right hand side of the assignment.
:param type_: Manually define a type for the variable, only use this when it's not possible
+1 -1
View File
@@ -107,7 +107,7 @@ def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes:
e,
)
return path.read_bytes()
raise cv.Invalid(f"Could not download from {url}: {e}") from e
raise cv.Invalid(f"Could not download from {url}: {e}")
path.parent.mkdir(parents=True, exist_ok=True)
data = req.content
+2 -1
View File
@@ -39,7 +39,8 @@ class _Schema(vol.Schema):
try:
res = extra(res)
except vol.Invalid as err:
raise ensure_multiple_invalid(err) from err
# pylint: disable=raise-missing-from
raise ensure_multiple_invalid(err)
return res
def _compile_mapping(self, schema, invalid_msg=None):
+1 -5
View File
@@ -171,7 +171,6 @@ VERSION_H_FORMAT = """\
DEFINES_H_TARGET = "esphome/core/defines.h"
VERSION_H_TARGET = "esphome/core/version.h"
BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h"
ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h"
ESPHOME_README_TXT = """
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
@@ -197,12 +196,9 @@ def copy_src_tree():
source_files_l.sort()
# Build #include list for esphome.h
# X-macro files are included multiple times with different macro definitions
# and must not be included bare in esphome.h
esphome_h_exclude = {Path(ENTITY_TYPES_H_TARGET)}
include_l = []
for target, _ in source_files_l:
if target.suffix in HEADER_FILE_EXTENSIONS and target not in esphome_h_exclude:
if target.suffix in HEADER_FILE_EXTENSIONS:
include_l.append(f'#include "{target}"')
include_l.append("")
include_s = "\n".join(include_l)

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