mirror of
https://github.com/esphome/esphome.git
synced 2026-07-07 15:39:36 +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 \
|
||||
|
||||
@@ -8,7 +8,7 @@ memory-constrained platforms like ESP8266.
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -65,7 +65,6 @@ class RamSymbol:
|
||||
size: int
|
||||
section: str
|
||||
demangled: str = "" # Demangled name, set after batch demangling
|
||||
aliases: list[str] = field(default_factory=list) # Other names at same address
|
||||
|
||||
|
||||
class RamStringsAnalyzer:
|
||||
@@ -236,11 +235,6 @@ class RamStringsAnalyzer:
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return
|
||||
|
||||
# Track symbols by address so aliases (multiple names for the same
|
||||
# object, e.g. the newlib __lock___* mutexes that all alias one
|
||||
# StaticSemaphore_t) are reported once instead of once per name.
|
||||
symbols_by_addr: dict[int, RamSymbol] = {}
|
||||
|
||||
for line in output.split("\n"):
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
@@ -259,18 +253,6 @@ class RamStringsAnalyzer:
|
||||
if sym_type not in DATA_SYMBOL_TYPES:
|
||||
continue
|
||||
|
||||
if (existing := symbols_by_addr.get(addr)) is not None:
|
||||
# Prefer a global (uppercase type) name as the primary so
|
||||
# nm output order can't hide it behind a local alias.
|
||||
if sym_type.isupper() and existing.sym_type.islower():
|
||||
existing.aliases.append(existing.name)
|
||||
existing.name = name
|
||||
existing.sym_type = sym_type
|
||||
else:
|
||||
existing.aliases.append(name)
|
||||
existing.size = max(existing.size, size)
|
||||
continue
|
||||
|
||||
# Check if symbol is in a RAM section
|
||||
for section_name in self.ram_sections:
|
||||
if section_name not in self.sections:
|
||||
@@ -278,15 +260,15 @@ class RamStringsAnalyzer:
|
||||
|
||||
section = self.sections[section_name]
|
||||
if section.address <= addr < section.address + section.size:
|
||||
symbol = RamSymbol(
|
||||
name=name,
|
||||
sym_type=sym_type,
|
||||
address=addr,
|
||||
size=size,
|
||||
section=section_name,
|
||||
self.ram_symbols.append(
|
||||
RamSymbol(
|
||||
name=name,
|
||||
sym_type=sym_type,
|
||||
address=addr,
|
||||
size=size,
|
||||
section=section_name,
|
||||
)
|
||||
)
|
||||
symbols_by_addr[addr] = symbol
|
||||
self.ram_symbols.append(symbol)
|
||||
break
|
||||
|
||||
def _demangle_symbols(self) -> None:
|
||||
@@ -454,13 +436,7 @@ class RamStringsAnalyzer:
|
||||
for symbol in largest_symbols:
|
||||
# Use demangled name if available, otherwise raw name
|
||||
display_name = symbol.demangled or symbol.name
|
||||
# Truncate the name, not the alias note, so merged aliases stay
|
||||
# visible even for long demangled C++ names.
|
||||
alias_note = f" (+{len(symbol.aliases)} aliases)" if symbol.aliases else ""
|
||||
max_name_len = 49 - len(alias_note)
|
||||
if len(display_name) > max_name_len:
|
||||
display_name = display_name[:max_name_len]
|
||||
name_display = display_name + alias_note
|
||||
name_display = display_name[:49] if len(display_name) > 49 else display_name
|
||||
lines.append(
|
||||
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
|
||||
)
|
||||
|
||||
@@ -1,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()) {
|
||||
|
||||
@@ -113,9 +113,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type == "wav":
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
|
||||
elif file_type in ("mp1", "mp2", "mp3", "mpeg", "mpga"):
|
||||
# With puremagic >=2.0 this can cause some MP3 (Layer III) files to be labeled as "mp1"/"mp2".
|
||||
# Treat those labels as MP3 so we still pick the MP3 decoder.
|
||||
elif file_type in ("mp3", "mpeg", "mpga"):
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
|
||||
elif file_type == "flac":
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
|
||||
|
||||
@@ -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,
|
||||
@@ -109,9 +107,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
|
||||
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_KEY_ID = "key_id"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_NVS_ENCRYPTION = "nvs_encryption"
|
||||
CONF_RELEASE = "release"
|
||||
CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification"
|
||||
CONF_SIGNING_KEY = "signing_key"
|
||||
@@ -169,20 +165,6 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
|
||||
VARIANT_ESP32,
|
||||
}
|
||||
|
||||
# NVS encryption (HMAC peripheral scheme) is only available on variants that
|
||||
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
|
||||
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
|
||||
# should be added here.
|
||||
NVS_ENCRYPTION_HMAC_VARIANTS = {
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
}
|
||||
|
||||
COMPILER_OPTIMIZATIONS = {
|
||||
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
|
||||
"NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE",
|
||||
@@ -1116,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
|
||||
@@ -1365,37 +1303,6 @@ def final_validate(config):
|
||||
"Binaries will NOT be signed automatically during build. "
|
||||
"You must sign them externally before flashing."
|
||||
)
|
||||
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
|
||||
variant = config[CONF_VARIANT]
|
||||
if variant in NVS_ENCRYPTION_HMAC_VARIANTS:
|
||||
_LOGGER.warning(
|
||||
"NVS encryption will burn an HMAC key into eFuse key block %d on the "
|
||||
"first boot of each device. This is PERMANENT and IRREVERSIBLE: "
|
||||
"the block cannot be erased or reused afterwards. Enabling (or "
|
||||
"later disabling) encryption also wipes any previously saved "
|
||||
"preferences once, because the older data can no longer be read.",
|
||||
nvs_enc[CONF_KEY_ID],
|
||||
)
|
||||
else:
|
||||
supported = ", ".join(
|
||||
sorted(VARIANT_FRIENDLY[v] for v in NVS_ENCRYPTION_HMAC_VARIANTS)
|
||||
)
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"NVS encryption (HMAC scheme) is not supported on "
|
||||
f"{VARIANT_FRIENDLY[variant]} (it has no HMAC peripheral). "
|
||||
f"Supported variants: {supported}.",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_ENCRYPTION],
|
||||
)
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -1633,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(
|
||||
{
|
||||
@@ -1648,15 +1552,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
|
||||
),
|
||||
cv.Optional(CONF_NVS_ENCRYPTION): cv.Schema(
|
||||
{
|
||||
# eFuse key block (0-5) that stores the HMAC key from
|
||||
# which the NVS encryption keys are derived. The block is
|
||||
# written on first boot if empty -- an irreversible
|
||||
# operation -- so it must be chosen explicitly.
|
||||
cv.Required(CONF_KEY_ID): cv.int_range(min=0, max=5),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
|
||||
): cv.boolean,
|
||||
@@ -2463,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)
|
||||
@@ -2499,20 +2384,6 @@ async def to_code(config):
|
||||
|
||||
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
|
||||
|
||||
# Encrypt NVS using the HMAC peripheral scheme. The NVS encryption keys are
|
||||
# derived at runtime from an HMAC key stored in the configured eFuse block
|
||||
# (no flash encryption required). The HMAC key is generated and burned into
|
||||
# the eFuse block on first boot if it is empty. With the scheme selected,
|
||||
# nvs_sec_provider registers it at startup and the default nvs_flash_init()
|
||||
# (used in esp32/preferences.cpp) transparently performs the secure init, so
|
||||
# no C++ changes are needed.
|
||||
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
|
||||
add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True)
|
||||
add_idf_sdkconfig_option("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC", True)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID", nvs_enc[CONF_KEY_ID]
|
||||
)
|
||||
|
||||
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
|
||||
|
||||
cg.add_define(
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#else
|
||||
#include "esphome/components/watchdog/watchdog.h"
|
||||
#include <cinttypes>
|
||||
extern "C" {
|
||||
#include <esp_hosted.h>
|
||||
#include <esp_hosted_misc.h>
|
||||
@@ -35,19 +33,6 @@ namespace esphome::esp32_ble {
|
||||
|
||||
static const char *const TAG = "esp32_ble";
|
||||
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
// Bringing up the remote BT controller issues synchronous RPCs to the
|
||||
// co-processor with 5 second response timeouts, and the default task watchdog
|
||||
// is also 5 seconds. If the co-processor firmware does not answer (for example
|
||||
// factory firmware without Bluetooth support), the watchdog would reboot the
|
||||
// device before the RPC could return an error, causing a boot loop. Raise the
|
||||
// watchdog for the duration of the bring-up so failures surface as error
|
||||
// returns instead. 60 seconds covers the worst case: transport reconnect
|
||||
// (up to ~20s), version preflight (1s), controller init/enable (5s each) and
|
||||
// the bluedroid host bring-up over the hosted HCI transport.
|
||||
static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
|
||||
#endif
|
||||
|
||||
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
|
||||
#define GAP_SCAN_COMPLETE_EVENTS \
|
||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
|
||||
@@ -179,9 +164,6 @@ void ESP32BLE::advertising_init_() {
|
||||
|
||||
bool ESP32BLE::ble_setup_() {
|
||||
esp_err_t err;
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
|
||||
#endif
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
|
||||
// start bt controller
|
||||
@@ -210,35 +192,15 @@ bool ESP32BLE::ble_setup_() {
|
||||
|
||||
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
|
||||
#else
|
||||
if (esp_hosted_connect_to_slave() != ESP_OK) { // NOLINT
|
||||
ESP_LOGE(TAG, "Co-processor transport failed; BLE disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast preflight (1 second RPC timeout): verifies the co-processor answers
|
||||
// RPCs at all before the 5 second timeout BT controller RPCs below, and
|
||||
// before hosted_hci_bluedroid_open(), which aborts if the transport is down.
|
||||
esp_hosted_coprocessor_fwver_t fw_ver{};
|
||||
if (esp_hosted_get_coprocessor_fwversion(&fw_ver) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Co-processor not responding; BLE disabled. Update its firmware with the esp32_hosted "
|
||||
"update component");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32, fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
esp_hosted_connect_to_slave(); // NOLINT
|
||||
|
||||
if (esp_hosted_bt_controller_init() != ESP_OK) {
|
||||
ESP_LOGE(TAG,
|
||||
"BT controller init failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
|
||||
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
|
||||
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_hosted_bt_controller_enable() != ESP_OK) {
|
||||
ESP_LOGE(TAG,
|
||||
"BT controller enable failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
|
||||
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
|
||||
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -370,10 +332,6 @@ bool ESP32BLE::ble_setup_() {
|
||||
}
|
||||
|
||||
bool ESP32BLE::ble_dismantle_() {
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
// Same 5 second RPCs as the bring-up path; see HOSTED_BT_WDT_TIMEOUT_MS
|
||||
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
|
||||
#endif
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
|
||||
@@ -419,12 +377,12 @@ bool ESP32BLE::ble_dismantle_() {
|
||||
}
|
||||
#else
|
||||
if (esp_hosted_bt_controller_disable() != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_hosted_bt_controller_disable failed");
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_hosted_bt_controller_deinit failed");
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ from esphome.const import (
|
||||
from esphome.cpp_generator import add_define
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
# esp32_ble raises the task watchdog around the remote BT controller bring-up
|
||||
AUTO_LOAD = ["watchdog"]
|
||||
|
||||
CONF_ACTIVE_HIGH = "active_high"
|
||||
CONF_BUS_WIDTH = "bus_width"
|
||||
|
||||
@@ -332,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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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 --------------------------
|
||||
|
||||
@@ -21,7 +21,6 @@ from esphome.const import (
|
||||
UNIT_EMPTY,
|
||||
UNIT_KELVIN,
|
||||
UNIT_KILOWATT,
|
||||
UNIT_LITRE_PER_HOUR,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@cfeenstra1024"]
|
||||
@@ -38,6 +37,7 @@ CONF_TEMP2 = "temp2"
|
||||
CONF_TEMP_DIFF = "temp_diff"
|
||||
|
||||
UNIT_GIGA_JOULE = "GJ"
|
||||
UNIT_LITRE_PER_HOUR = "l/h"
|
||||
|
||||
# Note: The sensor units are set automatically based un the received data from the meter
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -213,19 +213,17 @@ LightColorValues LightCall::validate_() {
|
||||
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
|
||||
bool explicit_turn_off_request = this->has_state() && !this->state_;
|
||||
|
||||
// Treat zero brightness as an implicit turn-off when no state was explicitly requested.
|
||||
if (this->has_brightness() && this->brightness_ == 0.0f && !this->has_state()) {
|
||||
// Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
|
||||
if (this->has_brightness() && this->brightness_ == 0.0f) {
|
||||
this->state_ = false;
|
||||
this->set_flag_(FLAG_HAS_STATE);
|
||||
}
|
||||
|
||||
// Make sure a turn-on makes the light visible: if the resulting brightness would be zero
|
||||
// (e.g. restored from a brightness=0 turn-off), reset it to full brightness.
|
||||
if (this->has_state() && this->state_ && (color_mode & ColorCapability::BRIGHTNESS)) {
|
||||
float brightness = this->has_brightness() ? this->brightness_ : this->parent_->remote_values.get_brightness();
|
||||
if (brightness == 0.0f) {
|
||||
if (color_mode & ColorCapability::BRIGHTNESS) {
|
||||
// Reset brightness so the light has nonzero brightness when turned back on.
|
||||
this->brightness_ = 1.0f;
|
||||
this->set_flag_(FLAG_HAS_BRIGHTNESS);
|
||||
} else {
|
||||
// Light doesn't support brightness; clear the flag to avoid a spurious
|
||||
// "brightness not supported" warning during capability validation.
|
||||
this->clear_flag_(FLAG_HAS_BRIGHTNESS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -414,10 +414,6 @@ async def to_code(configs):
|
||||
await cg.register_component(lv_component, config)
|
||||
if rotation := config.get(CONF_ROTATION):
|
||||
cg.add(lv_component.set_rotation(rotation))
|
||||
if paused := config[df.CONF_PAUSED]:
|
||||
cg.add(lv_component.set_paused(paused, False))
|
||||
if refr_time := config.get(df.CONF_REFRESH_INTERVAL):
|
||||
cg.add(lv_component.set_refresh_interval(refr_time.total_milliseconds))
|
||||
Widget.create(config[CONF_ID], lv_component, LvScrActType(), config)
|
||||
|
||||
lv_scr_act = get_screen_active(lv_component)
|
||||
@@ -602,7 +598,6 @@ LVGL_TOP_LEVEL_SCHEMA = (
|
||||
cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font,
|
||||
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_REFRESH_INTERVAL): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
@@ -647,7 +642,6 @@ LVGL_TOP_LEVEL_SCHEMA = (
|
||||
cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG,
|
||||
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
|
||||
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
|
||||
cv.Optional(df.CONF_PAUSED, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(DISP_BG_SCHEMA)
|
||||
|
||||
@@ -758,14 +758,12 @@ CONF_PAD_COLUMN = "pad_column"
|
||||
CONF_PAGE = "page"
|
||||
CONF_PAGE_WRAP = "page_wrap"
|
||||
CONF_PASSWORD_MODE = "password_mode"
|
||||
CONF_PAUSED = "paused"
|
||||
CONF_PIVOT_X = "pivot_x"
|
||||
CONF_PIVOT_Y = "pivot_y"
|
||||
CONF_PLACEHOLDER_TEXT = "placeholder_text"
|
||||
CONF_POINTS = "points"
|
||||
CONF_PREVIOUS = "previous"
|
||||
CONF_RADIUS = "radius"
|
||||
CONF_REFRESH_INTERVAL = "refresh_interval"
|
||||
CONF_REPEAT_COUNT = "repeat_count"
|
||||
CONF_RECOLOR = "recolor"
|
||||
CONF_RESUME_ON_INPUT = "resume_on_input"
|
||||
|
||||
@@ -401,10 +401,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
|
||||
}
|
||||
|
||||
void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
|
||||
// no guard here for display busy, since LVGL will not call flush_cb until the refresh timer fires,
|
||||
// and while the display is busy this is reset to 5 minutes. If that expires and the display is still
|
||||
// busy there are bigger problems.
|
||||
if (!this->paused_) {
|
||||
if (!this->is_paused()) {
|
||||
auto now = millis();
|
||||
this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(color_p));
|
||||
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1,
|
||||
@@ -623,20 +620,20 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
|
||||
void LvglComponent::draw_end_() {
|
||||
if (this->draw_end_callback_ != nullptr)
|
||||
this->draw_end_callback_->trigger();
|
||||
// Only reachable once the display is idle again: while busy, the display's refr_timer_ is
|
||||
// paused (see loop()), so LVGL never renders/flushes and this event never fires.
|
||||
if (this->update_when_display_idle_) {
|
||||
for (auto *disp : this->displays_)
|
||||
disp->update();
|
||||
}
|
||||
}
|
||||
|
||||
bool LvglComponent::displays_busy_() const {
|
||||
if (!this->update_when_display_idle_)
|
||||
return false;
|
||||
for (auto *disp : this->displays_) {
|
||||
if (!disp->is_idle())
|
||||
return true;
|
||||
bool LvglComponent::is_paused() const {
|
||||
if (this->paused_)
|
||||
return true;
|
||||
if (this->update_when_display_idle_) {
|
||||
for (auto *disp : this->displays_) {
|
||||
if (!disp->is_idle())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -780,8 +777,6 @@ void LvglComponent::setup() {
|
||||
if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) {
|
||||
lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this);
|
||||
}
|
||||
this->refr_timer_ = lv_display_get_refr_timer(this->disp_);
|
||||
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
|
||||
#if LV_USE_LOG
|
||||
lv_log_register_print_cb([](lv_log_level_t level, const char *buf) {
|
||||
auto next = strchr(buf, ')');
|
||||
@@ -807,32 +802,21 @@ void LvglComponent::update() {
|
||||
}
|
||||
|
||||
void LvglComponent::loop() {
|
||||
if (this->paused_) {
|
||||
if (this->show_snow_)
|
||||
if (this->is_paused()) {
|
||||
if (this->paused_ && this->show_snow_)
|
||||
this->write_random_();
|
||||
return;
|
||||
}
|
||||
// Pause/resume the display's own refresh timer to track its busy state. While paused, LVGL
|
||||
// still keeps track of invalidated areas but won't render or flush them, so nothing needs to
|
||||
// be discarded or replayed: once resumed, the accumulated areas are simply drawn as normal.
|
||||
// Input events and other timers keep being processed below regardless of this state.
|
||||
if (this->update_when_display_idle_) {
|
||||
bool busy = this->displays_busy_();
|
||||
if (busy && !this->refr_timer_paused_) {
|
||||
this->refr_timer_paused_ = true;
|
||||
// calling lv_timer_pause() here would be ineffective; LVGL pauses and resumes the timer based on its own internal
|
||||
// state, which is not aware of the display's busy state. Instead, we extend the timer period to avoid it firing
|
||||
// while the display is busy.
|
||||
lv_timer_set_period(this->refr_timer_, 5 * 60 * 1000);
|
||||
} else if (!busy && this->refr_timer_paused_) {
|
||||
this->refr_timer_paused_ = false;
|
||||
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
|
||||
// Don't wait for the timer's next natural period: refresh right away now that the
|
||||
// display is idle again.
|
||||
lv_timer_ready(this->refr_timer_);
|
||||
} else {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
auto now = millis();
|
||||
lv_timer_handler();
|
||||
auto elapsed = millis() - now;
|
||||
if (elapsed > 15) {
|
||||
ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now));
|
||||
}
|
||||
#else
|
||||
lv_timer_handler();
|
||||
#endif
|
||||
}
|
||||
lv_timer_handler();
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -214,14 +214,9 @@ class LvglComponent final : public PollingComponent {
|
||||
// @param paused If true, pause the display. If false, resume the display.
|
||||
// @param show_snow If true, show the snow effect when paused.
|
||||
void set_paused(bool paused, bool show_snow);
|
||||
void set_refresh_interval(uint32_t period) {
|
||||
this->refr_timer_period_ = period;
|
||||
if (this->refr_timer_ != nullptr)
|
||||
lv_timer_set_period(this->refr_timer_, period);
|
||||
}
|
||||
|
||||
// Returns true if the display has been explicitly paused via set_paused().
|
||||
bool is_paused() const { return this->paused_; }
|
||||
// Returns true if the display is explicitly paused, or a blocking display update is in progress.
|
||||
bool is_paused() const;
|
||||
// If the display is paused and we have resume_on_input_ set to true, resume the display.
|
||||
void maybe_wakeup() {
|
||||
if (this->paused_ && this->resume_on_input_) {
|
||||
@@ -304,9 +299,6 @@ class LvglComponent final : public PollingComponent {
|
||||
// Not checking for non-null callback since the
|
||||
// LVGL callback that calls it is not set in that case
|
||||
void draw_start_() const { this->draw_start_callback_->trigger(); }
|
||||
// Returns true if update_when_display_idle is enabled and at least one underlying display
|
||||
// component is currently busy (e.g. mid-refresh).
|
||||
bool displays_busy_() const;
|
||||
|
||||
void write_random_();
|
||||
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr);
|
||||
@@ -324,14 +316,6 @@ class LvglComponent final : public PollingComponent {
|
||||
|
||||
uint8_t *draw_buf_{};
|
||||
lv_display_t *disp_{};
|
||||
// The display's own periodic refresh timer, effectively paused while the display is busy (see
|
||||
// displays_busy_()) so LVGL neither renders nor flushes to it, without losing track of
|
||||
// invalidated areas. Other timers (indev reading, animations, ...) keep running as normal.
|
||||
lv_timer_t *refr_timer_{};
|
||||
// Tracks whether refr_timer_ is currently paused, so loop() can detect the busy -> idle edge
|
||||
// and kick off an immediate refresh instead of waiting for the timer's next natural period.
|
||||
bool refr_timer_paused_{};
|
||||
uint32_t refr_timer_period_{16};
|
||||
uint16_t width_{};
|
||||
uint16_t height_{};
|
||||
bool paused_{};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,7 +5,6 @@ from esphome.components.audio_dac import AudioDac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BITS_PER_SAMPLE,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INVERTED,
|
||||
@@ -17,11 +16,6 @@ from esphome.const import (
|
||||
CODEOWNERS = ["@remcom"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_ANALOG_GAIN = "analog_gain"
|
||||
CONF_CHANNEL_MIX = "channel_mix"
|
||||
CONF_VOLUME_MIN_DB = "volume_min_db"
|
||||
CONF_VOLUME_MAX_DB = "volume_max_db"
|
||||
|
||||
pcm5122_ns = cg.esphome_ns.namespace("pcm5122")
|
||||
PCM5122 = pcm5122_ns.class_("PCM5122", AudioDac, cg.Component, i2c.I2CDevice)
|
||||
CONF_PCM5122 = "pcm5122"
|
||||
@@ -33,60 +27,26 @@ PCM5122_BITS_PER_SAMPLE_ENUM = {
|
||||
32: pcm5122_bits_per_sample.PCM5122_BITS_PER_SAMPLE_32,
|
||||
}
|
||||
|
||||
pcm5122_analog_gain = pcm5122_ns.enum("PCM5122AnalogGain")
|
||||
PCM5122_ANALOG_GAIN_ENUM = {
|
||||
"0db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_0DB,
|
||||
"-6db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_MINUS_6DB,
|
||||
}
|
||||
|
||||
pcm5122_channel_mix = pcm5122_ns.enum("PCM5122ChannelMix")
|
||||
PCM5122_CHANNEL_MIX_ENUM = {
|
||||
"stereo": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_STEREO,
|
||||
"left": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_LEFT_ONLY,
|
||||
"right": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_RIGHT_ONLY,
|
||||
"swapped": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_SWAPPED,
|
||||
}
|
||||
|
||||
_validate_bits = cv.float_with_unit("bits", "bit")
|
||||
|
||||
|
||||
def _validate_volume_range(config):
|
||||
if config[CONF_VOLUME_MIN_DB] >= config[CONF_VOLUME_MAX_DB]:
|
||||
raise cv.Invalid(f"{CONF_VOLUME_MIN_DB} must be less than {CONF_VOLUME_MAX_DB}")
|
||||
return config
|
||||
|
||||
|
||||
PCM5122GPIOPin = pcm5122_ns.class_(
|
||||
"PCM5122GPIOPin",
|
||||
cg.GPIOPin,
|
||||
cg.Parented.template(PCM5122),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(PCM5122),
|
||||
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
|
||||
_validate_bits, cv.enum(PCM5122_BITS_PER_SAMPLE_ENUM)
|
||||
),
|
||||
cv.Optional(CONF_ANALOG_GAIN, default="0db"): cv.enum(
|
||||
PCM5122_ANALOG_GAIN_ENUM, lower=True
|
||||
),
|
||||
cv.Optional(CONF_CHANNEL_MIX, default="stereo"): cv.enum(
|
||||
PCM5122_CHANNEL_MIX_ENUM, lower=True
|
||||
),
|
||||
cv.Optional(CONF_VOLUME_MIN_DB, default="-52.5dB"): cv.All(
|
||||
cv.decibel, cv.float_range(min=-103.0, max=24.0)
|
||||
),
|
||||
cv.Optional(CONF_VOLUME_MAX_DB, default="0dB"): cv.All(
|
||||
cv.decibel, cv.float_range(min=-103.0, max=24.0)
|
||||
),
|
||||
cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x4D)),
|
||||
_validate_volume_range,
|
||||
.extend(i2c.i2c_device_schema(0x4D))
|
||||
)
|
||||
|
||||
|
||||
@@ -136,10 +96,3 @@ async def to_code(config):
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
|
||||
cg.add(var.set_analog_gain(config[CONF_ANALOG_GAIN]))
|
||||
cg.add(var.set_channel_mix(config[CONF_CHANNEL_MIX]))
|
||||
cg.add(var.set_volume_min_db(config[CONF_VOLUME_MIN_DB]))
|
||||
cg.add(var.set_volume_max_db(config[CONF_VOLUME_MAX_DB]))
|
||||
if enable_pin_config := config.get(CONF_ENABLE_PIN):
|
||||
enable_pin = await cg.gpio_pin_expression(enable_pin_config)
|
||||
cg.add(var.set_enable_pin(enable_pin))
|
||||
|
||||
@@ -10,12 +10,6 @@ namespace esphome::pcm5122 {
|
||||
static const char *const TAG = "pcm5122";
|
||||
|
||||
void PCM5122::setup() {
|
||||
// Hold XSMT low (soft mute asserted) until init completes
|
||||
if (this->enable_pin_ != nullptr) {
|
||||
this->enable_pin_->setup();
|
||||
this->enable_pin_->digital_write(false);
|
||||
}
|
||||
|
||||
// Select page 0 and verify chip presence via I2C ACK
|
||||
if (!this->select_page_(0)) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
@@ -57,22 +51,7 @@ void PCM5122::setup() {
|
||||
}
|
||||
this->reg(PCM5122_REG_AUDIO_FORMAT) = PCM5122_AUDIO_FORMAT_I2S | alen;
|
||||
|
||||
if (!this->write_channel_mix_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->write_analog_gain_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// PLL reference clock: BCK
|
||||
if (!this->select_page_(0)) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
optional<uint8_t> pll_ref = this->read_byte(PCM5122_REG_PLL_REF);
|
||||
if (!pll_ref.has_value()) {
|
||||
ESP_LOGE(TAG, "Failed to read PLL_REF");
|
||||
@@ -88,40 +67,15 @@ void PCM5122::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Release XSMT (soft un-mute) now that init has completed
|
||||
if (this->enable_pin_ != nullptr) {
|
||||
this->enable_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
|
||||
void PCM5122::dump_config() {
|
||||
const char *channel_mix_str;
|
||||
switch (this->channel_mix_) {
|
||||
case PCM5122_CHANNEL_MIX_LEFT_ONLY:
|
||||
channel_mix_str = "left only";
|
||||
break;
|
||||
case PCM5122_CHANNEL_MIX_RIGHT_ONLY:
|
||||
channel_mix_str = "right only";
|
||||
break;
|
||||
case PCM5122_CHANNEL_MIX_SWAPPED:
|
||||
channel_mix_str = "swapped";
|
||||
break;
|
||||
default:
|
||||
channel_mix_str = "stereo";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "Audio DAC:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Bits per sample: %u\n"
|
||||
" Analog gain: %s\n"
|
||||
" Channel mix: %s\n"
|
||||
" Volume range: %.1f dB to %.1f dB\n"
|
||||
" Muted: %s",
|
||||
this->bits_per_sample_, this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? "0 dB" : "-6 dB",
|
||||
channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_));
|
||||
LOG_PIN(" Enable Pin: ", this->enable_pin_);
|
||||
this->bits_per_sample_, YESNO(this->is_muted_));
|
||||
}
|
||||
|
||||
bool PCM5122::set_mute_off() {
|
||||
@@ -164,11 +118,11 @@ bool PCM5122::write_mute_() {
|
||||
}
|
||||
|
||||
bool PCM5122::write_volume_() {
|
||||
// DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFE = -103 dB, 0xFF = mute (-0.5 dB/step).
|
||||
// Note: volume=0.0 maps to volume_min_db_, which is not true silence unless set to -103 dB.
|
||||
// DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFF = mute (-0.5 dB/step).
|
||||
// Note: volume=0.0 maps to -52.5 dB (still audible), not true silence.
|
||||
// Use set_mute_on() for silence.
|
||||
const uint8_t dvol_max_volume = static_cast<uint8_t>(lroundf(0x30 - this->volume_max_db_ * 2.0f));
|
||||
const uint8_t dvol_min_volume = static_cast<uint8_t>(lroundf(0x30 - this->volume_min_db_ * 2.0f));
|
||||
const uint8_t dvol_max_volume = 0x30; // 0 dB at full scale
|
||||
const uint8_t dvol_min_volume = 0x99; // -52.5 dB at minimum
|
||||
|
||||
const uint8_t volume_byte =
|
||||
dvol_max_volume + static_cast<uint8_t>(lroundf((1.0f - this->volume_) * (dvol_min_volume - dvol_max_volume)));
|
||||
@@ -183,52 +137,4 @@ bool PCM5122::write_volume_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::write_analog_gain_() {
|
||||
uint8_t gain_byte = this->analog_gain_;
|
||||
if (!this->select_page_(1) || !this->write_byte(PCM5122_REG_ANALOG_GAIN, gain_byte)) {
|
||||
ESP_LOGE(TAG, "Writing analog gain failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::write_channel_mix_() {
|
||||
uint8_t channel_mix_byte = this->channel_mix_;
|
||||
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_DAC_DATA_PATH, channel_mix_byte)) {
|
||||
ESP_LOGE(TAG, "Writing channel mix failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::set_standby(bool enable) {
|
||||
bool prev_standby = this->standby_;
|
||||
this->standby_ = enable;
|
||||
if (!this->write_power_control_()) {
|
||||
this->standby_ = prev_standby;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::set_powerdown(bool enable) {
|
||||
bool prev_powerdown = this->powerdown_;
|
||||
this->powerdown_ = enable;
|
||||
if (!this->write_power_control_()) {
|
||||
this->powerdown_ = prev_powerdown;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::write_power_control_() {
|
||||
uint8_t power_byte =
|
||||
(this->standby_ ? PCM5122_POWER_CONTROL_RQST : 0) | (this->powerdown_ ? PCM5122_POWER_CONTROL_RQPD : 0);
|
||||
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_POWER_CONTROL, power_byte)) {
|
||||
ESP_LOGE(TAG, "Writing power control failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "esphome/components/audio_dac/audio_dac.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::pcm5122 {
|
||||
@@ -11,13 +10,11 @@ namespace esphome::pcm5122 {
|
||||
// Page 0 register addresses
|
||||
static const uint8_t PCM5122_REG_PAGE_SELECT = 0x00;
|
||||
static const uint8_t PCM5122_REG_RESET = 0x01;
|
||||
static const uint8_t PCM5122_REG_POWER_CONTROL = 0x02;
|
||||
static const uint8_t PCM5122_REG_MUTE = 0x03;
|
||||
static const uint8_t PCM5122_REG_GPIO_ENABLE = 0x08;
|
||||
static const uint8_t PCM5122_REG_PLL_REF = 0x0D;
|
||||
static const uint8_t PCM5122_REG_ERROR_DETECT = 0x25;
|
||||
static const uint8_t PCM5122_REG_AUDIO_FORMAT = 0x28;
|
||||
static const uint8_t PCM5122_REG_DAC_DATA_PATH = 0x2A;
|
||||
static const uint8_t PCM5122_REG_DVOL_LEFT = 0x3D;
|
||||
static const uint8_t PCM5122_REG_DVOL_RIGHT = 0x3E;
|
||||
static const uint8_t PCM5122_REG_GPIO_OUTPUT_SELECT = 0x50; // Base address; GPIO n uses offset n-1
|
||||
@@ -26,9 +23,6 @@ static const uint8_t PCM5122_REG_GPIO_OUTPUT = 0x56;
|
||||
static const uint8_t PCM5122_REG_GPIO_INVERT = 0x57;
|
||||
static const uint8_t PCM5122_REG_GPIO_INPUT = 0x77;
|
||||
|
||||
// Page 1 register addresses
|
||||
static const uint8_t PCM5122_REG_ANALOG_GAIN = 0x02;
|
||||
|
||||
// Register values for init sequence
|
||||
static const uint8_t PCM5122_RESET_MODULES = 0x10; // RSTM: reset audio modules
|
||||
static const uint8_t PCM5122_AUDIO_FORMAT_I2S = 0x00; // AFMT = I2S (bits [5:4] = 00)
|
||||
@@ -41,33 +35,12 @@ static const uint8_t PCM5122_ERROR_DETECT_DISABLE_DIV_AUTOSET = (1 << 1);
|
||||
static const uint8_t PCM5122_PLL_REF_MASK = (7 << 4); // SREF bits [6:4]
|
||||
static const uint8_t PCM5122_PLL_REF_SOURCE_BCK = (1 << 4); // SREF = 001 (BCK)
|
||||
|
||||
// Page 0, Register 2 (Power Control): RQST = standby request, RQPD = powerdown request (§10.5.3)
|
||||
static const uint8_t PCM5122_POWER_CONTROL_RQST = (1 << 4);
|
||||
static const uint8_t PCM5122_POWER_CONTROL_RQPD = (1 << 0);
|
||||
|
||||
// Page 1, Register 2 (Analog Gain Control): LAGN/RAGN select 0 dB or -6 dB analog gain (§8.3.5.5)
|
||||
static const uint8_t PCM5122_ANALOG_GAIN_LAGN = (1 << 4);
|
||||
static const uint8_t PCM5122_ANALOG_GAIN_RAGN = (1 << 0);
|
||||
|
||||
enum PCM5122BitsPerSample : uint8_t {
|
||||
PCM5122_BITS_PER_SAMPLE_16 = 16,
|
||||
PCM5122_BITS_PER_SAMPLE_24 = 24,
|
||||
PCM5122_BITS_PER_SAMPLE_32 = 32,
|
||||
};
|
||||
|
||||
enum PCM5122AnalogGain : uint8_t {
|
||||
PCM5122_ANALOG_GAIN_0DB = 0x00,
|
||||
PCM5122_ANALOG_GAIN_MINUS_6DB = PCM5122_ANALOG_GAIN_LAGN | PCM5122_ANALOG_GAIN_RAGN,
|
||||
};
|
||||
|
||||
// Page 0, Register 0x2A (DAC Data Path): AUPL/AUPR select which channel's data feeds each output (§7.4.2.42)
|
||||
enum PCM5122ChannelMix : uint8_t {
|
||||
PCM5122_CHANNEL_MIX_STEREO = 0x11, // Left data -> left out, right data -> right out
|
||||
PCM5122_CHANNEL_MIX_LEFT_ONLY = 0x12, // Left data -> both outputs
|
||||
PCM5122_CHANNEL_MIX_RIGHT_ONLY = 0x21, // Right data -> both outputs
|
||||
PCM5122_CHANNEL_MIX_SWAPPED = 0x22, // Left/right outputs swapped
|
||||
};
|
||||
|
||||
class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -75,11 +48,6 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::
|
||||
float get_setup_priority() const override { return setup_priority::IO; }
|
||||
|
||||
void set_bits_per_sample(PCM5122BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
|
||||
void set_analog_gain(PCM5122AnalogGain analog_gain) { this->analog_gain_ = analog_gain; }
|
||||
void set_channel_mix(PCM5122ChannelMix channel_mix) { this->channel_mix_ = channel_mix; }
|
||||
void set_volume_min_db(float volume_min_db) { this->volume_min_db_ = volume_min_db; }
|
||||
void set_volume_max_db(float volume_max_db) { this->volume_max_db_ = volume_max_db; }
|
||||
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
|
||||
|
||||
bool set_mute_off() override;
|
||||
bool set_mute_on() override;
|
||||
@@ -88,30 +56,17 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::
|
||||
bool is_muted() override;
|
||||
float volume() override;
|
||||
|
||||
bool set_standby(bool enable);
|
||||
bool set_powerdown(bool enable);
|
||||
|
||||
friend class PCM5122GPIOPin;
|
||||
|
||||
protected:
|
||||
bool select_page_(uint8_t page);
|
||||
bool write_mute_();
|
||||
bool write_volume_();
|
||||
bool write_analog_gain_();
|
||||
bool write_channel_mix_();
|
||||
bool write_power_control_();
|
||||
|
||||
GPIOPin *enable_pin_{nullptr};
|
||||
float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB)
|
||||
float volume_min_db_{-52.5f}; // Matches the previous hardcoded minimum (0x99)
|
||||
float volume_max_db_{0.0f}; // Matches the previous hardcoded maximum (0x30)
|
||||
int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes
|
||||
float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB)
|
||||
int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes
|
||||
bool is_muted_{false};
|
||||
bool standby_{false};
|
||||
bool powerdown_{false};
|
||||
PCM5122BitsPerSample bits_per_sample_{PCM5122_BITS_PER_SAMPLE_16};
|
||||
PCM5122AnalogGain analog_gain_{PCM5122_ANALOG_GAIN_0DB};
|
||||
PCM5122ChannelMix channel_mix_{PCM5122_CHANNEL_MIX_STEREO};
|
||||
};
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_POWER_MODE, ENTITY_CATEGORY_CONFIG
|
||||
|
||||
from ..audio_dac import CONF_PCM5122, PCM5122, pcm5122_ns
|
||||
|
||||
PCM5122PowerSwitch = pcm5122_ns.class_("PCM5122PowerSwitch", switch.Switch)
|
||||
|
||||
pcm5122_power_switch_mode = pcm5122_ns.enum("PCM5122PowerSwitchMode")
|
||||
PCM5122_POWER_SWITCH_MODE_ENUM = {
|
||||
"standby": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_STANDBY,
|
||||
"powerdown": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_POWERDOWN,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = switch.switch_schema(
|
||||
PCM5122PowerSwitch,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_PCM5122): cv.use_id(PCM5122),
|
||||
cv.Optional(CONF_POWER_MODE, default="powerdown"): cv.enum(
|
||||
PCM5122_POWER_SWITCH_MODE_ENUM, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_parented(var, config[CONF_PCM5122])
|
||||
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "power_switch.h"
|
||||
|
||||
namespace esphome::pcm5122 {
|
||||
|
||||
void PCM5122PowerSwitch::write_state(bool state) {
|
||||
bool ok = (this->mode_ == PCM5122_POWER_SWITCH_MODE_STANDBY) ? this->parent_->set_standby(state)
|
||||
: this->parent_->set_powerdown(state);
|
||||
if (ok)
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
#include "../pcm5122.h"
|
||||
|
||||
namespace esphome::pcm5122 {
|
||||
|
||||
enum PCM5122PowerSwitchMode : uint8_t {
|
||||
PCM5122_POWER_SWITCH_MODE_STANDBY,
|
||||
PCM5122_POWER_SWITCH_MODE_POWERDOWN,
|
||||
};
|
||||
|
||||
class PCM5122PowerSwitch final : public switch_::Switch, public Parented<PCM5122> {
|
||||
public:
|
||||
void set_power_mode(PCM5122PowerSwitchMode mode) { this->mode_ = mode; }
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
|
||||
PCM5122PowerSwitchMode mode_{PCM5122_POWER_SWITCH_MODE_POWERDOWN};
|
||||
};
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "usb_uart.h"
|
||||
#include "usb/usb_host.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/uart/uart_debugger.h"
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
@@ -397,14 +396,7 @@ int USBUartTypeFT23XX::set_dtr_rts_(USBUartChannel *channel) {
|
||||
}
|
||||
|
||||
void USBUartTypeFT23XX::start_input(USBUartChannel *channel) {
|
||||
if (!channel->initialised_.load())
|
||||
return;
|
||||
|
||||
// Use compare_exchange_strong to avoid a check-then-act race: start_input() is called
|
||||
// from both the USB task (self-restart on success) and the main loop (backpressure
|
||||
// restart), so a plain load()/store() pair can let both threads submit a transfer.
|
||||
auto started = false;
|
||||
if (!channel->input_started_.compare_exchange_strong(started, true))
|
||||
if (!channel->initialised_.load() || channel->input_started_.load())
|
||||
return;
|
||||
|
||||
const auto *ep = channel->cdc_dev_.in_ep;
|
||||
@@ -416,55 +408,39 @@ void USBUartTypeFT23XX::start_input(USBUartChannel *channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FTDI prepends a 2-byte modem/line status header to every bulk IN packet.
|
||||
size_t uart_data_len = (status.data_len > 2) ? (status.data_len - 2) : 0;
|
||||
|
||||
if (uart_data_len > 0) {
|
||||
ESP_LOGV(TAG, "RX callback: Received %zu bytes, channel=%d", uart_data_len, channel->index_);
|
||||
if (!channel->dummy_receiver_) {
|
||||
UsbDataChunk *chunk = this->chunk_pool_.allocate();
|
||||
if (chunk == nullptr) {
|
||||
this->usb_data_queue_.increment_dropped_count();
|
||||
channel->input_started_.store(false);
|
||||
// Queue is full — wake the main loop to drain it, then let read_array()
|
||||
// retrigger start_input() rather than spinning here in the USB task.
|
||||
this->enable_loop_soon_any_context();
|
||||
App.wake_loop_threadsafe();
|
||||
return;
|
||||
}
|
||||
// Strip the 2-byte FTDI header before queuing.
|
||||
memcpy(chunk->data, status.data + 2, uart_data_len);
|
||||
chunk->length = static_cast<uint16_t>(uart_data_len);
|
||||
chunk->channel = channel;
|
||||
this->usb_data_queue_.push(chunk);
|
||||
// Copy the entire received UART payload into the ring buffer in one
|
||||
// operation to avoid per-byte overhead and reduce the chance of
|
||||
// heap activity in hot paths.
|
||||
channel->input_buffer_.push(status.data + 2, uart_data_len);
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
if (channel->debug_) {
|
||||
// Debug path creates a temporary vector for logging only; this is
|
||||
// acceptable because debug mode is opt-in and not used in release.
|
||||
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX,
|
||||
std::vector<uint8_t>(status.data + 2, status.data + 2 + uart_data_len), ',',
|
||||
channel->debug_prefix_);
|
||||
}
|
||||
#endif
|
||||
this->enable_loop_soon_any_context();
|
||||
App.wake_loop_threadsafe();
|
||||
}
|
||||
} else if (status.data_len >= 2) {
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "RX: Status packet, modem=0x%02X line=0x%02X, ch=%d", status.data[0], status.data[1],
|
||||
channel->index_);
|
||||
}
|
||||
|
||||
channel->input_started_.store(false);
|
||||
this->start_input(channel);
|
||||
if (channel->dummy_receiver_ ||
|
||||
channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
|
||||
this->start_input(channel);
|
||||
}
|
||||
};
|
||||
|
||||
if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) {
|
||||
ESP_LOGE(TAG, "RX transfer submission failed for ep=0x%02X", ep->bEndpointAddress);
|
||||
channel->input_started_.store(false);
|
||||
}
|
||||
}
|
||||
|
||||
void USBUartTypeFT23XX::on_rx_overflow(USBUartChannel *channel) {
|
||||
ESP_LOGW(TAG, "RX buffer overflow on channel %d, clearing to resync", channel->index_);
|
||||
channel->input_buffer_.clear();
|
||||
channel->input_started_.store(true);
|
||||
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
|
||||
}
|
||||
|
||||
void USBUartTypeFT23XX::enable_channels() {
|
||||
|
||||
@@ -228,11 +228,6 @@ void USBUartComponent::loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// If there is not enough space for the full chunk, let the device subclass
|
||||
// handle it (e.g. FTDI clears the buffer to prevent mid-telegram corruption).
|
||||
if (channel->input_buffer_.get_free_space() < chunk->length) {
|
||||
this->on_rx_overflow(channel);
|
||||
}
|
||||
// Push data to ring buffer (now safe in main loop)
|
||||
channel->input_buffer_.push(chunk->data, chunk->length);
|
||||
|
||||
|
||||
@@ -192,13 +192,9 @@ class USBUartComponent : public usb_host::USBClient {
|
||||
|
||||
void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); }
|
||||
|
||||
virtual void start_input(USBUartChannel *channel);
|
||||
void start_input(USBUartChannel *channel);
|
||||
void start_output(USBUartChannel *channel);
|
||||
|
||||
// Called from loop() when input_buffer_ has insufficient space for the incoming chunk.
|
||||
// Default is a no-op; override in device-specific subclasses that need resync on overflow.
|
||||
virtual void on_rx_overflow(USBUartChannel *channel) {}
|
||||
|
||||
// Lock-free data transfer from USB task to main loop
|
||||
static constexpr int USB_DATA_QUEUE_SIZE = 32;
|
||||
LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_;
|
||||
@@ -252,8 +248,7 @@ class USBUartTypeFT23XX : public USBUartTypeCdcAcm {
|
||||
public:
|
||||
USBUartTypeFT23XX(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
|
||||
|
||||
void start_input(USBUartChannel *channel) override;
|
||||
void on_rx_overflow(USBUartChannel *channel) override;
|
||||
void start_input(USBUartChannel *channel);
|
||||
|
||||
protected:
|
||||
std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace esphome::zephyr_ble_server {
|
||||
|
||||
class BLEServer final : public Component {
|
||||
class BLEServer : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -21,7 +21,7 @@ class BLEServer final : public Component {
|
||||
CallbackManager<void(uint32_t)> passkey_cb_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLENumericComparisonReplyAction final : public Action<Ts...> {
|
||||
template<typename... Ts> class BLENumericComparisonReplyAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit BLENumericComparisonReplyAction(BLEServer *parent) : parent_(parent) {}
|
||||
|
||||
|
||||
@@ -599,6 +599,18 @@ class LoadValidationStep(ConfigValidationStep):
|
||||
CORE.loaded_integrations.add(self.domain)
|
||||
# For platform components, normalize conf before creating MetadataValidationStep
|
||||
if component.is_platform_component:
|
||||
# Legacy config migration: allow a platform component to rewrite a
|
||||
# pre-platform-format top-level config (e.g. a bare list or legacy
|
||||
# dict form) into the normalized list of `platform:` tagged entries.
|
||||
# Removable deprecation shim hook; no-op for components that do not
|
||||
# define LEGACY_CONFIG_MIGRATE.
|
||||
if (
|
||||
(migrate := component.legacy_config_migrate) is not None
|
||||
and self.conf
|
||||
and not isinstance(self.conf, core.AutoLoad)
|
||||
and (migrated := migrate(self.conf)) is not None
|
||||
):
|
||||
result[self.domain] = self.conf = migrated
|
||||
if not self.conf:
|
||||
result[self.domain] = self.conf = []
|
||||
elif not isinstance(self.conf, list):
|
||||
|
||||
@@ -1255,7 +1255,6 @@ UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh"
|
||||
UNIT_KILOWATT = "kW"
|
||||
UNIT_KILOWATT_HOURS = "kWh"
|
||||
UNIT_LITRE = "L"
|
||||
UNIT_LITRE_PER_HOUR = "L/h"
|
||||
UNIT_LITRE_PER_SECOND = "L/s"
|
||||
UNIT_LUX = "lx"
|
||||
UNIT_MEGAJOULE = "MJ"
|
||||
|
||||
@@ -239,12 +239,9 @@
|
||||
#define ESPHOME_TASK_LOG_BUFFER_SIZE 768
|
||||
#define USE_OTA_ROLLBACK
|
||||
#define USE_OTA_SIGNED_VERIFICATION
|
||||
#define USE_OTA_DOWNGRADE_PROTECTION
|
||||
#define USE_ESP32_MIN_CHIP_REVISION_SET
|
||||
#define USE_ESP32_RTC_PREFERENCES
|
||||
#define USE_ESP32_SRAM1_AS_IRAM
|
||||
#define USE_ESPNOW
|
||||
#define USE_ESPNOW_MAX_PAYLOAD_SIZE 1470
|
||||
|
||||
#define USE_BLUETOOTH_PROXY
|
||||
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
|
||||
|
||||
@@ -184,10 +184,8 @@ template<size_t InlineSize = 8> class SmallInlineBuffer {
|
||||
SmallInlineBuffer(const SmallInlineBuffer &) = delete;
|
||||
SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete;
|
||||
|
||||
/// Resize to `size` bytes of (uninitialized) storage and return a writable pointer to fill.
|
||||
/// Allocates heap only when `size` exceeds the inline capacity. Use this when the contents are
|
||||
/// built in place (e.g. assembling a frame and appending a checksum) to avoid a staging copy.
|
||||
uint8_t *init(size_t size) {
|
||||
/// Set buffer contents, allocating heap if needed
|
||||
void set(const uint8_t *src, size_t size) {
|
||||
// Free existing heap allocation if switching from heap to inline or different heap size
|
||||
if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) {
|
||||
delete[] this->heap_;
|
||||
@@ -198,12 +196,9 @@ template<size_t InlineSize = 8> class SmallInlineBuffer {
|
||||
this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
this->len_ = size;
|
||||
return this->data();
|
||||
memcpy(this->data(), src, size);
|
||||
}
|
||||
|
||||
/// Set buffer contents, allocating heap if needed
|
||||
void set(const uint8_t *src, size_t size) { memcpy(this->init(size), src, size); }
|
||||
|
||||
uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; }
|
||||
const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; }
|
||||
size_t size() const { return this->len_; }
|
||||
|
||||
@@ -52,7 +52,6 @@ RESPONSE_ERROR_PARTITION_TABLE_VERIFY = 0x8F
|
||||
RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90
|
||||
RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91
|
||||
RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92
|
||||
RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93
|
||||
RESPONSE_ERROR_UNKNOWN = 0xFF
|
||||
|
||||
OTA_VERSION_1_0 = 1
|
||||
@@ -158,11 +157,6 @@ _ERROR_MESSAGES: dict[int, str] = {
|
||||
"the bootloader update without rebooting the device. If the device "
|
||||
"fails to boot, recover it via a serial flash."
|
||||
),
|
||||
RESPONSE_ERROR_VERSION_DOWNGRADE: (
|
||||
"The device rejected the update because it has OTA downgrade protection "
|
||||
"enabled: the new firmware's version must be newer than the version the "
|
||||
"device is currently running."
|
||||
),
|
||||
RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP",
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user