mirror of
https://github.com/esphome/esphome.git
synced 2026-07-08 07:55:34 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 014131233e | |||
| cb0c428d6a | |||
| 1d49daa90c | |||
| 0b1e3283d5 | |||
| d1938a87aa | |||
| dc5e6a20af |
@@ -1,3 +1,5 @@
|
||||
# Normalize line endings to LF in the repository
|
||||
* text eol=lf
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.apng binary
|
||||
|
||||
@@ -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
@@ -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
@@ -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)")
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
# pylint: disable=unused-import
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
ComponentMarker,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
LineComment,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace esphome::zephyr {
|
||||
|
||||
class CdcAcm final : public Component {
|
||||
class CdcAcm : public Component {
|
||||
public:
|
||||
CdcAcm();
|
||||
void setup() override;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user