Compare commits

..

6 Commits

Author SHA1 Message Date
Jesse Hills 014131233e [online_image] Use rp2_arduino framework key after rp2040->rp2 rename
dev renamed the rp2040 platform to rp2 (framework key rp2040_arduino ->
rp2_arduino). Propagate that into the online_image file platform, whose
framework-version guard was relocated here during the image platform
refactor and so was not covered by the rename on dev.
2026-07-07 09:36:36 +12:00
Jesse Hills cb0c428d6a Merge remote-tracking branch 'origin/dev' into jesserockz-2026-405
# Conflicts:
#	esphome/components/online_image/__init__.py
2026-07-07 09:35:37 +12:00
Jesse Hills 1d49daa90c [image] Drop obsolete language-schema image fix
`image` is now a platform component, so build_language_schema.py no
longer needs the fix_image() hook that imported the now-removed
IMAGE_SCHEMA (it crashed the schema build). The builder emits image with
its file/animation/online_image platforms generically, like other
platform components.
2026-07-06 20:38:25 +12:00
Jesse Hills 0b1e3283d5 [image] Only migrate recognised legacy image configs
Guard the legacy `image:` migration so it only rewrites shapes the
pre-platform schema actually accepted. A list with a non-dict or
already-platform-tagged entry, or a dict with no recognised image keys
(e.g. `image: ["bad"]`, `image: {foo: 1}`), now returns None from the
migration hook, so normal platform validation surfaces a proper error
instead of the migration silently dropping the input.
2026-07-06 20:24:55 +12:00
Jesse Hills d1938a87aa Merge remote-tracking branch 'origin/dev' into jesserockz-2026-405
# Conflicts:
#	tests/unit_tests/test_config_normalization.py
2026-07-06 20:19:30 +12:00
Jesse Hills dc5e6a20af [image] Restructure into a platform component
Make `image` a platform component with `file`, `animation` and
`online_image` as platforms. Static-file image handling moves into the
new `file` platform; `animation` and `online_image` become thin
platforms whose real schema/codegen live in `image.py`, leaving only
deletable legacy top-level shims in `__init__.py`.

The deprecated top-level `image:`, `animation:` and `online_image:`
forms keep working through 2027.1.0 and now emit a one-shot deprecation
warning that prints a pasteable, migrated `image:` block.
2026-07-06 16:31:37 +12:00
168 changed files with 2701 additions and 3306 deletions
+2
View File
@@ -1,3 +1,5 @@
# Normalize line endings to LF in the repository
* text eol=lf
*.png binary
*.gif binary
*.apng binary
+1
View File
@@ -187,6 +187,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento
esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/file/* @esphome/core
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
esphome/components/font/* @clydebarrow @esphome/core
esphome/components/fs3000/* @kahrendt
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.2.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.1.0
RUN \
platformio settings set enable_telemetry No \
+17 -45
View File
@@ -28,13 +28,13 @@ from esphome.const import (
ALLOWED_NAME_CHARS,
ARGUMENT_HELP_DEVICE,
CONF_API,
CONF_AUTH,
CONF_BAUD_RATE,
CONF_BROKER,
CONF_DEASSERT_RTS_DTR,
CONF_DISABLED,
CONF_ESPHOME,
CONF_LEVEL,
CONF_LOG,
CONF_LOG_TOPIC,
CONF_LOGGER,
CONF_MDNS,
@@ -48,7 +48,7 @@ from esphome.const import (
CONF_PORT,
CONF_SUBSTITUTIONS,
CONF_TOPIC,
CONF_VERSION,
CONF_USERNAME,
CONF_WEB_SERVER,
CONF_WIFI,
ENV_NOGITIGNORE,
@@ -317,12 +317,9 @@ def choose_upload_log_host(
]
resolved.append(choose_prompt(options, purpose=purpose))
elif device == "OTA":
# Logs can stream over a network transport via the native API
# or the web_server HTTP SSE feed.
network_logging = has_api() or has_web_server_logging()
# ensure IP adresses are used first
if is_ip_address(CORE.address) and (
(purpose == Purpose.LOGGING and network_logging)
(purpose == Purpose.LOGGING and has_api())
or (purpose == Purpose.UPLOADING and has_ota())
):
resolved.extend(_resolve_with_cache(CORE.address, purpose))
@@ -334,11 +331,7 @@ def choose_upload_log_host(
if has_mqtt_logging():
resolved.append("MQTT")
if (
network_logging
and has_non_ip_address()
and has_resolvable_address()
):
if has_api() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_ota_hostnames_for_default(purpose))
elif purpose == Purpose.UPLOADING:
@@ -400,7 +393,7 @@ def choose_upload_log_host(
mqtt_config = CORE.config[CONF_MQTT]
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if has_api() or has_web_server_logging():
if has_api():
add_ota_options()
elif purpose == Purpose.UPLOADING and has_ota():
@@ -493,21 +486,6 @@ def has_web_server_ota() -> bool:
)
def has_web_server_logging() -> bool:
"""Check if logs can be streamed over the web_server HTTP SSE endpoint.
The ``web_server`` component exposes a ``/events`` Server-Sent Events
stream that carries ``event: log`` frames. This requires version 2+ (the
v1 UI has no ``/events`` endpoint) and the ``log`` option enabled (default).
"""
web_conf = CORE.config.get(CONF_WEB_SERVER)
if web_conf is None:
return False
if web_conf.get(CONF_VERSION, 2) == 1:
return False
return web_conf.get(CONF_LOG, True)
def has_mqtt_ip_lookup() -> bool:
"""Check if MQTT is available and IP lookup is supported."""
from esphome.components.mqtt import CONF_DISCOVER_IP
@@ -730,7 +708,6 @@ def _wrap_to_code(name, comp, yaml_util):
@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)
@@ -1314,23 +1291,25 @@ def _upload_via_native_api(
def _upload_via_web_server(
config: ConfigType, network_devices: list[str], binary: Path
) -> tuple[int, str | None]:
web_conf = config.get(CONF_WEB_SERVER)
if not web_conf:
raise EsphomeError(
f"Cannot upload via web_server OTA: the {CONF_WEB_SERVER} component "
f"is not configured."
)
remote_port = int(web_conf[CONF_PORT])
auth = web_conf.get(CONF_AUTH) or {}
username = auth.get(CONF_USERNAME)
password = auth.get(CONF_PASSWORD)
from esphome import web_server_ota
from esphome.web_server_helpers import get_web_server_connection
remote_port, username, password = get_web_server_connection(config)
return web_server_ota.run_ota(
network_devices, remote_port, username, password, binary
)
def _show_logs_via_web_server(config: ConfigType, network_devices: list[str]) -> int:
from esphome import web_server_logs
from esphome.web_server_helpers import get_web_server_connection
port, username, password = get_web_server_connection(config)
return web_server_logs.run_logs(network_devices, port, username, password)
# Layout of esp_partition_info_t on flash. Each entry is 32 bytes, leading with a
# 16-bit little-endian magic. ESP-IDF defines ESP_PARTITION_MAGIC = 0x50AA (stored as
# bytes 0xAA, 0x50) for partition entries and ESP_PARTITION_MAGIC_MD5 = 0xEBEB for the
@@ -1459,13 +1438,6 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
config, args.topic, args.username, args.password, args.client_id
)
# Fall back to the web_server HTTP SSE log stream for devices that have
# web_server: but no api: (the logging counterpart to web_server OTA).
if has_web_server_logging() and (
network_devices := _resolve_network_devices(devices, config, args)
):
return _show_logs_via_web_server(config, network_devices)
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
+1 -2
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import heapq
import json
from operator import itemgetter
from pathlib import Path
import sys
@@ -42,7 +41,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
# Symbol size threshold for detailed analysis
SYMBOL_SIZE_THRESHOLD: int = (
10 # Show symbols larger than this in detailed analysis
100 # Show symbols larger than this in detailed analysis
)
# Lower threshold for RAM symbols (RAM is more constrained)
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
+10 -34
View File
@@ -8,7 +8,7 @@ memory-constrained platforms like ESP8266.
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from dataclasses import dataclass
import logging
from pathlib import Path
import re
@@ -65,7 +65,6 @@ class RamSymbol:
size: int
section: str
demangled: str = "" # Demangled name, set after batch demangling
aliases: list[str] = field(default_factory=list) # Other names at same address
class RamStringsAnalyzer:
@@ -236,11 +235,6 @@ class RamStringsAnalyzer:
except (subprocess.CalledProcessError, FileNotFoundError):
return
# Track symbols by address so aliases (multiple names for the same
# object, e.g. the newlib __lock___* mutexes that all alias one
# StaticSemaphore_t) are reported once instead of once per name.
symbols_by_addr: dict[int, RamSymbol] = {}
for line in output.split("\n"):
parts = line.split()
if len(parts) < 4:
@@ -259,18 +253,6 @@ class RamStringsAnalyzer:
if sym_type not in DATA_SYMBOL_TYPES:
continue
if (existing := symbols_by_addr.get(addr)) is not None:
# Prefer a global (uppercase type) name as the primary so
# nm output order can't hide it behind a local alias.
if sym_type.isupper() and existing.sym_type.islower():
existing.aliases.append(existing.name)
existing.name = name
existing.sym_type = sym_type
else:
existing.aliases.append(name)
existing.size = max(existing.size, size)
continue
# Check if symbol is in a RAM section
for section_name in self.ram_sections:
if section_name not in self.sections:
@@ -278,15 +260,15 @@ class RamStringsAnalyzer:
section = self.sections[section_name]
if section.address <= addr < section.address + section.size:
symbol = RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
self.ram_symbols.append(
RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
)
)
symbols_by_addr[addr] = symbol
self.ram_symbols.append(symbol)
break
def _demangle_symbols(self) -> None:
@@ -454,13 +436,7 @@ class RamStringsAnalyzer:
for symbol in largest_symbols:
# Use demangled name if available, otherwise raw name
display_name = symbol.demangled or symbol.name
# Truncate the name, not the alias note, so merged aliases stay
# visible even for long demangled C++ names.
alias_note = f" (+{len(symbol.aliases)} aliases)" if symbol.aliases else ""
max_name_len = 49 - len(alias_note)
if len(display_name) > max_name_len:
display_name = display_name[:max_name_len]
name_display = display_name + alias_note
name_display = display_name[:49] if len(display_name) > 49 else display_name
lines.append(
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
)
-1
View File
@@ -10,7 +10,6 @@
# pylint: disable=unused-import
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
ComponentMarker,
Expression,
FlashStringLiteral,
LineComment,
+20 -98
View File
@@ -1,114 +1,36 @@
import logging
# ---------------------------------------------------------------------------
# Legacy top-level `animation:` deprecation shim -- REMOVE this whole file after
# 2027.1.0.
#
# Animations are now a platform of the `image:` component (`platform:
# animation`); the real schema, actions and codegen live in `image.py`. This
# module only keeps the deprecated top-level `animation:` key working during the
# deprecation window: it reuses that schema/codegen and adds a one-shot
# deprecation warning (with a pasteable migrated `image:` block) at validation
# time. Deleting this file drops the top-level form entirely.
# ---------------------------------------------------------------------------
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_LOOP
import esphome.components.image as espImage
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT
_LOGGER = logging.getLogger(__name__)
from .image import ANIMATION_CONFIG_SCHEMA, setup_animation
AUTO_LOAD = ["image"]
AUTO_LOAD = ["image", "file"]
CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
DOMAIN = "animation"
animation_ns = cg.esphome_ns.namespace("animation")
LEGACY_REMOVAL_VERSION = "2027.1.0"
Animation_ = animation_ns.class_("Animation", espImage.Image_)
# Actions
NextFrameAction = animation_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = animation_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
_capture_legacy_entry, _warn_legacy_animation = (
espImage.legacy_platform_migration_warning(DOMAIN, DOMAIN, LEGACY_REMOVAL_VERSION)
)
CONFIG_SCHEMA = cv.All(
espImage.IMAGE_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
},
),
espImage.validate_settings,
)
CONFIG_SCHEMA = cv.All(_capture_legacy_entry, ANIMATION_CONFIG_SCHEMA)
FINAL_VALIDATE_SCHEMA = _warn_legacy_animation
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (frame := config.get(CONF_FRAME)) is not None:
template_ = await cg.templatable(frame, args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def to_code(config):
(
prog_arr,
width,
height,
image_type,
trans_value,
frame_count,
) = await espImage.write_image(config, all_frames=True)
var = cg.new_Pvariable(
config[CONF_ID],
prog_arr,
width,
height,
frame_count,
image_type,
trans_value,
)
if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frame_count)
count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))
to_code = setup_animation
+115
View File
@@ -0,0 +1,115 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_LOOP
from esphome.components.file.image import image_schema, write_image
from esphome.components.image import Image_, validate_settings
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT
from esphome.types import ConfigType
CODEOWNERS = ["@syndlex"]
AUTO_LOAD = ["file"]
DEPENDENCIES = ["display"]
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
animation_ns = cg.esphome_ns.namespace("animation")
Animation_ = animation_ns.class_("Animation", Image_)
# Actions
NextFrameAction = animation_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = animation_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
ANIMATION_SCHEMA = image_schema(Animation_).extend(
{
cv.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
},
)
# Shared schema used by both the (deprecated) top-level `animation:` key and the
# `image:` `platform: animation` entry.
ANIMATION_CONFIG_SCHEMA = cv.All(ANIMATION_SCHEMA, validate_settings)
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (frame := config.get(CONF_FRAME)) is not None:
template_ = await cg.templatable(frame, args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def setup_animation(config: ConfigType) -> None:
(
prog_arr,
width,
height,
image_type,
trans_value,
frame_count,
) = await write_image(config, all_frames=True)
var = cg.new_Pvariable(
config[CONF_ID],
prog_arr,
width,
height,
frame_count,
image_type,
trans_value,
)
if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frame_count)
count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))
CONFIG_SCHEMA = ANIMATION_CONFIG_SCHEMA
to_code = setup_animation
+1 -2
View File
@@ -240,13 +240,12 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
}
void APIServer::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Server:\n"
" Address: %s:%u\n"
" Listen backlog: %u\n"
" Max connections: %u",
network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
-1
View File
@@ -16,7 +16,6 @@ CONF_COLOR_DEPTH = "color_depth"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
-67
View File
@@ -11,7 +11,6 @@ from typing import Any
from esphome import yaml_util
import esphome.codegen as cg
from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
@@ -30,7 +29,6 @@ from esphome.const import (
CONF_PATH,
CONF_PLATFORM_VERSION,
CONF_PLATFORMIO_OPTIONS,
CONF_PROJECT,
CONF_REF,
CONF_SAFE_MODE,
CONF_SIZE,
@@ -1100,50 +1098,6 @@ def _detect_variant(value):
return value
def _ota_downgrade_protection_errors(
project_version: str | None, signed_ota_enabled: bool
) -> list[cv.Invalid]:
"""Validate prerequisites for OTA downgrade protection.
Called only when the feature is enabled. Returns a ``cv.Invalid`` for each
unmet requirement: a dotted-numeric project version (the firmware version
compared on-device) and signed OTA (so the embedded version cannot be
forged).
"""
path = [CONF_FRAMEWORK, CONF_ADVANCED, CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]
errs: list[cv.Invalid] = []
if not project_version:
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires a "
f"'{CONF_PROJECT}' with a '{CONF_VERSION}' to be set in the "
f"'{CONF_ESPHOME}' section; this version is the firmware version "
"compared during OTA.",
path=path,
)
)
elif not re.fullmatch(r"\d+(\.\d+)*", project_version):
# The on-device comparison parses dotted-numeric versions only.
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires the "
f"'{CONF_PROJECT}' '{CONF_VERSION}' to be dotted-numeric (such "
f"as '1.2.3'), got '{project_version}'.",
path=path,
)
)
if not signed_ota_enabled:
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires "
f"'{CONF_SIGNED_OTA_VERIFICATION}' to be enabled; without signed "
"OTA the embedded version cannot be trusted.",
path=path,
)
)
return errs
def final_validate(config):
# Imported locally to avoid circular import issues
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
@@ -1349,14 +1303,6 @@ def final_validate(config):
"Binaries will NOT be signed automatically during build. "
"You must sign them externally before flashing."
)
if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]:
project = full_config[CONF_ESPHOME].get(CONF_PROJECT)
errs.extend(
_ota_downgrade_protection_errors(
project[CONF_VERSION] if project else None,
bool(advanced.get(CONF_SIGNED_OTA_VERIFICATION)),
)
)
if errs:
raise cv.MultipleInvalid(errs)
@@ -1594,9 +1540,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
min=8192, max=32768
),
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
cv.Optional(
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION, default=False
): cv.boolean,
cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All(
cv.Schema(
{
@@ -2415,16 +2358,6 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True)
cg.add_define("USE_OTA_ROLLBACK")
# Enable software OTA downgrade protection. Embed the project version into
# the image's esp_app_desc_t so the OTA backend can compare it against the
# running version (final_validate guarantees a dotted-numeric project
# version and that signed OTA is enabled).
if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]:
project_version = CORE.config[CONF_ESPHOME][CONF_PROJECT][CONF_VERSION]
add_idf_sdkconfig_option("CONFIG_APP_PROJECT_VER_FROM_CONFIG", True)
add_idf_sdkconfig_option("CONFIG_APP_PROJECT_VER", project_version)
cg.add_define("USE_OTA_DOWNGRADE_PROTECTION")
# Enable signed app verification without hardware secure boot
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True)
+1 -16
View File
@@ -274,16 +274,7 @@ async def to_code(config):
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option(
"platform_packages",
[
f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}",
# Override the ancient tool-esptoolpy ~1.30000.0 pinned by
# platform-espressif8266 4.2.1 with the same 5.x build the
# pioarduino ESP32 platform uses, so both platforms share the
# same installed package and stop reinstalling on every switch.
# The 0.0.1 path component is pioarduino's stable registry
# release tag (not the tool version); the tool itself is 5.2.0.
"pioarduino/tool-esptoolpy@https://github.com/pioarduino/registry/releases/download/0.0.1/esptoolpy-v5.2.0.zip",
],
[f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}"],
)
# Default for platformio is LWIP2_LOW_MEMORY with:
@@ -341,12 +332,6 @@ async def to_code(config):
for symbol in ("vprintf", "printf", "fprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap the lwIP2 glue's do-nothing dhcp_cleanup()/dhcp_release() stubs so the
# linker can drop their "STUB: ..." message strings from DRAM.
# See lwip_glue_stubs.cpp for implementation.
for symbol in ("dhcp_cleanup", "dhcp_release"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap Arduino's millis() so all callers (including Arduino libraries and ISR
# handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply
# implementation in the Arduino ESP8266 core.
@@ -1,35 +0,0 @@
/*
* Linker wrap stubs for the lwIP2 glue's dead DHCP entry points.
*
* The ESP8266 SDK blobs call dhcp_cleanup() and dhcp_release() when the
* station leaves an access point (cnx_sta_leave, wifi_station_dhcpc_stop).
* In the prebuilt lwIP2 glue (liblwip2-*.a, glue-esp/lwip-esp.c) these are
* stubs whose only effect is printing "STUB: dhcp_cleanup" and
* "STUB: dhcp_release"; the real DHCP teardown happens through lwIP2's
* renamed dhcp_cleanup_LWIP2()/dhcp_release_LWIP2() functions.
*
* On ESP8266 .rodata lives in DRAM, so those message strings waste scarce
* RAM. Wrapping the stubs with silent equivalents lets the linker garbage
* collect the glue stub bodies together with their strings.
*
* Saves 38 bytes of RAM and removes the "STUB:" log noise on Wi-Fi
* disconnect. Behavior is otherwise unchanged.
*/
#if defined(USE_ESP8266)
namespace esphome::esp8266 {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
// The callers are closed-source SDK blobs; the netif argument is unused.
void __wrap_dhcp_cleanup(void * /*netif*/) {}
// The glue stub returns ERR_ABRT (-8; lwIP 1.4 err_t is a signed char).
signed char __wrap_dhcp_release(void * /*netif*/) { return -8; }
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP8266
@@ -94,12 +94,11 @@ void ESPHomeOTAComponent::setup() {
}
void ESPHomeOTAComponent::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Over-The-Air updates:\n"
" Address: %s:%u\n"
" Version: %d",
network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION);
network::get_use_address(), this->port_, USE_OTA_VERSION);
#ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) {
ESP_LOGCONFIG(TAG, " Password configured");
+3 -29
View File
@@ -41,7 +41,7 @@ DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action)
ESPNowHandlerTrigger = automation.Trigger.template(
ESPNowRecvInfoConstRef,
cg.uint8.operator("const").operator("ptr"),
cg.uint16,
cg.uint8,
)
OnUnknownPeerTrigger = espnow_ns.class_(
@@ -56,20 +56,6 @@ OnBroadcastTrigger = espnow_ns.class_(
CONF_AUTO_ADD_PEER = "auto_add_peer"
CONF_MAX_PAYLOAD_SIZE = "max_payload_size"
# Payload limits of ESP-NOW v1 and v2 frames. The radio negotiates the
# protocol version per peer on its own; the option only sizes this device's
# packet buffers, whose static RAM cost is proportional to it (~8 KB at 250
# bytes, ~44 KB at 1470).
ESPNOW_PAYLOAD_V1 = 250
ESPNOW_PAYLOAD_V2 = 1470
# Config-time cap for action payloads. The per-device limit is the
# ``max_payload_size`` option, which the action schema cannot see; send()
# enforces it at runtime.
MAX_ESPNOW_PACKET_SIZE = ESPNOW_PAYLOAD_V2
CONF_PEERS = "peers"
CONF_ON_SENT = "on_sent"
CONF_ON_UNKNOWN_PEER = "on_unknown_peer"
@@ -77,15 +63,7 @@ CONF_ON_BROADCAST = "on_broadcast"
CONF_CONTINUE_ON_ERROR = "continue_on_error"
CONF_WAIT_FOR_SENT = "wait_for_sent"
def _validate_max_payload_size(value: int) -> int:
if value > ESPNOW_PAYLOAD_V1:
return cv.require_framework_version(
esp_idf=cv.Version(5, 4, 0),
esp32_arduino=cv.Version(3, 2, 0),
extra_message="ESP-NOW v2 frames need an ESP-NOW v2 capable framework",
)(value)
return value
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
def validate_channel(value):
@@ -100,9 +78,6 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(ESPNowComponent),
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): validate_channel,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_MAX_PAYLOAD_SIZE, default=ESPNOW_PAYLOAD_V1): cv.All(
cv.int_range(min=1, max=ESPNOW_PAYLOAD_V2), _validate_max_payload_size
),
cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean,
cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address),
cv.Optional(CONF_ON_UNKNOWN_PEER): automation.validate_automation(
@@ -138,7 +113,7 @@ async def _trigger_to_code(config):
[
(ESPNowRecvInfoConstRef, "info"),
(cg.uint8.operator("const").operator("ptr"), "data"),
(cg.uint16, "size"),
(cg.uint8, "size"),
],
config,
)
@@ -150,7 +125,6 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add_define("USE_ESPNOW")
cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE])
if wifi_channel := config.get(CONF_CHANNEL):
cg.add(var.set_wifi_channel(wifi_channel))
+6 -6
View File
@@ -119,7 +119,7 @@ template<typename... Ts> class SetChannelAction final : public Action<Ts...>, pu
}
};
class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowReceivedPacketHandler {
public:
explicit OnReceiveTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
@@ -128,7 +128,7 @@ class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint
explicit OnReceiveTrigger() {}
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -141,15 +141,15 @@ class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint
bool has_address_{false};
uint8_t address_[ESP_NOW_ETH_ALEN]{};
};
class OnUnknownPeerTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
class OnUnknownPeerTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowUnknownPeerHandler {
public:
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
this->trigger(info, data, size);
return false; // Return false to continue processing other internal handlers
}
};
class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowBroadcastHandler {
public:
explicit OnBroadcastTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
@@ -157,7 +157,7 @@ class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const ui
}
explicit OnBroadcastTrigger() {}
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -4,7 +4,6 @@
#include "espnow_err.h"
#include <algorithm>
#include <cinttypes>
#include "esphome/core/application.h"
@@ -97,9 +96,9 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
// Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a
// v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B),
// but the receive buffer only fits v2 frames with ``max_payload_size``; copying a
// larger frame would overflow packet_.receive.data.
if (size < 0 || size > ESPNOW_MAX_DATA_LEN) {
// but our receive buffer is ESP_NOW_MAX_DATA_LEN (250 B); copying a larger
// frame would overflow packet_.receive.data.
if (size < 0 || size > ESP_NOW_MAX_DATA_LEN) {
global_esp_now->receive_packet_queue_.increment_dropped_count();
return;
}
@@ -286,14 +285,11 @@ void ESPNowComponent::loop() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
// Cap the hex dump at a v1 frame: a full v2 frame would need a
// ~4.4 KB stack buffer.
char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)];
format_mac_addr_upper(info.src_addr, src_buf);
format_mac_addr_upper(info.des_addr, dst_buf);
ESP_LOGV(TAG, "<<< [%s -> %s] %s", src_buf, dst_buf,
format_hex_pretty_to(hex_buf, packet->packet_.receive.data,
std::min<uint16_t>(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN)));
format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size));
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcast_handlers_) {
@@ -366,7 +362,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
return ESP_ERR_ESPNOW_PEER_NOT_SET;
} else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
return ESP_ERR_ESPNOW_OWN_ADDRESS;
} else if (size > ESPNOW_MAX_DATA_LEN) {
} else if (size > ESP_NOW_MAX_DATA_LEN) {
return ESP_ERR_ESPNOW_DATA_SIZE;
} else if (!esp_now_is_peer_exist(peer_address)) {
if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) {
+3 -3
View File
@@ -62,7 +62,7 @@ class ESPNowUnknownPeerHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
/// Handler interface for receiving ESPNow packets
@@ -74,7 +74,7 @@ class ESPNowReceivedPacketHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
/// Handler interface for receiving ESPNow broadcast packets
/// Components should inherit from this class to handle incoming ESPNow data
@@ -85,7 +85,7 @@ class ESPNowBroadcastHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
class ESPNowComponent final : public Component {
+9 -26
View File
@@ -19,23 +19,6 @@ namespace esphome::espnow {
static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};
// Maximum payload this component sends and receives, from the
// ``max_payload_size`` option. The radio stack speaks ESP-NOW v2 regardless
// (negotiated per peer); payloads beyond the v1 limit (250 bytes) are opt-in
// because the packet pools are statically sized from this, so their RAM cost
// is proportional (~8 KB at 250 bytes, ~44 KB at the v2 limit of 1470).
#ifndef USE_ESPNOW_MAX_PAYLOAD_SIZE
#define USE_ESPNOW_MAX_PAYLOAD_SIZE ESP_NOW_MAX_DATA_LEN
#endif
static constexpr uint16_t ESPNOW_MAX_DATA_LEN = USE_ESPNOW_MAX_PAYLOAD_SIZE;
#ifdef ESP_NOW_MAX_DATA_LEN_V2
static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN_V2,
"espnow max_payload_size cannot exceed the ESP-NOW v2 frame limit");
#else
static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN,
"espnow max_payload_size beyond 250 bytes requires an ESP-IDF with ESP-NOW v2 support (5.4+)");
#endif
struct WifiPacketRxControl {
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
@@ -95,10 +78,10 @@ class ESPNowPacket {
union {
// NOLINTNEXTLINE(readability-identifier-naming)
struct received_data {
ESPNowRecvInfo info; // Information about the received packet
uint8_t data[ESPNOW_MAX_DATA_LEN]; // Data received in the packet
uint16_t size; // Size of the received data
WifiPacketRxControl rx_ctrl; // Status of the received packet
ESPNowRecvInfo info; // Information about the received packet
uint8_t data[ESP_NOW_MAX_DATA_LEN]; // Data received in the packet
uint8_t size; // Size of the received data
WifiPacketRxControl rx_ctrl; // Status of the received packet
} receive;
// NOLINTNEXTLINE(readability-identifier-naming)
@@ -161,15 +144,15 @@ class ESPNowSendPacket {
this->callback_ = nullptr; // Reset callback
}
uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to
uint8_t data_[ESPNOW_MAX_DATA_LEN]{0}; // Data to send
uint16_t size_{0}; // Size of the data to send, must be <= ESPNOW_MAX_DATA_LEN
send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete
uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to
uint8_t data_[ESP_NOW_MAX_DATA_LEN]{0}; // Data to send
uint8_t size_{0}; // Size of the data to send, must be <= ESP_NOW_MAX_DATA_LEN
send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete
private:
void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN);
if (size > ESPNOW_MAX_DATA_LEN) {
if (size > ESP_NOW_MAX_DATA_LEN) {
this->size_ = 0;
return;
}
@@ -42,8 +42,8 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
return;
}
if (buf.size() > ESPNOW_MAX_DATA_LEN) {
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %u)", buf.size(), (unsigned) ESPNOW_MAX_DATA_LEN);
if (buf.size() > ESP_NOW_MAX_DATA_LEN) {
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN);
return;
}
@@ -55,8 +55,8 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
});
}
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size, info.src_addr[0],
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
if (data == nullptr || size == 0) {
@@ -70,9 +70,9 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data
return false; // Allow other handlers to run
}
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) {
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size,
info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
if (data == nullptr || size == 0) {
ESP_LOGW(TAG, "Received empty or null broadcast packet");
@@ -24,12 +24,12 @@ class ESPNowTransport final : public packet_transport::PacketTransport,
}
// ESPNow handler interface
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override;
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
size_t get_max_packet_size() override { return ESPNOW_MAX_DATA_LEN; }
size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; }
bool should_send() override;
peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
+2 -2
View File
@@ -4,7 +4,7 @@ import logging
from esphome import automation, pins
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.network import add_use_address, ip_address_literal
from esphome.components.network import ip_address_literal
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -543,7 +543,7 @@ async def to_code(config):
await _to_code_rp2040(var, config)
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
add_use_address(var, config[CONF_USE_ADDRESS])
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
# enable_on_boot defaults to true in C++ - only set if false
if not config[CONF_ENABLE_ON_BOOT]:
cg.add(var.set_enable_on_boot(False))
@@ -145,8 +145,6 @@ class EthernetComponent final : public Component {
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
void get_eth_mac_address_raw(uint8_t *mac);
@@ -348,7 +346,7 @@ class EthernetComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{nullptr};
const char *use_address_{""};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+1 -1
View File
@@ -83,7 +83,7 @@ void FanCall::validate_() {
auto traits = this->parent_.get_traits();
if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"
+3 -3
View File
@@ -14,7 +14,7 @@ class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
@@ -26,9 +26,9 @@ class FanTraits {
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
/// Return how many speed levels the fan has
uint8_t supported_speed_count() const { return this->speed_count_; }
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@esphome/core"]
+315
View File
@@ -0,0 +1,315 @@
from __future__ import annotations
import contextlib
import hashlib
import io
import logging
from pathlib import Path
import re
from PIL import Image, UnidentifiedImageError
from esphome import core, external_files
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_OPAQUE,
CONF_TRANSPARENCY,
DOMAIN,
IMAGE_TYPE,
Image_,
ImageEncoder,
add_metadata,
get_image_type_enum,
get_transparency_enum,
is_svg_file,
validate_settings,
validate_transparency,
validate_type,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_DITHER,
CONF_FILE,
CONF_ICON,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
_LOGGER = logging.getLogger(__name__)
# If the MDI file cannot be downloaded within this time, abort.
IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
SOURCE_LOCAL = "local"
SOURCE_WEB = "web"
SOURCE_MDI = "mdi"
SOURCE_MDIL = "mdil"
SOURCE_MEMORY = "memory"
MDI_SOURCES = {
SOURCE_MDI: "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/",
SOURCE_MDIL: "https://raw.githubusercontent.com/Pictogrammers/MaterialDesignLight/refs/heads/master/svg/",
SOURCE_MEMORY: "https://raw.githubusercontent.com/Pictogrammers/Memory/refs/heads/main/src/svg/",
}
def compute_local_image_path(value) -> Path:
url = value[CONF_URL] if isinstance(value, dict) else value
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
# Downloaded files are cached under the shared `image` domain directory so
# the cache location is unaffected by which platform requested the file.
base_dir = external_files.compute_local_file_dir(DOMAIN)
return base_dir / key
def local_path(value):
value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
def download_file(url, path):
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return str(path)
def download_gh_svg(value, source):
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
base_dir = external_files.compute_local_file_dir(DOMAIN) / source
path = base_dir / f"{mdi_id}.svg"
url = MDI_SOURCES[source] + mdi_id + ".svg"
return download_file(url, path)
def download_image(value):
value = value[CONF_URL] if isinstance(value, dict) else value
return download_file(value, compute_local_image_path(value))
def validate_file_shorthand(value):
value = cv.string_strict(value)
parts = value.strip().split(":")
if len(parts) == 2 and parts[0] in MDI_SOURCES:
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
if match is None:
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
return download_gh_svg(parts[1], parts[0])
if value.startswith(("http://", "https://")):
return download_image(value)
value = cv.file_(value)
return local_path(value)
LOCAL_SCHEMA = cv.All(
{
cv.Required(CONF_PATH): cv.file_,
},
local_path,
)
def mdi_schema(source):
def validate_mdi(value):
return download_gh_svg(value, source)
return cv.All(
cv.Schema(
{
cv.Required(CONF_ICON): cv.string,
}
),
validate_mdi,
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.string,
},
download_image,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
}
| {source: mdi_schema(source) for source in MDI_SOURCES},
key=CONF_SOURCE,
)
OPTIONS_SCHEMA = {
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
}
def image_schema(class_: MockObjClass = Image_) -> cv.Schema:
"""Build the validation schema for a single file-backed image entry.
Shared by the built-in ``file`` image platform and the ``animation``
platform (which extends it). Platforms that source their pixels elsewhere
(e.g. ``online_image``) provide their own schema instead.
:param class_: The declared C++ class for the generated image instance.
"""
return cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(class_),
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
**OPTIONS_SCHEMA,
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
}
)
def validate_image_final(config: ConfigType) -> ConfigType:
"""Per-entry final validation, shared by file-backed image platforms.
For LVGL 9 the default byte order for RGB565 images is little-endian, so
fill in that default when the user did not specify a byte order and warn
when big-endian was explicitly requested.
"""
if byte_order := config.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",
config.get(CONF_FILE),
)
else:
config[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config
async def new_image(config: ConfigType) -> MockObj:
"""Generate a single file-backed ``image::Image`` instance.
Used by the built-in ``file`` platform; encodes the image data, registers
the C++ variable and records its metadata for other components to consume.
"""
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
var = cg.new_Pvariable(
config[CONF_ID], prog_arr, width, height, image_type, trans_value
)
add_metadata(
config[CONF_ID], width, height, config[CONF_TYPE], config[CONF_TRANSPARENCY]
)
return var
async def write_image(config, all_frames=False):
path = Path(config[CONF_FILE])
if not path.is_file():
raise core.EsphomeError(f"Could not load image file {path}")
resize = config.get(CONF_RESIZE)
try:
if is_svg_file(path):
import resvg_py
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))
width, height = image.size
else:
image = Image.open(path)
width, height = image.size
if resize:
# Preserve aspect ratio
new_width_max = min(width, resize[0])
new_height_max = min(height, resize[1])
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
except (OSError, UnidentifiedImageError, ValueError) as exc:
raise core.EsphomeError(f"Could not read image file {path}: {exc}") from exc
if not resize and (width > 500 or height > 500):
_LOGGER.warning(
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
path,
)
dither = (
Image.Dither.NONE
if config[CONF_DITHER] == "NONE"
else Image.Dither.FLOYDSTEINBERG
)
type = config[CONF_TYPE]
transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE)
invert_alpha = config[CONF_INVERT_ALPHA]
frame_count = 1
if all_frames:
with contextlib.suppress(AttributeError):
frame_count = image.n_frames
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)
# Encode each frame with its own encoder and concatenate. This keeps every
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
# correct without needing to know the total frame count.
byte_order = config.get(CONF_BYTE_ORDER)
combined_data: list[int] = []
encoder: ImageEncoder | None = None
for frame_index in range(frame_count):
image.seek(frame_index)
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
combined_data.extend(encoder.data)
rhs = [HexInt(x) for x in combined_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(encoder.transparency)
return prog_arr, width, height, image_type, trans_value, frame_count
# The built-in static-image platform: pixels embedded at compile time from a
# local file, a downloaded web image, or a Material Design Icon.
CONFIG_SCHEMA = cv.All(image_schema(Image_), validate_settings)
FINAL_VALIDATE_SCHEMA = validate_image_final
async def to_code(config: ConfigType) -> None:
await new_image(config)
+1 -1
View File
@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
+2 -2
View File
@@ -14,7 +14,7 @@ enum DecayMode {
class HBridgeFan final : public Component, public fan::Fan {
public:
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
@@ -35,7 +35,7 @@ class HBridgeFan final : public Component, public fan::Fan {
output::FloatOutput *pin_b_;
output::FloatOutput *enable_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
+1 -1
View File
@@ -187,7 +187,7 @@ void HeatpumpIRClimate::transmit_state() {
swing_h_cmd = HDIR_SWING;
}
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
fan_speed_cmd = FAN_2;
break;
+1 -1
View File
@@ -118,7 +118,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
+206 -410
View File
@@ -1,38 +1,27 @@
from __future__ import annotations
import contextlib
from collections.abc import Callable
from dataclasses import dataclass
import hashlib
import io
import logging
from pathlib import Path
import re
from PIL import Image, UnidentifiedImageError
from esphome import core, external_files
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA
import esphome.config_validation as cv
from esphome.const import (
CONF_DEFAULTS,
CONF_DITHER,
CONF_FILE,
CONF_ICON,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.const import CONF_DEFAULTS, CONF_FILE, CONF_ID, CONF_PLATFORM, CONF_TYPE
from esphome.core import CORE
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
DOMAIN = "image"
DEPENDENCIES = ["display"]
IS_PLATFORM_COMPONENT = True
# Name of the built-in static-image platform (local file / web / MDI sources).
PLATFORM_FILE = "file"
image_ns = cg.esphome_ns.namespace("image")
@@ -135,17 +124,6 @@ class ImageEncoder:
"""
return False
@classmethod
def get_options(cls) -> list[str]:
"""
Get the available options for this image encoder
"""
options = [*OPTIONS]
if not cls.is_endian():
options.remove(CONF_BYTE_ORDER)
options.append(CONF_RAW_DATA_ID)
return options
def is_alpha_only(image: Image):
"""
@@ -338,60 +316,11 @@ TransparencyType = image_ns.enum("TransparencyType")
CONF_TRANSPARENCY = "transparency"
# If the MDI file cannot be downloaded within this time, abort.
IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
SOURCE_LOCAL = "local"
SOURCE_WEB = "web"
SOURCE_MDI = "mdi"
SOURCE_MDIL = "mdil"
SOURCE_MEMORY = "memory"
MDI_SOURCES = {
SOURCE_MDI: "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/",
SOURCE_MDIL: "https://raw.githubusercontent.com/Pictogrammers/MaterialDesignLight/refs/heads/master/svg/",
SOURCE_MEMORY: "https://raw.githubusercontent.com/Pictogrammers/Memory/refs/heads/main/src/svg/",
}
Image_ = image_ns.class_("Image")
INSTANCE_TYPE = Image_
def compute_local_image_path(value) -> Path:
url = value[CONF_URL] if isinstance(value, dict) else value
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
return base_dir / key
def local_path(value):
value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
def download_file(url, path):
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return str(path)
def download_gh_svg(value, source):
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
base_dir = external_files.compute_local_file_dir(DOMAIN) / source
path = base_dir / f"{mdi_id}.svg"
url = MDI_SOURCES[source] + mdi_id + ".svg"
return download_file(url, path)
def download_image(value):
value = value[CONF_URL] if isinstance(value, dict) else value
return download_file(value, compute_local_image_path(value))
def is_svg_file(file):
if not file:
return False
@@ -399,62 +328,6 @@ def is_svg_file(file):
return "<svg" in str(f.read(1024))
def validate_file_shorthand(value):
value = cv.string_strict(value)
parts = value.strip().split(":")
if len(parts) == 2 and parts[0] in MDI_SOURCES:
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
if match is None:
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
return download_gh_svg(parts[1], parts[0])
if value.startswith(("http://", "https://")):
return download_image(value)
value = cv.file_(value)
return local_path(value)
LOCAL_SCHEMA = cv.All(
{
cv.Required(CONF_PATH): cv.file_,
},
local_path,
)
def mdi_schema(source):
def validate_mdi(value):
return download_gh_svg(value, source)
return cv.All(
cv.Schema(
{
cv.Required(CONF_ICON): cv.string,
}
),
validate_mdi,
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.string,
},
download_image,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
}
| {source: mdi_schema(source) for source in MDI_SOURCES},
key=CONF_SOURCE,
)
def validate_transparency(choices=TRANSPARENCY_TYPES):
def validate(value):
if isinstance(value, bool):
@@ -508,271 +381,6 @@ def validate_settings(value, path=()):
return value
IMAGE_ID_SCHEMA = {
cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
OPTIONS_SCHEMA = {
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
}
DEFAULTS_SCHEMA = {
**OPTIONS_SCHEMA,
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
}
OPTIONS = [key.schema for key in OPTIONS_SCHEMA]
# image schema with no defaults, used with `CONF_IMAGES` in the config
IMAGE_SCHEMA_NO_DEFAULTS = {
**IMAGE_ID_SCHEMA,
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
}
IMAGE_SCHEMA = cv.Schema(
{
**IMAGE_ID_SCHEMA,
**OPTIONS_SCHEMA,
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
}
)
def apply_defaults(image, defaults, path):
"""
Apply defaults to an image configuration
"""
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
if type is None:
raise cv.Invalid(
"Type is required either in the image config or in the defaults", path=path
)
type_class = IMAGE_TYPE[type]
config = {
**{key: image.get(key, defaults.get(key)) for key in type_class.get_options()},
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
CONF_TYPE: image.get(CONF_TYPE, defaults.get(CONF_TYPE)),
}
validate_settings(config, path)
return config
def validate_defaults(value):
"""
Apply defaults to the images in the configuration and flatten to a single list.
"""
defaults = value[CONF_DEFAULTS]
result = []
# Apply defaults to the images: list and add the list entries to the result
for index, image in enumerate(value.get(CONF_IMAGES, [])):
result.append(apply_defaults(image, defaults, [CONF_IMAGES, index]))
# Apply defaults to images under the type keys and add them to the result
for image_type, type_config in value.items():
type_upper = image_type.upper()
if type_upper not in IMAGE_TYPE:
continue
type_class = IMAGE_TYPE[type_upper]
if isinstance(type_config, list):
# If the type is a list, apply defaults to each entry
for index, image in enumerate(type_config):
result.append(apply_defaults(image, defaults, [image_type, index]))
else:
# Handle transparency options for the type
for trans_type in set(type_class.allow_config).intersection(type_config):
for index, image in enumerate(type_config[trans_type]):
result.append(
apply_defaults(image, defaults, [image_type, trans_type, index])
)
return result
def typed_image_schema(image_type):
"""
Construct a schema for a specific image type, allowing transparency options
"""
return cv.Any(
cv.Schema(
{
cv.Optional(t.lower()): cv.ensure_list(
{
**IMAGE_ID_SCHEMA,
**{
cv.Optional(key): OPTIONS_SCHEMA[key]
for key in OPTIONS
if key != CONF_TRANSPARENCY
},
cv.Optional(
CONF_TRANSPARENCY, default=t
): validate_transparency((t,)),
cv.Optional(CONF_TYPE, default=image_type): validate_type(
(image_type,)
),
}
)
for t in IMAGE_TYPE[image_type].allow_config.intersection(
TRANSPARENCY_TYPES
)
}
),
# Allow a default configuration with no transparency preselected
cv.ensure_list(
{
**IMAGE_SCHEMA_NO_DEFAULTS,
cv.Optional(CONF_TYPE, default=image_type): validate_type(
(image_type,)
),
}
),
)
# The config schema can be a (possibly empty) single list of images,
# or a dictionary with optional keys `defaults:`, `images:` and the image types
def _config_schema(value):
if isinstance(value, list) or (
isinstance(value, dict) and (CONF_ID in value or CONF_FILE in value)
):
return cv.ensure_list(cv.All(IMAGE_SCHEMA, validate_settings))(value)
if not isinstance(value, dict):
raise cv.Invalid(
"Badly formed image configuration, expected a list or a dictionary",
)
return cv.All(
cv.Schema(
{
cv.Optional(CONF_DEFAULTS, default={}): DEFAULTS_SCHEMA,
cv.Optional(CONF_IMAGES, default=[]): cv.ensure_list(
{
**IMAGE_SCHEMA_NO_DEFAULTS,
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
}
),
**{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE},
}
),
validate_defaults,
)(value)
CONFIG_SCHEMA = _config_schema
def _final_validate(config):
"""
For LVGL 9 the default byte order for RGB565 images is little-endian
: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"
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def write_image(config, all_frames=False):
path = Path(config[CONF_FILE])
if not path.is_file():
raise core.EsphomeError(f"Could not load image file {path}")
resize = config.get(CONF_RESIZE)
try:
if is_svg_file(path):
import resvg_py
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))
width, height = image.size
else:
image = Image.open(path)
width, height = image.size
if resize:
# Preserve aspect ratio
new_width_max = min(width, resize[0])
new_height_max = min(height, resize[1])
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
except (OSError, UnidentifiedImageError, ValueError) as exc:
raise core.EsphomeError(f"Could not read image file {path}: {exc}") from exc
if not resize and (width > 500 or height > 500):
_LOGGER.warning(
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
path,
)
dither = (
Image.Dither.NONE
if config[CONF_DITHER] == "NONE"
else Image.Dither.FLOYDSTEINBERG
)
type = config[CONF_TYPE]
transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE)
invert_alpha = config[CONF_INVERT_ALPHA]
frame_count = 1
if all_frames:
with contextlib.suppress(AttributeError):
frame_count = image.n_frames
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)
# Encode each frame with its own encoder and concatenate. This keeps every
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
# correct without needing to know the total frame count.
byte_order = config.get(CONF_BYTE_ORDER)
combined_data: list[int] = []
encoder: ImageEncoder | None = None
for frame_index in range(frame_count):
image.seek(frame_index)
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
combined_data.extend(encoder.data)
rhs = [HexInt(x) for x in combined_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(encoder.transparency)
return prog_arr, width, height, image_type, trans_value, frame_count
def add_metadata(id: str, width: int, height: int, image_type: str, transparency):
all_metadata = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
all_metadata[str(id)] = ImageMetaData(
@@ -780,17 +388,10 @@ def add_metadata(id: str, width: int, height: int, image_type: str, transparency
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
# Base platform-component codegen: each entry is generated by its platform's
# own ``to_code``; here we only need the feature define to be present.
cg.add_define("USE_IMAGE")
# By now the config will be a simple list.
for entry in config:
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
cg.new_Pvariable(
entry[CONF_ID], prog_arr, width, height, image_type, trans_value
)
add_metadata(
entry[CONF_ID], width, height, entry[CONF_TYPE], entry[CONF_TRANSPARENCY]
)
def get_all_image_metadata() -> dict[str, ImageMetaData]:
@@ -801,3 +402,198 @@ def get_all_image_metadata() -> dict[str, ImageMetaData]:
def get_image_metadata(image_id: str) -> ImageMetaData | None:
"""Get image metadata by ID for use by other components."""
return get_all_image_metadata().get(image_id)
# ---------------------------------------------------------------------------
# Legacy top-level component -> `image:` platform deprecation helpers
# -- REMOVE after 2027.1.0 together with the `animation:`/`online_image:` shims.
#
# `animation:` and `online_image:` used to be standalone top-level components and
# are now platforms of `image:`. Their deprecated top-level shims use this helper
# to (1) record each raw entry as it is validated and (2) print a single,
# pasteable migrated `image:` block once every entry has been seen. The block is
# emitted from FINAL_VALIDATE_SCHEMA, which always runs after every per-entry
# CONFIG_SCHEMA step, so all entries are captured before it fires.
# ---------------------------------------------------------------------------
def legacy_platform_migration_warning(
domain: str, platform: str, removal_version: str
) -> tuple[
Callable[[ConfigType], ConfigType],
Callable[[ConfigType], ConfigType],
]:
"""Build the per-entry capture and one-shot warning validators for a
deprecated top-level component that is now an ``image:`` platform.
Returns ``(capture, finalize)``:
* ``capture`` is a ``CONFIG_SCHEMA`` validator placed *before* the real
schema so it sees the raw user entry; it records a copy of each entry.
* ``finalize`` is a ``FINAL_VALIDATE_SCHEMA`` validator that warns exactly
once with the migrated, pasteable ``image:`` block.
"""
entries_key = "legacy_entries"
shown_key = "legacy_warning_shown"
def capture(config: ConfigType) -> ConfigType:
data = CORE.data.setdefault(domain, {})
data.setdefault(entries_key, []).append(dict(config))
return config
def finalize(config: ConfigType) -> ConfigType:
data = CORE.data.setdefault(domain, {})
if not data.get(shown_key):
data[shown_key] = True
from esphome import yaml_util
migrated = [
{CONF_PLATFORM: platform, **entry}
for entry in data.get(entries_key, [])
]
_LOGGER.warning(
"The top-level '%s:' configuration is deprecated and will be "
"removed in ESPHome %s. '%s' is now a platform of the 'image' "
"component. Replace your '%s:' block with:\n\n%s",
domain,
removal_version,
domain,
domain,
yaml_util.dump({DOMAIN: migrated}),
)
return config
return capture, finalize
# ---------------------------------------------------------------------------
# Legacy `image:` config migration -- REMOVE after 2027.1.0
#
# Before `image` became a platform component, its top-level config was either a
# bare list of image dicts, a single image dict, or a dict with `defaults:`,
# `images:` and per-type group keys. This block transparently rewrites those
# forms into the new ``platform: file`` list and prints the migrated YAML.
# It is intentionally self-contained so it can be deleted in one piece together
# with the ``LEGACY_CONFIG_MIGRATE`` assignment below.
# ---------------------------------------------------------------------------
LEGACY_REMOVAL_VERSION = "2027.1.0"
def _is_new_image_format(config: object) -> bool:
"""True when the config is already the new ``platform:``-tagged list."""
return isinstance(config, list) and all(
isinstance(entry, dict) and CONF_PLATFORM in entry for entry in config
)
def _is_legacy_image_format(config: object) -> bool:
"""True when ``config`` matches a shape the pre-platform schema accepted.
Only these shapes are migrated. Anything else -- a list containing a
non-dict (or already platform-tagged) entry, or a dict with no recognised
image keys -- is left untouched so the platform validation surfaces a
proper error instead of the migration silently dropping the input.
"""
if isinstance(config, list):
# A bare list of (not-yet-platform-tagged) image dicts.
return bool(config) and all(
isinstance(entry, dict) and CONF_PLATFORM not in entry for entry in config
)
if not isinstance(config, dict):
return False
# A single image dict, or the grouped `defaults:`/`images:`/type-key form.
return (
CONF_ID in config
or CONF_FILE in config
or any(
key in (CONF_DEFAULTS, CONF_IMAGES) or key.upper() in IMAGE_TYPE
for key in config
)
)
def _flatten_legacy_image_config(config: object) -> list[dict]:
"""Structurally flatten a legacy ``image:`` config into image dicts.
No validation or file IO is performed -- the ``file`` platform schema
validates the resulting entries. Unrecognised shapes yield no entries so the
normal platform validation surfaces the error.
"""
if isinstance(config, list):
return [dict(entry) for entry in config if isinstance(entry, dict)]
if not isinstance(config, dict):
return []
if CONF_ID in config or CONF_FILE in config:
return [dict(config)]
defaults = config.get(CONF_DEFAULTS) or {}
result: list[dict] = []
def _add(entry: dict, extra: dict) -> None:
merged = {**defaults, **extra, **entry}
# The legacy `defaults:`/type-grouped forms only applied `byte_order` to
# types that support it. Replicate that so an endian default merged into
# e.g. a binary image stays valid.
type_class = IMAGE_TYPE.get(str(merged.get(CONF_TYPE, "")).upper())
if (
CONF_BYTE_ORDER in merged
and isinstance(type_class, type)
and issubclass(type_class, ImageEncoder)
and not type_class.is_endian()
):
del merged[CONF_BYTE_ORDER]
result.append(merged)
def _add_entries(entries: object, extra: dict) -> None:
# `entries` may be a single image dict or a list of them; non-dict
# members are silently skipped, mirroring the old `ensure_list` leniency.
for entry in [entries] if isinstance(entries, dict) else entries:
if isinstance(entry, dict):
_add(entry, extra)
_add_entries(config.get(CONF_IMAGES, []), {})
for key, value in config.items():
if key in (CONF_DEFAULTS, CONF_IMAGES) or key.upper() not in IMAGE_TYPE:
continue
type_extra = {CONF_TYPE: key}
if isinstance(value, dict) and (
transparency_keys := [k for k in value if k in TRANSPARENCY_TYPES]
):
for trans in transparency_keys:
_add_entries(value[trans], {**type_extra, CONF_TRANSPARENCY: trans})
elif isinstance(value, (list, dict)):
_add_entries(value, type_extra)
return result
def _migrate_legacy_image_config(config: object) -> list[dict] | None:
"""Rewrite a legacy ``image:`` config into the ``platform: file`` list.
Returns None for the already-migrated platform form and for any shape the
pre-platform schema never accepted, so normal platform validation can
surface a proper error instead of the migration silently discarding input.
"""
if _is_new_image_format(config) or not _is_legacy_image_format(config):
return None
migrated = [
{CONF_PLATFORM: PLATFORM_FILE, **entry}
for entry in _flatten_legacy_image_config(config)
]
from esphome import yaml_util
_LOGGER.warning(
"The 'image:' configuration format is deprecated and will be removed in "
"ESPHome %s. Images are now platforms of the 'image' component. Replace "
"your 'image:' block with:\n\n%s",
LEGACY_REMOVAL_VERSION,
yaml_util.dump({DOMAIN: migrated}),
)
return migrated
LEGACY_CONFIG_MIGRATE = _migrate_legacy_image_config
# --------------------------- end legacy migration --------------------------
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/components/improv_base/improv_base.h"
#include "esphome/components/logger/logger.h"
#include "esphome/components/wifi/wifi_component.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
@@ -66,53 +65,7 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
std::vector<uint8_t> build_rpc_settings_response_(improv::Command command);
std::vector<uint8_t> build_version_info_();
ESPHOME_ALWAYS_INLINE optional<uint8_t> read_byte_() {
optional<uint8_t> byte;
uint8_t data = 0;
#ifdef USE_ESP32
switch (this->uart_selection_) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
case logger::UART_SELECTION_UART2:
#endif
if (this->uart_num_ >= 0) {
size_t available;
uart_get_buffered_data_len(this->uart_num_, &available);
if (available) {
uart_read_bytes(this->uart_num_, &data, 1, 0);
byte = data;
}
}
break;
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
case logger::UART_SELECTION_USB_CDC:
if (esp_usb_console_available_for_read()) {
esp_usb_console_read_buf((char *) &data, 1);
byte = data;
}
break;
#endif
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case logger::UART_SELECTION_USB_SERIAL_JTAG: {
if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
byte = data;
}
break;
}
#endif
default:
break;
}
#elif defined(USE_ARDUINO)
if (this->hw_serial_->available()) {
this->hw_serial_->readBytes(&data, 1);
byte = data;
}
#endif
return byte;
}
optional<uint8_t> read_byte_();
void write_data_(const uint8_t *data = nullptr, size_t size = 0);
uint8_t tx_header_[TX_BUFFER_SIZE] = {
@@ -132,7 +85,6 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
#ifdef USE_ESP32
uart_port_t uart_num_;
logger::UARTSelection uart_selection_{logger::UART_SELECTION_UART0};
#elif defined(USE_ARDUINO)
Stream *hw_serial_{nullptr};
#endif
+1 -1
View File
@@ -10,7 +10,7 @@ static const char *const TAG = "kuntze";
static const uint8_t CMD_READ_REG = 0x03;
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
-3
View File
@@ -31,9 +31,6 @@
namespace esphome::ld24xx {
/// Parse a little-endian 16-bit unsigned value from two bytes.
static constexpr uint16_t two_byte_to_uint16(uint8_t low, uint8_t high) { return ((uint16_t) high << 8) | low; }
// Helper to find index of value in constexpr array
template<size_t N> optional<size_t> find_index(const uint32_t (&arr)[N], uint32_t value) {
for (size_t i = 0; i < N; i++) {
@@ -5,7 +5,7 @@
namespace esphome::libretiny {
static const char *const TAG = "libretiny.gpio";
static const char *const TAG = "lt.gpio";
static int IRAM_ATTR flags_to_mode(gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
@@ -6,7 +6,7 @@
namespace esphome::libretiny {
static const char *const TAG = "libretiny";
static const char *const TAG = "lt.component";
void LTComponent::dump_config() {
ESP_LOGCONFIG(TAG,
+1 -1
View File
@@ -155,7 +155,7 @@ void MideaIR::transmit_state() {
data.set_fahrenheit(this->fahrenheit_);
data.set_temp(this->target_temperature);
data.set_mode(this->mode);
data.set_fan_mode(this->fan_mode.value_or(ClimateFanMode::CLIMATE_FAN_ON));
data.set_fan_mode(this->fan_mode.value_or(ClimateFanMode::CLIMATE_FAN_AUTO));
data.set_sleep_preset(this->preset == climate::CLIMATE_PRESET_SLEEP);
data.fix();
this->transmit_(data);
+72
View File
@@ -10,6 +10,7 @@ from esphome.components.mipi import (
GMCTR,
GMCTRN1,
GMCTRP1,
IDMOFF,
IFCTR,
IFMODE,
INVCTR,
@@ -22,6 +23,7 @@ from esphome.components.mipi import (
PWCTR5,
PWSET,
PWSETN,
SETEXTC,
VMCTR,
VMCTR1,
VMCTR2,
@@ -30,6 +32,60 @@ from esphome.components.mipi import (
)
from esphome.components.spi import TYPE_OCTAL
DriverChip(
"M5CORE",
width=320,
height=240,
cs_pin=14,
dc_pin=27,
reset_pin=33,
initsequence=(
(SETEXTC, 0xFF, 0x93, 0x42),
(PWCTR1, 0x12, 0x12),
(PWCTR2, 0x03),
(VMCTR1, 0xF2),
(IFMODE, 0xE0),
(0xF6, 0x01, 0x00, 0x00),
(
GMCTRP1,
0x00,
0x0C,
0x11,
0x04,
0x11,
0x08,
0x37,
0x89,
0x4C,
0x06,
0x0C,
0x0A,
0x2E,
0x34,
0x0F,
),
(
GMCTRN1,
0x00,
0x0B,
0x11,
0x05,
0x13,
0x09,
0x33,
0x67,
0x48,
0x07,
0x0E,
0x0B,
0x2E,
0x33,
0x0F,
),
(DFUNCTR, 0x08, 0x82, 0x1D, 0x04),
(IDMOFF,),
),
)
ILI9341 = DriverChip(
"ILI9341",
mirror_x=True,
@@ -118,6 +174,22 @@ ILI9342 = DriverChip(
),
)
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
ILI9341.extend(
"M5CORE2",
# Reset native dimensions due to axis swap.
native_width=320,
native_height=240,
width=320,
height=240,
mirror_x=False,
cs_pin=5,
dc_pin=15,
invert_colors=True,
pixel_mode="18bit",
data_rate="40MHz",
)
DriverChip(
"ILI9481",
mirror_x=True,
@@ -1,71 +0,0 @@
from esphome.components.mipi import (
DFUNCTR,
GMCTRN1,
GMCTRP1,
IDMOFF,
IFMODE,
PWCTR1,
PWCTR2,
SETEXTC,
VMCTR1,
DriverChip,
)
from .ili import ILI9341, ST7789V
# fmt: off
DriverChip(
"M5CORE",
width=320,
height=240,
cs_pin=14,
dc_pin=27,
reset_pin=33,
initsequence=(
(SETEXTC, 0xFF, 0x93, 0x42),
(PWCTR1, 0x12, 0x12),
(PWCTR2, 0x03),
(VMCTR1, 0xF2),
(IFMODE, 0xE0),
(0xF6, 0x01, 0x00, 0x00),
(GMCTRP1, 0x00, 0x0C, 0x11, 0x04, 0x11, 0x08, 0x37, 0x89, 0x4C, 0x06, 0x0C, 0x0A, 0x2E, 0x34, 0x0F,),
(GMCTRN1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x2E, 0x33, 0x0F,),
(DFUNCTR, 0x08, 0x82, 0x1D, 0x04),
(IDMOFF,),
),
)
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
ILI9341.extend(
"M5CORE2",
# Reset native dimensions due to axis swap.
native_width=320,
native_height=240,
width=320,
height=240,
mirror_x=False,
cs_pin=5,
dc_pin=15,
invert_colors=True,
pixel_mode="18bit",
data_rate="40MHz",
)
GC9107 = ST7789V.extend(
"GC9107",
width=128,
height=128,
offset_width=2,
offset_height=1,
pad_width=2,
pad_height=1,
)
GC9107.extend(
"M5STACK-ATOMS3R-GC9107",
data_rate="40MHz",
invert_colors=True,
reset_pin=48,
dc_pin=42,
cs_pin=14,
)
+12 -13
View File
@@ -51,7 +51,7 @@ void ModbusClientHub::loop() {
// If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response
if (this->waiting_for_response_.has_value()) {
ModbusDeviceCommand &wfr = this->waiting_for_response_.value();
uint8_t expected_address = wfr.frame.data.data()[0];
uint8_t expected_address = wfr.frame.data.get()[0];
if (this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ &&
(this->rx_buffer_.empty() || this->rx_buffer_[0] != expected_address)) {
ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", expected_address,
@@ -270,8 +270,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t funct
// Check if the response matches the expected address and function code
ModbusDeviceCommand &wfr = this->waiting_for_response_.value();
uint8_t expected_address = wfr.frame.data.data()[0];
uint8_t expected_function_code = wfr.frame.data.data()[1];
uint8_t expected_address = wfr.frame.data.get()[0];
uint8_t expected_function_code = wfr.frame.data.get()[1];
if (expected_address != address || expected_function_code != (function_code & FUNCTION_CODE_MASK)) {
ESP_LOGW(TAG,
"Received incorrect frame address %" PRIu8 " <> %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32
@@ -458,7 +458,7 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
ESP_LOGE(TAG, "Attempted to send while transmission blocked");
return false;
}
if (frame.size() > MAX_FRAME_SIZE) {
if (frame.size > MAX_FRAME_SIZE) {
ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %" PRIu16 " bytes", MAX_FRAME_SIZE);
return false;
}
@@ -470,13 +470,13 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
if (this->flow_control_pin_ != nullptr) {
this->flow_control_pin_->digital_write(true);
this->write_array(frame.data.data(), frame.size());
this->write_array(frame.data.get(), frame.size);
this->flush();
this->flow_control_pin_->digital_write(false);
this->last_send_tx_offset_ = 0;
} else {
this->write_array(frame.data.data(), frame.size());
this->last_send_tx_offset_ = frame.size() * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
this->write_array(frame.data.get(), frame.size);
this->last_send_tx_offset_ = frame.size * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
}
uint32_t now = millis();
@@ -484,7 +484,7 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send, %" PRIu32 "ms after last receive",
format_hex_pretty_to(hex_buf, frame.data.data(), frame.size()), now - this->last_send_,
format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), now - this->last_send_,
now - this->last_modbus_byte_);
this->last_send_ = now;
return true;
@@ -590,13 +590,12 @@ void ModbusClientHub::queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t p
void ModbusClientHub::clear_tx_queue_for_address(uint8_t address, bool clear_sent) {
// Remove any pending commands for this address from the tx buffer
auto &tx_buffer = this->tx_buffer_;
tx_buffer.erase(
std::remove_if(tx_buffer.begin(), tx_buffer.end(),
[address](const ModbusDeviceCommand &cmd) { return cmd.frame.data.data()[0] == address; }),
tx_buffer.end());
tx_buffer.erase(std::remove_if(tx_buffer.begin(), tx_buffer.end(),
[address](const ModbusDeviceCommand &cmd) { return cmd.frame.data[0] == address; }),
tx_buffer.end());
if (clear_sent && this->waiting_for_response_.has_value() && this->waiting_for_response_.value().device) {
if (this->waiting_for_response_.value().frame.data.data()[0] == address) {
if (this->waiting_for_response_.value().frame.data[0] == address) {
ESP_LOGV(TAG, "Clearing waiting for response for address %" PRIu8, address);
// Invalidate the waiting device so it won't process a response.
this->waiting_for_response_.value().device = nullptr;
+10 -18
View File
@@ -18,27 +18,19 @@ namespace esphome::modbus {
static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15;
static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes
// (address + 5-byte PDU + 2-byte CRC) and fit inline with no heap allocation.
static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8;
struct ModbusFrame {
// Frame held in a small-buffer-optimized buffer. Typical frames fit inline; only larger
// multi-register or custom frames spill to a single heap allocation. This keeps the common,
// high-frequency tx traffic off the heap entirely, avoiding per-frame alloc/free churn.
// The buffer tracks its own length, so no separate size field is needed.
SmallInlineBuffer<MODBUS_FRAME_INLINE_SIZE> data; // Modbus RTU max is 256 bytes
// Frame with exact-size allocation to avoid std::vector overhead
std::unique_ptr<uint8_t[]> data;
uint16_t size; // Modbus RTU max is 256 bytes
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) {
uint8_t *buf = this->data.init(pdu_len + 3);
buf[0] = address;
memcpy(buf + 1, pdu, pdu_len);
auto crc = crc16(buf, pdu_len + 1);
buf[pdu_len + 1] = crc >> 0;
buf[pdu_len + 2] = crc >> 8;
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len)
: data(std::make_unique<uint8_t[]>(pdu_len + 3)), size(pdu_len + 3) {
data[0] = address;
memcpy(data.get() + 1, pdu, pdu_len);
auto crc = crc16(data.get(), pdu_len + 1);
data[pdu_len + 1] = crc >> 0;
data[pdu_len + 2] = crc >> 8;
}
uint16_t size() const { return static_cast<uint16_t>(this->data.size()); }
};
class Modbus : public uart::UARTDevice, public Component {
-13
View File
@@ -59,19 +59,6 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
return IPAddress(str(ip))
def add_use_address(var: cg.MockObj, use_address: str) -> None:
"""Generate a set_use_address() call only when the address must be baked in.
The default "<name>.local" is not stored in the firmware; it is rebuilt at
runtime from the device name (see network::get_use_address_to()), which also
picks up the MAC suffix when name_add_mac_suffix is enabled. A compile-time
string could never include that suffix, so baking it in would log the wrong
address.
"""
if use_address != f"{CORE.name}.local":
cg.add(var.set_use_address(use_address))
def require_high_performance_networking() -> None:
"""Request high performance networking for network and WiFi.
-23
View File
@@ -1,7 +1,5 @@
#include "util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_NETWORK
namespace esphome::network {
@@ -22,27 +20,6 @@ bool is_disabled() {
return false;
}
const char *get_use_address_to(std::span<char, USE_ADDRESS_BUFFER_SIZE> buf) {
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
const char *addr = nullptr;
#if defined(USE_ETHERNET)
addr = ethernet::global_eth_component->get_use_address();
#elif defined(USE_MODEM)
addr = modem::global_modem_component->get_use_address();
#elif defined(USE_WIFI)
addr = wifi::global_wifi_component->get_use_address();
#elif defined(USE_OPENTHREAD)
addr = openthread::global_openthread_component->get_use_address();
#endif
if (addr != nullptr && addr[0] != '\0')
return addr;
// No explicit use_address configured: the address is the runtime device name
// (which includes the MAC suffix when name_add_mac_suffix is enabled) plus ".local"
const auto &name = App.get_name();
make_name_with_suffix_to(buf.data(), buf.size(), name.c_str(), name.size(), '.', "local", 5);
return buf.data();
}
network::IPAddresses get_ip_addresses() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr)
+24 -7
View File
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#include <span>
#include <string>
#include "esphome/core/helpers.h"
#include "ip_address.h"
@@ -54,12 +53,30 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() {
/// Return whether the network is disabled (only wifi for now)
bool is_disabled();
/// Buffer size for get_use_address_to(): 63-char DNS label + ".local" + null terminator
static constexpr size_t USE_ADDRESS_BUFFER_SIZE = 70;
/// Get the active network address for logging. Returns the explicitly configured
/// use_address when one was set, otherwise formats "<name>.local" from the runtime
/// device name into buf (so it includes the MAC suffix from name_add_mac_suffix).
const char *get_use_address_to(std::span<char, USE_ADDRESS_BUFFER_SIZE> buf);
/// Get the active network hostname
ESPHOME_ALWAYS_INLINE inline const char *get_use_address() {
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
#ifdef USE_ETHERNET
return ethernet::global_eth_component->get_use_address();
#endif
#ifdef USE_MODEM
return modem::global_modem_component->get_use_address();
#endif
#ifdef USE_WIFI
return wifi::global_wifi_component->get_use_address();
#endif
#ifdef USE_OPENTHREAD
return openthread::global_openthread_component->get_use_address();
#endif
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD)
// Fallback when no network component is defined (e.g., host platform)
return "";
#endif
}
IPAddresses get_ip_addresses();
} // namespace esphome::network
+21 -136
View File
@@ -1,150 +1,35 @@
import logging
# ---------------------------------------------------------------------------
# Legacy top-level `online_image:` deprecation shim -- REMOVE this whole file
# after 2027.1.0.
#
# Online images are now a platform of the `image:` component (`platform:
# online_image`); the real schema, actions and codegen live in `image.py`. This
# module only keeps the deprecated top-level `online_image:` key working during
# the deprecation window: it reuses that schema/codegen and adds a one-shot
# deprecation warning (with a pasteable migrated `image:` block) at validation
# time. Deleting this file drops the top-level form entirely.
# ---------------------------------------------------------------------------
from esphome import automation
import esphome.codegen as cg
from esphome.components import runtime_image
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import CONF_TRANSPARENCY, add_metadata
import esphome.components.image as espImage
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL
from esphome.core import Lambda
from .image import ONLINE_IMAGE_CONFIG_SCHEMA, setup_online_image
AUTO_LOAD = ["image", "runtime_image"]
DEPENDENCIES = ["display", "http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
MULTI_CONF = True
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_UPDATE = "update"
DOMAIN = "online_image"
_LOGGER = logging.getLogger(__name__)
LEGACY_REMOVAL_VERSION = "2027.1.0"
online_image_ns = cg.esphome_ns.namespace("online_image")
OnlineImage = online_image_ns.class_(
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
_capture_legacy_entry, _warn_legacy_online_image = (
espImage.legacy_platform_migration_warning(DOMAIN, DOMAIN, LEGACY_REMOVAL_VERSION)
)
# Actions
SetUrlAction = online_image_ns.class_(
"OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage)
)
ReleaseImageAction = online_image_ns.class_(
"OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage)
)
CONFIG_SCHEMA = cv.All(_capture_legacy_entry, ONLINE_IMAGE_CONFIG_SCHEMA)
FINAL_VALIDATE_SCHEMA = _warn_legacy_online_image
ONLINE_IMAGE_SCHEMA = (
runtime_image.runtime_image_schema(OnlineImage)
.extend(
{
# Online Image specific options
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)})
),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
}
)
.extend(cv.polling_component_schema("never"))
)
CONFIG_SCHEMA = cv.Schema(
cv.All(
ONLINE_IMAGE_SCHEMA,
cv.require_framework_version(
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
# esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(4, 0, 0),
rp2_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
),
runtime_image.validate_runtime_image_settings,
)
)
SET_URL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(OnlineImage),
cv.Required(CONF_URL): cv.templatable(cv.url),
cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool),
}
)
RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(OnlineImage),
}
)
@automation.register_action(
"online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True
)
@automation.register_action(
"online_image.release",
ReleaseImageAction,
RELEASE_IMAGE_SCHEMA,
synchronous=True,
)
async def online_image_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_URL in config:
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
if CONF_UPDATE in config:
template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_)
cg.add(var.set_update(template_))
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
),
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
)
async def to_code(config):
# Use the enhanced helper function to get all runtime image parameters
settings = await runtime_image.process_runtime_image_config(config)
add_metadata(
config[CONF_ID],
settings.width,
settings.height,
config[CONF_TYPE],
config[CONF_TRANSPARENCY],
)
url = config[CONF_URL]
var = cg.new_Pvariable(
config[CONF_ID],
url,
settings.width,
settings.height,
settings.format_enum,
settings.image_type_enum,
settings.transparent,
settings.placeholder or cg.nullptr,
config[CONF_BUFFER_SIZE],
settings.byte_order_big_endian,
)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
for key, value in config.get(CONF_REQUEST_HEADERS, {}).items():
if isinstance(value, Lambda):
template_ = await cg.templatable(value, [], cg.std_string)
cg.add(var.add_request_header(key, template_))
else:
cg.add(var.add_request_header(key, value))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
to_code = setup_online_image
+152
View File
@@ -0,0 +1,152 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import runtime_image
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import CONF_TRANSPARENCY, add_metadata
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL
from esphome.core import Lambda
from esphome.types import ConfigType
AUTO_LOAD = ["runtime_image"]
DEPENDENCIES = ["http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_UPDATE = "update"
online_image_ns = cg.esphome_ns.namespace("online_image")
OnlineImage = online_image_ns.class_(
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
)
# Actions
SetUrlAction = online_image_ns.class_(
"OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage)
)
ReleaseImageAction = online_image_ns.class_(
"OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage)
)
ONLINE_IMAGE_SCHEMA = (
runtime_image.runtime_image_schema(OnlineImage)
.extend(
{
# Online Image specific options
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)})
),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
}
)
.extend(cv.polling_component_schema("never"))
)
# Shared schema used by both the (deprecated) top-level `online_image:` key and
# the `image:` `platform: online_image` entry.
ONLINE_IMAGE_CONFIG_SCHEMA = cv.All(
ONLINE_IMAGE_SCHEMA,
cv.require_framework_version(
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
# esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(4, 0, 0),
rp2_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
),
runtime_image.validate_runtime_image_settings,
)
SET_URL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(OnlineImage),
cv.Required(CONF_URL): cv.templatable(cv.url),
cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool),
}
)
RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(OnlineImage),
}
)
@automation.register_action(
"online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True
)
@automation.register_action(
"online_image.release",
ReleaseImageAction,
RELEASE_IMAGE_SCHEMA,
synchronous=True,
)
async def online_image_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_URL in config:
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
if CONF_UPDATE in config:
template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_)
cg.add(var.set_update(template_))
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
),
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
)
async def setup_online_image(config: ConfigType) -> None:
# Use the enhanced helper function to get all runtime image parameters
settings = await runtime_image.process_runtime_image_config(config)
add_metadata(
config[CONF_ID],
settings.width,
settings.height,
config[CONF_TYPE],
config[CONF_TRANSPARENCY],
)
url = config[CONF_URL]
var = cg.new_Pvariable(
config[CONF_ID],
url,
settings.width,
settings.height,
settings.format_enum,
settings.image_type_enum,
settings.transparent,
settings.placeholder or cg.nullptr,
config[CONF_BUFFER_SIZE],
settings.byte_order_big_endian,
)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
for key, value in config.get(CONF_REQUEST_HEADERS, {}).items():
if isinstance(value, Lambda):
template_ = await cg.templatable(value, [], cg.std_string)
cg.add(var.add_request_header(key, template_))
else:
cg.add(var.add_request_header(key, value))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
CONFIG_SCHEMA = ONLINE_IMAGE_CONFIG_SCHEMA
to_code = setup_online_image
+1 -2
View File
@@ -14,7 +14,6 @@ from esphome.components.esp32 import (
require_vfs_select,
)
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
from esphome.components.network import add_use_address
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
@@ -289,7 +288,7 @@ async def to_code(config):
enable_mdns_storage()
ot = cg.new_Pvariable(config[CONF_ID])
add_use_address(ot, config[CONF_USE_ADDRESS])
cg.add(ot.set_use_address(config[CONF_USE_ADDRESS]))
await cg.register_component(ot, config)
if (poll_period := config.get(CONF_POLL_PERIOD)) is not None:
cg.add(ot.set_poll_period(poll_period))
+1 -3
View File
@@ -39,8 +39,6 @@ class OpenThreadComponent final : public Component {
void on_factory_reset(std::function<void()> callback);
void defer_factory_reset_external_callback();
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
#if CONFIG_OPENTHREAD_MTD
@@ -78,7 +76,7 @@ class OpenThreadComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{nullptr};
const char *use_address_{""};
};
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-28
View File
@@ -2,34 +2,6 @@
namespace esphome::ota {
bool version_is_older(const char *candidate, const char *reference) {
if (candidate == nullptr || reference == nullptr)
return false;
while (true) {
uint32_t a = 0;
while (*candidate >= '0' && *candidate <= '9') {
a = a * 10 + static_cast<uint32_t>(*candidate - '0');
candidate++;
}
uint32_t b = 0;
while (*reference >= '0' && *reference <= '9') {
b = b * 10 + static_cast<uint32_t>(*reference - '0');
reference++;
}
if (a != b)
return a < b;
// Components equal so far; advance past a single separator on each side.
const bool a_more = (*candidate == '.');
const bool b_more = (*reference == '.');
if (a_more)
candidate++;
if (b_more)
reference++;
if (!a_more && !b_more)
return false; // Both strings exhausted with all components equal.
}
}
#ifdef USE_OTA_STATE_LISTENER
OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-15
View File
@@ -46,24 +46,9 @@ enum OTAResponseTypes {
OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90,
OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91,
OTA_RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92,
OTA_RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93,
OTA_RESPONSE_ERROR_UNKNOWN = 0xFF,
};
/** Compare two dotted-numeric version strings (such as "1.2.3").
*
* Returns true when @p candidate represents a strictly older (lower) firmware
* version than @p reference. Each dot-separated component is parsed as an
* integer and compared left-to-right; absent trailing components count as 0,
* so "1.2" and "1.2.0" are equal. Equal versions return false so that
* re-flashing the same version is permitted.
*
* Used for software OTA downgrade protection. Inputs come from the project
* version embedded in the signed firmware image, which is validated to be
* dotted-numeric at config time. Non-digit characters terminate a component.
*/
bool version_is_older(const char *candidate, const char *reference);
enum OTAState {
OTA_COMPLETED = 0,
OTA_STARTED,
@@ -9,9 +9,6 @@
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <spi_flash_mmap.h>
#ifdef USE_OTA_DOWNGRADE_PROTECTION
#include <esp_app_desc.h>
#endif
namespace esphome::ota {
@@ -162,23 +159,6 @@ OTAResponseTypes IDFOTABackend::end() {
}
#endif
if (err == ESP_OK) {
#ifdef USE_OTA_DOWNGRADE_PROTECTION
// The image is written and (when signing is enabled) signature-verified by
// esp_ota_end(), so its embedded project version can be trusted. Reject the
// update if it is older than the running version by leaving the boot
// partition unchanged -- the staged image simply never boots.
esp_app_desc_t incoming;
esp_err_t desc_err = esp_ota_get_partition_description(this->partition_, &incoming);
if (desc_err != ESP_OK) {
// Couldn't read the staged image's version, so the comparison is skipped.
// Warn so the bypassed check is observable rather than silent.
ESP_LOGW(TAG, "Downgrade protection: could not read image version (err=0x%X); allowing update", desc_err);
} else if (version_is_older(incoming.version, ESPHOME_PROJECT_VERSION)) {
ESP_LOGE(TAG, "Rejecting downgrade: image version '%s' is older than running version '%s'", incoming.version,
ESPHOME_PROJECT_VERSION);
return OTA_RESPONSE_ERROR_VERSION_DOWNGRADE;
}
#endif
err = esp_ota_set_boot_partition(this->partition_);
if (err == ESP_OK) {
return OTA_RESPONSE_OK;
+1
View File
@@ -10,6 +10,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
CONF_ENABLE_FULL_PRINTF,
CONF_FRAMEWORK,
CONF_PLATFORM_VERSION,
CONF_SOURCE,
-1
View File
@@ -1,6 +1,5 @@
import esphome.codegen as cg
CONF_ENABLE_FULL_PRINTF = "enable_full_printf"
KEY_BOARD = "board"
KEY_LWIP_OPTS = "lwip_opts"
KEY_RP2 = "rp2"
@@ -81,19 +81,6 @@ void RuntimeStatsCollector::log_stats_() {
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);
uint64_t sched = this->period_before_sched_time_us_;
uint64_t wdt = this->period_before_wdt_time_us_;
uint64_t before_residual = before > sched + wdt ? before - sched - wdt : 0;
ESP_LOGI(TAG, " main_loop_before_breakdown: sched=%.1fms, wdt=%.1fms, residual=%.1fms",
static_cast<double>(sched) / 1000.0, static_cast<double>(wdt) / 1000.0,
static_cast<double>(before_residual) / 1000.0);
uint32_t wdt_slow_hits = App.wdt_slow_count_;
App.wdt_slow_count_ = 0;
ESP_LOGI(TAG, " wdt_slow_path: hits=%" PRIu32 " (%.1f%% of iters, avg=%.2fus/hit)", wdt_slow_hits,
this->period_active_count_ > 0
? 100.0 * static_cast<double>(wdt_slow_hits) / static_cast<double>(this->period_active_count_)
: 0.0,
wdt_slow_hits > 0 ? static_cast<double>(wdt) / static_cast<double>(wdt_slow_hits) : 0.0);
}
// Log total stats since boot (only for active components - idle ones haven't changed)
@@ -129,12 +116,6 @@ void RuntimeStatsCollector::log_stats_() {
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);
uint64_t sched = this->total_before_sched_time_us_;
uint64_t wdt = this->total_before_wdt_time_us_;
uint64_t before_residual = before > sched + wdt ? before - sched - wdt : 0;
ESP_LOGI(TAG, " main_loop_before_breakdown: sched=%.1fms, wdt=%.1fms, residual=%.1fms",
static_cast<double>(sched) / 1000.0, static_cast<double>(wdt) / 1000.0,
static_cast<double>(before_residual) / 1000.0);
}
// Reset period stats
@@ -145,8 +126,6 @@ void RuntimeStatsCollector::log_stats_() {
this->period_active_time_us_ = 0;
this->period_active_max_us_ = 0;
this->period_before_time_us_ = 0;
this->period_before_sched_time_us_ = 0;
this->period_before_wdt_time_us_ = 0;
this->period_tail_time_us_ = 0;
}
@@ -49,8 +49,7 @@ class RuntimeStatsCollector final {
// which captures per-iteration inter-component bookkeeping (set_current_component,
// LoopBlockingGuard 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 sched_us, uint32_t wdt_us,
uint32_t tail_us) {
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_)
@@ -62,10 +61,6 @@ class RuntimeStatsCollector final {
this->period_before_time_us_ += before_us;
this->total_before_time_us_ += before_us;
this->period_before_sched_time_us_ += sched_us;
this->total_before_sched_time_us_ += sched_us;
this->period_before_wdt_time_us_ += wdt_us;
this->total_before_wdt_time_us_ += wdt_us;
this->period_tail_time_us_ += tail_us;
this->total_tail_time_us_ += tail_us;
}
@@ -93,11 +88,6 @@ class RuntimeStatsCollector final {
// Split of overhead sections — accumulated per iteration.
uint64_t period_before_time_us_{0};
uint64_t total_before_time_us_{0};
// Sub-split of `before` into scheduler_tick_ vs. post-scheduler feed_wdt_with_time.
uint64_t period_before_sched_time_us_{0};
uint64_t total_before_sched_time_us_{0};
uint64_t period_before_wdt_time_us_{0};
uint64_t total_before_wdt_time_us_{0};
uint64_t period_tail_time_us_{0};
uint64_t total_tail_time_us_{0};
};
+1 -1
View File
@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SPEED): cv.invalid(
"Configuring individual speeds is deprecated."
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)
+2 -2
View File
@@ -9,7 +9,7 @@ namespace esphome::speed {
class SpeedFan final : public Component, public fan::Fan {
public:
SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {}
SpeedFan(int speed_count) : speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_output(output::FloatOutput *output) { this->output_ = output; }
@@ -28,7 +28,7 @@ class SpeedFan final : public Component, public fan::Fan {
output::FloatOutput *output_;
output::BinaryOutput *oscillating_{nullptr};
output::BinaryOutput *direction_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
fan::FanTraits traits_;
};
+1 -1
View File
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
{
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)
@@ -24,7 +24,7 @@ class TemplateFan final : public Component, public fan::Fan {
bool has_oscillating_{false};
bool has_direction_{false};
uint8_t speed_count_{0};
int speed_count_{0};
fan::FanTraits traits_;
};
+1 -1
View File
@@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
}
)
.extend(cv.COMPONENT_SCHEMA),
+2 -2
View File
@@ -8,7 +8,7 @@ namespace esphome::tuya {
class TuyaFan final : public Component, public fan::Fan {
public:
TuyaFan(Tuya *parent, uint8_t speed_count) : parent_(parent), speed_count_(speed_count) {}
TuyaFan(Tuya *parent, int speed_count) : parent_(parent), speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
@@ -26,7 +26,7 @@ class TuyaFan final : public Component, public fan::Fan {
optional<uint8_t> switch_id_{};
optional<uint8_t> oscillation_id_{};
optional<uint8_t> direction_id_{};
uint8_t speed_count_{};
int speed_count_{};
TuyaDatapointType speed_type_{};
TuyaDatapointType oscillation_type_{};
};
+12 -14
View File
@@ -257,10 +257,8 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
}
// used for logs plus the initial ping/config
void DeferredUpdateEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event,
uint32_t id, uint32_t reconnect) {
// ESPAsyncWebServer's send() only accepts null-terminated strings
(void) message_len;
void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id,
uint32_t reconnect) {
this->send(message, event, id, reconnect);
}
@@ -281,10 +279,10 @@ void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const ch
}
}
void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, size_t message_len, const char *event,
uint32_t id, uint32_t reconnect) {
void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, const char *event, uint32_t id,
uint32_t reconnect) {
for (DeferredUpdateEventSource *dues : *this) {
dues->try_send_nodefer(message, message_len, event, id, reconnect);
dues->try_send_nodefer(message, event, id, reconnect);
}
}
@@ -306,7 +304,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
// Configure reconnect timeout and send config
// this should always go through since the AsyncEventSourceClient event queue is empty on connect
auto message = ws->get_config_json();
source->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000);
source->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) {
@@ -317,7 +315,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
auto group_msg = builder.serialize();
// up to 31 groups should be able to be queued initially without defer
source->try_send_nodefer(group_msg.c_str(), group_msg.size(), "sorting_group");
source->try_send_nodefer(group_msg.c_str(), "sorting_group");
}
#endif
@@ -397,8 +395,8 @@ void WebServer::setup() {
return;
char buf[32];
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
size_t len = buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
this->events_.try_send_nodefer(buf, len, "ping", millis(), 30000);
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
});
}
void WebServer::loop() {
@@ -416,16 +414,16 @@ void WebServer::loop() {
void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
(void) level;
(void) tag;
this->events_.try_send_nodefer(message, message_len, "log", millis());
(void) message_len;
this->events_.try_send_nodefer(message, "log", millis());
}
#endif
void WebServer::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Web Server:\n"
" Address: %s:%u",
network::get_use_address_to(addr_buf), this->base_->get_port());
network::get_use_address(), this->base_->get_port());
}
float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
+2 -4
View File
@@ -160,8 +160,7 @@ class DeferredUpdateEventSource final : public AsyncEventSource {
void loop();
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
};
class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEventSource *> {
@@ -174,8 +173,7 @@ class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEvent
bool loop();
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void add_new_client(WebServer *ws, AsyncWebServerRequest *request);
};
@@ -546,11 +546,10 @@ void AsyncEventSource::adopt_pending_sessions_main_loop_() {
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
uint32_t reconnect) {
void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
for (auto *ses : this->sessions_) {
if (ses->fd_.load() != 0) { // Skip dead sessions
ses->try_send_nodefer(message, message_len, event, id, reconnect);
ses->try_send_nodefer(message, event, id, reconnect);
}
}
}
@@ -601,7 +600,7 @@ void AsyncEventSourceResponse::start_session_main_loop_() {
// tcp send buffer is empty on connect, so these should always go through
auto message = ws->get_config_json();
this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000);
this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) {
@@ -613,7 +612,7 @@ void AsyncEventSourceResponse::start_session_main_loop_() {
// a (very) large number of these should be able to be queued initially without defer
// since the only thing in the send buffer at this point is the initial ping/config
this->try_send_nodefer(message.c_str(), message.size(), "sorting_group");
this->try_send_nodefer(message.c_str(), "sorting_group");
}
#endif
@@ -648,7 +647,7 @@ void AsyncEventSourceResponse::process_deferred_queue_() {
while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front();
auto message = de.message_generator_(web_server_, de.source_);
if (this->try_send_nodefer(message.c_str(), message.size(), "state")) {
if (this->try_send_nodefer(message.c_str(), "state")) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin());
} else {
@@ -719,7 +718,7 @@ void AsyncEventSourceResponse::loop() {
this->entities_iterator_.advance();
}
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id,
uint32_t reconnect) {
if (this->fd_.load() == 0) {
return false;
@@ -765,18 +764,19 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t mess
// Fast path: check if message contains any newlines at all
// Most SSE messages (JSON state updates) have no newlines
const char *first_n = static_cast<const char *>(memchr(message, '\n', message_len));
const char *first_r = static_cast<const char *>(memchr(message, '\r', message_len));
const char *first_n = strchr(message, '\n');
const char *first_r = strchr(message, '\r');
if (first_n == nullptr && first_r == nullptr) {
// No newlines - fast path (most common case)
event_buffer_.append("data: ", sizeof("data: ") - 1);
event_buffer_.append(message, message_len);
event_buffer_.append(message);
event_buffer_.append(CRLF_STR CRLF_STR, CRLF_LEN * 2); // data line + blank line terminator
} else {
// Has newlines - handle multi-line message
const char *line_start = message;
const char *msg_end = message + message_len;
size_t msg_len = strlen(message);
const char *msg_end = message + msg_len;
// Reuse the first search results
const char *next_n = first_n;
@@ -789,7 +789,7 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t mess
if (next_n == nullptr && next_r == nullptr) {
// No more line breaks - output remaining text as final line
event_buffer_.append("data: ", sizeof("data: ") - 1);
event_buffer_.append(line_start, msg_end - line_start);
event_buffer_.append(line_start);
event_buffer_.append(CRLF_STR, CRLF_LEN);
break;
}
@@ -828,8 +828,8 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t mess
}
// Search for next newlines only in remaining string
next_n = static_cast<const char *>(memchr(line_start, '\n', msg_end - line_start));
next_r = static_cast<const char *>(memchr(line_start, '\r', msg_end - line_start));
next_n = strchr(line_start, '\n');
next_r = strchr(line_start, '\r');
}
// Terminate message with blank line
@@ -884,7 +884,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
deq_push_back_with_dedup_(source, message_generator);
} else {
auto message = message_generator(web_server_, source);
if (!this->try_send_nodefer(message.c_str(), message.size(), "state")) {
if (!this->try_send_nodefer(message.c_str(), "state")) {
deq_push_back_with_dedup_(source, message_generator);
}
}
@@ -291,8 +291,7 @@ class AsyncEventSourceResponse {
friend class AsyncEventSource;
public:
bool try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
bool try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
void loop();
@@ -344,8 +343,7 @@ class AsyncEventSource : public AsyncWebHandler {
// NOLINTNEXTLINE(readability-identifier-naming)
void onConnect(connect_handler_t &&cb) { this->on_connect_ = std::move(cb); }
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
/// Returns true if there are sessions remaining (including pending cleanup).
bool loop();
+1 -2
View File
@@ -14,7 +14,6 @@ from esphome.components.esp32 import (
request_wifi,
)
from esphome.components.network import (
add_use_address,
has_high_performance_networking,
ip_address_literal,
)
@@ -586,7 +585,7 @@ def wifi_network(config, ap, static_ip):
@coroutine_with_priority(CoroPriority.COMMUNICATION)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
add_use_address(var, config[CONF_USE_ADDRESS])
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
# Track if any network uses Enterprise authentication
has_eap = False
+1 -3
View File
@@ -501,8 +501,6 @@ class WiFiComponent final : public Component {
network::IPAddress get_dns_address(int num);
network::IPAddresses get_ip_addresses();
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
@@ -998,7 +996,7 @@ class WiFiComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{nullptr};
const char *use_address_{""};
};
extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -39,7 +39,6 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/lock_free_queue.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
+1 -1
View File
@@ -8,7 +8,7 @@ namespace esphome::wts01 {
constexpr uint8_t PACKET_SIZE = 9;
class WTS01Sensor final : public sensor::Sensor, public uart::UARTDevice, public Component {
class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Component {
public:
void loop() override;
void dump_config() override;
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::x9c {
class X9cOutput final : public output::FloatOutput, public Component {
class X9cOutput : public output::FloatOutput, public Component {
public:
void set_cs_pin(InternalGPIOPin *pin) { cs_pin_ = pin; }
void set_inc_pin(InternalGPIOPin *pin) { inc_pin_ = pin; }
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::xdb401 {
class XDB401Component final : public PollingComponent, public i2c::I2CDevice {
class XDB401Component : public PollingComponent, public i2c::I2CDevice {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
+1 -1
View File
@@ -20,7 +20,7 @@ enum XGZP68XXOversampling : uint8_t {
XGZP68XX_OVERSAMPLING_UNKNOWN = (uint8_t) -1,
};
class XGZP68XXComponent final : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice {
class XGZP68XXComponent : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice {
public:
SUB_SENSOR(temperature)
SUB_SENSOR(pressure)
+1 -1
View File
@@ -72,7 +72,7 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address);
bool report_xiaomi_results(const optional<XiaomiParseResult> &result, const char *address);
class XiaomiListener final : public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener {
public:
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
};
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_cgdk2 {
class XiaomiCGDK2 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
void set_bindkey(const char *bindkey);
+1 -1
View File
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_cgg1 {
class XiaomiCGG1 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
void set_bindkey(const char *bindkey);
@@ -10,9 +10,9 @@
namespace esphome::xiaomi_cgpr1 {
class XiaomiCGPR1 final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGPR1 : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
void set_bindkey(const char *bindkey);
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_gcls002 {
class XiaomiGCLS002 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_hhccjcy01 {
class XiaomiHHCCJCY01 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -8,7 +8,7 @@
namespace esphome::xiaomi_hhccjcy10 {
class XiaomiHHCCJCY10 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_hhccpot002 {
class XiaomiHHCCPOT002 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_jqjcy01ym {
class XiaomiJQJCY01YM final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_lywsd02 {
class XiaomiLYWSD02 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_lywsd02mmc {
class XiaomiLYWSD02MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(const char *bindkey);
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_lywsd03mmc {
class XiaomiLYWSD03MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
void set_bindkey(const char *bindkey);
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_lywsdcgq {
class XiaomiLYWSDCGQ final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_mhoc303 {
class XiaomiMHOC303 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_mhoc401 {
class XiaomiMHOC401 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
void set_bindkey(const char *bindkey);
@@ -16,7 +16,7 @@ struct ParseResult {
optional<float> impedance;
};
class XiaomiMiscale final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
@@ -10,9 +10,9 @@
namespace esphome::xiaomi_mjyd02yla {
class XiaomiMJYD02YLA final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMJYD02YLA : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
void set_bindkey(const char *bindkey);
@@ -9,9 +9,9 @@
namespace esphome::xiaomi_mue4094rt {
class XiaomiMUE4094RT final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMUE4094RT : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -15,7 +15,7 @@
namespace esphome::xiaomi_rtcgq02lm {
class XiaomiRTCGQ02LM final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
void set_bindkey(const char *bindkey);
@@ -10,9 +10,9 @@
namespace esphome::xiaomi_wx08zm {
class XiaomiWX08ZM final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiWX08ZM : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_xmwsdj04mmc {
class XiaomiXMWSDJ04MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(const char *bindkey);
+2 -2
View File
@@ -17,7 +17,7 @@ enum {
XL9535_CONFIG_PORT_1_REGISTER = 0x07,
};
class XL9535Component final : public Component, public i2c::I2CDevice {
class XL9535Component : public Component, public i2c::I2CDevice {
public:
bool digital_read(uint8_t pin);
void digital_write(uint8_t pin, bool value);
@@ -28,7 +28,7 @@ class XL9535Component final : public Component, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::IO; }
};
class XL9535GPIOPin final : public GPIOPin {
class XL9535GPIOPin : public GPIOPin {
public:
void set_parent(XL9535Component *parent) { this->parent_ = parent; }
void set_pin(uint8_t pin) { this->pin_ = pin; }
@@ -11,9 +11,9 @@ namespace esphome::xpt2046 {
using namespace touchscreen;
class XPT2046Component final : public Touchscreen,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
class XPT2046Component : public Touchscreen,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
public:
/// Set the threshold for the touch detection.
void set_threshold(int16_t threshold) { this->threshold_ = threshold; }
+1 -1
View File
@@ -9,7 +9,7 @@
namespace esphome::yashima {
class YashimaClimate final : public climate::Climate, public Component {
class YashimaClimate : public climate::Climate, public Component {
public:
void setup() override;
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
+1 -1
View File
@@ -7,7 +7,7 @@
namespace esphome::zephyr {
class CdcAcm final : public Component {
class CdcAcm : public Component {
public:
CdcAcm();
void setup() override;
+1 -1
View File
@@ -16,7 +16,7 @@ struct ZephyrGPIOInterrupt {
void *arg{nullptr};
};
class ZephyrGPIOPin final : public InternalGPIOPin {
class ZephyrGPIOPin : public InternalGPIOPin {
public:
ZephyrGPIOPin(const device *gpio, int gpio_size, const char *pin_name_prefix) {
this->gpio_ = gpio;

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