Compare commits

..

23 Commits

Author SHA1 Message Date
J. Nick Koston cb2e8320f0 [api] Pass pos by value through encode_varint_raw_loop 2026-04-09 22:13:14 -10:00
J. Nick Koston 49ef5f9ad6 [api] Force inline encode_varint_raw_loop so pos stays in a register 2026-04-09 22:07:15 -10:00
J. Nick Koston 707f3f4749 [api] Unroll varint encode loop for compile-time bounded types 2026-04-09 21:59:35 -10:00
J. Nick Koston ec420d5792 [api] Add (inline_encode) proto option for sub-message inlining (#15599) 2026-04-10 15:33:56 +12:00
J. Nick Koston 17209df7b5 [mcp23016] Add interrupt pin support (#15616) 2026-04-10 15:29:52 +12:00
J. Nick Koston 9cf9b02ba2 [pca6416a] Add interrupt pin support (#15614) 2026-04-10 15:29:26 +12:00
J. Nick Koston c90fa2378a [tca9555] Add interrupt pin support (#15613) 2026-04-10 15:29:00 +12:00
Jesse Hills c04dfa922e [hbridge] Move light pin switching to loop (#15615) 2026-04-10 14:02:49 +12:00
Jesse Hills 668007707d [CI] Add org fork detection warning to auto-label PR workflow (#15588) 2026-04-10 12:13:22 +12:00
dependabot[bot] ab71f5276f Bump ruff from 0.15.9 to 0.15.10 (#15609)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-09 19:36:25 +00:00
Jonathan Swoboda d062f62656 [sx127x][cc1101] Disable loop when packet mode is inactive (#15606) 2026-04-09 15:00:52 -04:00
J. Nick Koston 03db32d045 [core] Add CodSpeed benchmarks for hot helper functions (#15593) 2026-04-09 07:48:32 -10:00
J. Nick Koston 8f6d489a9a [ci] Use --base-only for memory impact builds (#15598) 2026-04-09 11:48:33 -04:00
J. Nick Koston dd07fba943 [socket] Document ready() contract: callers must drain or track (#15590) 2026-04-09 11:48:18 -04:00
J. Nick Koston 6f5d642a31 [gdk101] Increase reset retries for slow-booting sensor MCU (#15584) 2026-04-09 11:48:10 -04:00
dependabot[bot] 2721f08bcc Bump aioesphomeapi from 44.12.0 to 44.13.1 (#15600)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 10:03:58 +00:00
J. Nick Koston eafc5df3f2 [safe_mode] Combine related OTA rollback log messages (#15592) 2026-04-09 05:30:39 +00:00
J. Nick Koston 46d0c29be5 [safe_mode] Use loop component start time instead of millis() (#15591) 2026-04-09 05:20:32 +00:00
J. Nick Koston abdbbf4dd2 [api] Fix ListEntitiesRequest not read due to LWIP rcvevent tracking (#15589) 2026-04-09 02:14:01 +00:00
Jesse Hills 4dc0599a7d Merge branch 'beta' into dev 2026-04-09 13:41:27 +12:00
Jesse Hills 52c35ec09c Bump version to 2026.5.0-dev 2026-04-09 11:28:48 +12:00
J. Nick Koston 76490e45bc [ci] Fix status-check-labels workflow flooding CI queue (#15585) 2026-04-08 13:08:29 -10:00
Angel Nunez Mencias 0a8130858c [ade7953_spi] Fix SPI mode on esp-idf (#14824)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-08 22:57:53 +00:00
139 changed files with 979 additions and 3102 deletions
+1 -1
View File
@@ -1 +1 @@
10c432ae818f9ed7fd4a0176a04467b1f2634363f5ec985045a6d72747f60b90
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6
@@ -4,6 +4,7 @@ module.exports = {
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
TOO_BIG_MARKER: '<!-- too-big-request -->',
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
MANAGED_LABELS: [
'new-component',
@@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
return { labels, deprecatedInfo };
}
// Strategy: Detect when maintainers cannot modify the PR branch
function detectMaintainerAccess(context) {
const pr = context.payload.pull_request;
// Only relevant for cross-repo PRs (forks)
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
return null;
}
if (pr.maintainer_can_modify) {
return null;
}
const isOrgFork = pr.head.repo.owner.type === 'Organization';
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
return { isOrgFork, orgName: pr.head.repo.owner.login };
}
// Strategy: Requirements detection
async function detectRequirements(allLabels, prFiles, context) {
const labels = new Set();
@@ -329,5 +347,6 @@ module.exports = {
detectTests,
detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectMaintainerAccess,
detectRequirements
};
+11 -5
View File
@@ -12,9 +12,10 @@ const {
detectTests,
detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectMaintainerAccess,
detectRequirements
} = require('./detectors');
const { handleReviews } = require('./reviews');
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
const { applyLabels, removeOldLabels } = require('./labels');
// Fetch API data
@@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => {
codeOwnerLabels,
testLabels,
checkboxLabels,
deprecatedResult
deprecatedResult,
maintainerAccess
] = await Promise.all([
detectMergeBranch(context),
detectComponentPlatforms(changedFiles, apiData),
@@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => {
detectCodeOwner(github, context, changedFiles),
detectTests(changedFiles),
detectPRTemplateCheckboxes(context),
detectDeprecatedComponents(github, context, changedFiles)
detectDeprecatedComponents(github, context, changedFiles),
detectMaintainerAccess(context)
]);
// Extract deprecated component info
@@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => {
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
// Handle reviews and org fork comment
await Promise.all([
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
handleMaintainerAccessComment(github, context, maintainerAccess)
]);
// Apply labels
await applyLabels(github, context, finalLabels);
+60 -2
View File
@@ -2,7 +2,8 @@ const {
BOT_COMMENT_MARKER,
CODEOWNERS_MARKER,
TOO_BIG_MARKER,
DEPRECATED_COMPONENT_MARKER
DEPRECATED_COMPONENT_MARKER,
ORG_FORK_MARKER
} = require('./constants');
// Generate review messages
@@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
}
}
// Handle maintainer access warning comment
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
if (!maintainerAccess) {
return;
}
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
const prAuthor = context.payload.pull_request.user.login;
// Check if we already posted the warning (iterate pages to exit early)
let existingComment;
for await (const { data: comments } of github.paginate.iterator(
github.rest.issues.listComments,
{ owner, repo, issue_number: pr_number }
)) {
existingComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body && comment.body.includes(ORG_FORK_MARKER)
);
if (existingComment) {
break;
}
}
if (existingComment) {
console.log('Maintainer access warning comment already exists, skipping');
return;
}
let body;
if (maintainerAccess.isOrgFork) {
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
`Hey there @${prAuthor},\n` +
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
} else {
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
`Hey there @${prAuthor},\n` +
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body
});
console.log('Created maintainer access warning comment');
}
module.exports = {
handleReviews
handleReviews,
handleMaintainerAccessComment
};
+4 -2
View File
@@ -868,7 +868,8 @@ jobs:
python script/test_build_components.py \
-e compile \
-c "$component_list" \
-t "$platform" 2>&1 | \
-t "$platform" \
--base-only 2>&1 | \
tee /dev/stderr | \
python script/ci_memory_impact_extract.py \
--output-env \
@@ -954,7 +955,8 @@ jobs:
python script/test_build_components.py \
-e compile \
-c "$component_list" \
-t "$platform" 2>&1 | \
-t "$platform" \
--base-only 2>&1 | \
tee /dev/stderr | \
python script/ci_memory_impact_extract.py \
--output-env \
+12 -13
View File
@@ -2,30 +2,29 @@ name: Status check labels
on:
pull_request:
types: [labeled, unlabeled]
types: [opened, reopened, labeled, unlabeled, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
check:
name: Check ${{ matrix.label }}
name: Check blocking labels
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
label:
- needs-docs
- merge-after-release
- chained-pr
steps:
- name: Check for ${{ matrix.label }} label
- name: Check for blocking labels
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
if (hasLabel) {
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
const labelNames = labels.map(l => l.name);
const found = blockingLabels.filter(bl => labelNames.includes(bl));
if (found.length > 0) {
core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`);
}
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.9
rev: v0.15.10
hooks:
# Run the linter.
- id: ruff
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.4.1
PROJECT_NUMBER = 2026.5.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -8
View File
@@ -750,15 +750,8 @@ def upload_using_esptool(
platformio_api.FlashImage(
path=idedata.firmware_bin_path, offset=firmware_offset
),
*idedata.extra_flash_images,
]
for image in idedata.extra_flash_images:
if not image.path.is_file():
_LOGGER.warning(
"Skipping missing flash image declared by platform: %s",
image.path,
)
continue
flash_images.append(image)
mcu = "esp8266"
if CORE.is_esp32:
+6 -72
View File
@@ -151,8 +151,8 @@ class ConfigBundleCreator:
def __init__(self, config: dict[str, Any]) -> None:
self._config = config
self._config_dir = Path(CORE.config_dir).resolve()
self._config_path = Path(CORE.config_path).resolve()
self._config_dir = CORE.config_dir
self._config_path = CORE.config_path
self._files: list[BundleFile] = []
self._seen_paths: set[Path] = set()
self._secrets_paths: set[Path] = set()
@@ -258,36 +258,21 @@ class ConfigBundleCreator:
def _discover_yaml_includes(self) -> None:
"""Discover YAML files loaded during config parsing.
Deliberately uses a fresh re-parse and force-loads every deferred
``IncludeFile`` to include *all* potentially-reachable includes,
even branches not selected by the local substitutions. Bundles are
meant to be compiled on another system where command-line
substitution overrides may choose a different branch — e.g.
``!include network/${eth_model}/config.yaml`` must ship every
candidate so the remote build can pick any one.
Entries with unresolved substitution variables in the filename
path are skipped with a warning (they cannot be resolved without
the substitution pass).
We track files by wrapping _load_yaml_internal. The config has already
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
re-load just to discover the file list.
Secrets files are tracked separately so we can filter them to
only include the keys this config actually references.
"""
# Must be a fresh parse: IncludeFile.load() caches its result in
# _content, and we discover files by listening for loader calls. On
# an already-parsed tree the cache is populated, .load() returns
# without calling the loader, the listener never fires, and the
# referenced files would be silently dropped from the bundle.
with yaml_util.track_yaml_loads() as loaded_files:
try:
data = yaml_util.load_yaml(self._config_path)
yaml_util.load_yaml(self._config_path)
except EsphomeError:
_LOGGER.debug(
"Bundle: re-loading YAML for include discovery failed, "
"proceeding with partial file list"
)
else:
_force_load_include_files(data)
for fpath in loaded_files:
if fpath == self._config_path.resolve():
@@ -623,57 +608,6 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
tar.addfile(info, io.BytesIO(data))
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
resolved during the substitution pass. During bundle discovery we need
the referenced files to actually load so the ``track_yaml_loads``
listener fires for them.
``IncludeFile`` instances with unresolved substitution variables in the
filename cannot be loaded — we skip and warn about those.
"""
if _seen is None:
_seen = set()
if isinstance(obj, yaml_util.IncludeFile):
if id(obj) in _seen:
return
_seen.add(id(obj))
if obj.has_unresolved_expressions():
_LOGGER.warning(
"Bundle: cannot resolve !include %s (referenced from %s) "
"with substitutions in path",
obj.file,
obj.parent_file,
)
return
try:
loaded = obj.load()
except EsphomeError as err:
_LOGGER.warning(
"Bundle: failed to load !include %s (referenced from %s): %s",
obj.file,
obj.parent_file,
err,
)
return
_force_load_include_files(loaded, _seen)
elif isinstance(obj, dict):
if id(obj) in _seen:
return
_seen.add(id(obj))
for value in obj.values():
_force_load_include_files(value, _seen)
elif isinstance(obj, (list, tuple)):
if id(obj) in _seen:
return
_seen.add(id(obj))
for item in obj:
_force_load_include_files(item, _seen)
def _resolve_include_path(include_path: Any) -> Path | None:
"""Resolve an include path to absolute, skipping system includes."""
if isinstance(include_path, str) and include_path.startswith("<"):
+1 -14
View File
@@ -2,11 +2,7 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import (
get_esp32_variant,
include_builtin_idf_component,
require_adc_oneshot_iram,
)
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
from esphome.components.zephyr import (
zephyr_add_overlay,
@@ -28,7 +24,6 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE
from esphome.types import ConfigType
from . import (
ATTENUATION_MODES,
@@ -70,13 +65,6 @@ def validate_config(config):
return config
def _require_adc_iram(config: ConfigType) -> ConfigType:
"""Register ADC oneshot IRAM requirement during config validation."""
if CORE.is_esp32:
require_adc_oneshot_iram()
return config
ADCSensor = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
)
@@ -107,7 +95,6 @@ CONFIG_SCHEMA = cv.All(
)
.extend(cv.polling_component_schema("60s")),
validate_config,
_require_adc_iram,
)
CONF_ADC_CHANNEL_ID = "adc_channel_id"
@@ -8,6 +8,9 @@ namespace ade7953_base {
static const char *const TAG = "ade7953";
constexpr uint16_t CONFIG_DEFAULT = 0x8004u;
constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u;
static const float ADE_POWER_FACTOR = 154.0f;
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
@@ -18,7 +21,12 @@ void ADE7953::setup() {
// The chip might take up to 100ms to initialise
this->set_timeout(100, [this]() {
// this->ade_write_8(0x0010, 0x04);
// Lock communication interface (SPI or I2C)
uint16_t config_v = CONFIG_DEFAULT;
this->ade_read_16(CONFIG_16, &config_v);
config_v &= static_cast<uint16_t>(~CONFIG_LOCK_BIT); // Clear the lock bit
this->ade_write_16(CONFIG_16, config_v);
// Configure optimum settings according to datasheet
this->ade_write_8(0x00FE, 0xAD);
this->ade_write_16(0x0120, 0x0030);
// Set gains
+17 -13
View File
@@ -9,31 +9,35 @@
namespace esphome {
namespace ade7953_base {
static const uint8_t PGA_V_8 =
static constexpr uint8_t PGA_V_8 =
0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0])
static const uint8_t PGA_IA_8 =
static constexpr uint8_t PGA_IA_8 =
0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0])
static const uint8_t PGA_IB_8 =
static constexpr uint8_t PGA_IB_8 =
0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0])
static const uint32_t AIGAIN_32 =
static constexpr uint16_t CONFIG_16 = 0x102; // CONFIG, (R/W) Default: 0x8004, Unsigned, Configuration register
static constexpr uint16_t AIGAIN_32 =
0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit)
static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
static const uint32_t AWGAIN_32 =
static constexpr uint16_t AVGAIN_32 =
0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
static constexpr uint16_t AWGAIN_32 =
0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit)
static const uint32_t AVARGAIN_32 =
static constexpr uint16_t AVARGAIN_32 =
0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit)
static const uint32_t AVAGAIN_32 =
static constexpr uint16_t AVAGAIN_32 =
0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit)
static const uint32_t BIGAIN_32 =
static constexpr uint16_t BIGAIN_32 =
0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit)
static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
static const uint32_t BWGAIN_32 =
static constexpr uint16_t BVGAIN_32 =
0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
static constexpr uint16_t BWGAIN_32 =
0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit)
static const uint32_t BVARGAIN_32 =
static constexpr uint16_t BVARGAIN_32 =
0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit)
static const uint32_t BVAGAIN_32 =
static constexpr uint16_t BVAGAIN_32 =
0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit)
class ADE7953 : public PollingComponent, public sensor::Sensor {
@@ -7,6 +7,9 @@ namespace ade7953_spi {
static const char *const TAG = "ade7953";
// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS
constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2;
void AdE7953Spi::setup() {
this->spi_setup();
ade7953_base::ADE7953::setup();
@@ -32,6 +35,9 @@ bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) {
this->write_byte16(reg);
this->transfer_byte(0);
this->write_byte16(value);
if (reg == ade7953_base::CONFIG_16) {
delayMicroseconds(CONFIG_LOCK_SETTLE_US);
}
this->disable();
return false;
}
+1 -1
View File
@@ -12,7 +12,7 @@ namespace esphome {
namespace ade7953_spi {
class AdE7953Spi : public ade7953_base::ADE7953,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_LEADING,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_1MHZ> {
public:
void setup() override;
+2 -5
View File
@@ -671,7 +671,6 @@ message SensorStateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR";
option (no_delay) = true;
option (speed_optimized) = true;
fixed32 key = 1 [(force) = true];
float state = 2;
@@ -778,10 +777,9 @@ message SubscribeLogsResponse {
option (source) = SOURCE_SERVER;
option (log) = false;
option (no_delay) = false;
option (speed_optimized) = true;
LogLevel level = 1 [(force) = true];
bytes message = 3 [(force) = true];
LogLevel level = 1;
bytes message = 3;
}
// ==================== NOISE ENCRYPTION ====================
@@ -1640,7 +1638,6 @@ message BluetoothLERawAdvertisementsResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BLUETOOTH_PROXY";
option (no_delay) = true;
option (speed_optimized) = true;
repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
}
+4 -1
View File
@@ -195,7 +195,10 @@ class APIFrameHelper {
}
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() const { return frame_footer_size_; }
// Check if socket has data ready to read
// Check if socket has buffered data ready to read.
// Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
// or track that they stopped early and retry without this check.
// See Socket::ready() for details.
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
// Release excess memory from internal buffers after initial sync
void release_buffers() {
-1
View File
@@ -23,7 +23,6 @@ extend google.protobuf.MessageOptions {
optional bool no_delay = 1040 [default=false];
optional string base_class = 1041;
optional bool inline_encode = 1042 [default=false];
optional bool speed_optimized = 1043 [default=false];
}
extend google.protobuf.FieldOptions {
+10 -24
View File
@@ -745,9 +745,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
#endif
return size;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
@@ -757,9 +755,7 @@ SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) c
#endif
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SensorStateResponse::calculate_size() const {
uint32_t SensorStateResponse::calculate_size() const {
uint32_t size = 0;
size += 5;
size += ProtoSize::calc_float(1, this->state);
@@ -916,22 +912,16 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
}
return true;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SubscribeLogsResponse::calculate_size() const {
uint32_t SubscribeLogsResponse::calculate_size() const {
uint32_t size = 0;
size += 2;
size += ProtoSize::calc_length_force(1, this->message_len_);
size += this->level ? 2 : 0;
size += ProtoSize::calc_length(1, this->message_len_);
return size;
}
#ifdef USE_API_NOISE
@@ -2338,9 +2328,7 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
}
return true;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
@@ -2362,9 +2350,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
}
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
BluetoothLERawAdvertisementsResponse::calculate_size() const {
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
uint32_t size = 0;
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
+26 -8
View File
@@ -298,15 +298,33 @@ constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
class ProtoEncode {
public:
/// Write a multi-byte varint directly through a pos pointer.
/// Unrolled based on the compile-time max varint length for T
/// (5 bytes for uint32_t, 10 bytes for uint64_t). The explicit unroll gives
/// the compiler straight-line code with early exits instead of a
/// data-dependent back-edge branch, which measurably speeds up BLE raw
/// advertisement MAC encoding (always 7-byte varints) among other hot paths.
///
/// Takes/returns pos by value so this function can remain out-of-line without
/// forcing the caller to spill pos to memory on every byte write. Callers
/// (which are ALWAYS_INLINE) should assign the returned pos back to their
/// local. Code size win vs. inlining the whole unroll at every call site.
template<typename T>
static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) {
do {
static uint8_t *encode_varint_raw_loop(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, T value) {
constexpr int MAX_VARINT_BYTES = (sizeof(T) * 8 + 6) / 7; // 5 for u32, 10 for u64
#pragma GCC unroll 10
for (int i = 0; i < MAX_VARINT_BYTES - 1; i++) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value | 0x80);
*pos++ = static_cast<uint8_t>(value) | 0x80;
value >>= 7;
} while (value > 0x7F);
if (value <= 0x7F) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value);
return pos;
}
}
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value);
return pos;
}
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t value) {
@@ -315,7 +333,7 @@ class ProtoEncode {
*pos++ = static_cast<uint8_t>(value);
return;
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
@@ -331,7 +349,7 @@ class ProtoEncode {
*pos++ = static_cast<uint8_t>(value >> 7);
return;
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint64_t value) {
@@ -340,7 +358,7 @@ class ProtoEncode {
*pos++ = static_cast<uint8_t>(value);
return;
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t type) {
@@ -402,7 +420,7 @@ class ProtoEncode {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
*pos++ = static_cast<uint8_t>(len);
} else {
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
}
std::memcpy(pos, string, len);
+2 -2
View File
@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
#endif
float get_reference_voltage(uint8_t phase) {
#ifdef USE_NUMBER
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
#else
return 120.0; // Default voltage
#endif
}
float get_reference_current(uint8_t phase) {
#ifdef USE_NUMBER
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
#else
return 5.0f; // Default current
#endif
+1 -1
View File
@@ -63,7 +63,7 @@ void BM8563::read_time() {
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+1
View File
@@ -162,6 +162,7 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
await cg.register_parented(var, config[CONF_CANBUS_ID])
if (can_id := config.get(CONF_CAN_ID)) is not None:
can_id = await cg.templatable(can_id, args, cg.uint32)
cg.add(var.set_can_id(can_id))
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
+9 -16
View File
@@ -102,8 +102,6 @@ CC1101Component::CC1101Component() {
memset(this->pa_table_, 0, sizeof(this->pa_table_));
}
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
void CC1101Component::setup() {
this->spi_setup();
this->cs_->digital_write(true);
@@ -150,12 +148,11 @@ void CC1101Component::setup() {
// Defer pin mode setup until after all components have completed setup()
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
if (this->gdo0_pin_ != nullptr) {
this->defer([this]() {
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
}
});
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
}
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
this->disable_loop();
}
}
@@ -167,7 +164,6 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
}
void CC1101Component::loop() {
this->disable_loop();
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) {
return;
@@ -248,7 +244,6 @@ void CC1101Component::begin_tx() {
this->write_(Register::PKTCTRL0, 0x32);
ESP_LOGV(TAG, "Beginning TX sequence");
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->detach_interrupt();
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
}
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
@@ -678,12 +673,10 @@ void CC1101Component::set_packet_mode(bool value) {
this->state_.GDO0_CFG = 0x0D;
}
if (this->initialized_) {
if (this->gdo0_pin_ != nullptr) {
if (value) {
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
} else {
this->gdo0_pin_->detach_interrupt();
}
if (value) {
this->enable_loop();
} else {
this->disable_loop();
}
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
-1
View File
@@ -93,7 +93,6 @@ class CC1101Component : public Component,
// GDO pin for packet reception
InternalGPIOPin *gdo0_pin_{nullptr};
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
+1 -1
View File
@@ -44,7 +44,7 @@ void DS1307Component::read_time() {
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+11 -35
View File
@@ -671,12 +671,11 @@ def _is_framework_url(source: str) -> bool:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 8),
"latest": cv.Version(3, 3, 8),
"dev": cv.Version(3, 3, 8),
"recommended": cv.Version(3, 3, 7),
"latest": cv.Version(3, 3, 7),
"dev": cv.Version(3, 3, 7),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
@@ -696,7 +695,6 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(3, 3, 8): cv.Version(5, 5, 4),
cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"),
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
@@ -716,15 +714,17 @@ ARDUINO_IDF_VERSION_LOOKUP = {
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 5, 4),
"latest": cv.Version(5, 5, 4),
"recommended": cv.Version(5, 5, 3, "1"),
"latest": cv.Version(5, 5, 3, "1"),
"dev": cv.Version(5, 5, 4),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
cv.Version(
5, 5, 4
): "https://github.com/pioarduino/platform-espressif32.git#develop",
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
@@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 38, "1"),
"latest": cv.Version(55, 3, 38, "1"),
"recommended": cv.Version(55, 3, 37),
"latest": cv.Version(55, 3, 37),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
}
@@ -1058,7 +1058,6 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
# VFS requirement tracking
# Components that need VFS features can call require_vfs_*() functions
@@ -1072,7 +1071,6 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
def require_vfs_select() -> None:
@@ -1170,17 +1168,6 @@ def require_fatfs() -> None:
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
def require_adc_oneshot_iram() -> None:
"""Mark that ADC oneshot IRAM safety is required by a component.
Call this from components that use the ADC oneshot driver. When flash cache is
disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management),
the ADC oneshot read function must be in IRAM to avoid crashes.
This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM.
"""
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *)
@@ -1222,7 +1209,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
*ESP32_CHIP_REVISIONS, string=True
*ESP32_CHIP_REVISIONS
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
@@ -1281,7 +1268,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
}
),
@@ -2082,16 +2068,6 @@ async def to_code(config):
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
# Place ADC oneshot control functions in IRAM for cache safety
# When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread,
# power management, etc.), ADC reads will crash if these functions are in flash.
# Components using ADC call require_adc_oneshot_iram() to force this.
if (
CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False)
or advanced[CONF_ADC_ONESHOT_IN_IRAM]
):
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
# Disable FATFS support
# Components that need FATFS (SD card, etc.) can call require_fatfs()
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
-16
View File
@@ -1960,10 +1960,6 @@ BOARDS = {
"name": "Hornbill ESP32 Minima",
"variant": VARIANT_ESP32,
},
"huidu_hd_wf1": {
"name": "Huidu HD-WF1",
"variant": VARIANT_ESP32S2,
},
"huidu_hd_wf2": {
"name": "Huidu HD-WF2",
"variant": VARIANT_ESP32S3,
@@ -2032,10 +2028,6 @@ BOARDS = {
"name": "LilyGo T-Display-S3",
"variant": VARIANT_ESP32S3,
},
"lilygo-t-energy-s3": {
"name": "LilyGo T-Energy-S3",
"variant": VARIANT_ESP32S3,
},
"lilygo-t3-s3": {
"name": "LilyGo T3-S3",
"variant": VARIANT_ESP32S3,
@@ -2297,18 +2289,10 @@ BOARDS = {
"name": "S.ODI Ultra v1",
"variant": VARIANT_ESP32,
},
"seeed_xiao_esp32_s3_plus": {
"name": "Seeed Studio XIAO ESP32S3 Plus",
"variant": VARIANT_ESP32S3,
},
"seeed_xiao_esp32c3": {
"name": "Seeed Studio XIAO ESP32C3",
"variant": VARIANT_ESP32C3,
},
"seeed_xiao_esp32c5": {
"name": "Seeed Studio XIAO ESP32C5",
"variant": VARIANT_ESP32C5,
},
"seeed_xiao_esp32c6": {
"name": "Seeed Studio XIAO ESP32C6",
"variant": VARIANT_ESP32C6,
+3 -9
View File
@@ -172,16 +172,10 @@ def validate_gpio_pin(pin):
exc,
)
else:
# `ignore_pin_validation_error` only suppresses an error raised by the
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
# numbers). If that didn't raise, the option is a no-op -- warn so the
# user can clean it up, but don't block the build.
# Throw an exception if used for a pin that would not have resulted
# in a validation error anyway!
if ignore_pin_validation_warning:
_LOGGER.warning(
"GPIO%d has no validation errors to ignore; "
"remove `ignore_pin_validation_error: true` from this pin.",
pin[CONF_NUMBER],
)
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
return pin
@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
int reset_pin_{-1};
int phy_addr_spi_{-1};
int clock_speed_;
spi_host_device_t interface_{SPI2_HOST};
spi_host_device_t interface_{SPI3_HOST};
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
uint32_t polling_interval_{0};
#endif
+1 -6
View File
@@ -108,13 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren)
# Use the global's value_type alias as the lambda return type so
# TemplatableFn stores a direct function pointer instead of going through
# the deprecated converting trampoline when the value expression deduces
# to a different type (e.g. int literal assigned to a float global).
value_type = cg.RawExpression(f"{full_id.type}::value_type")
templ = await cg.templatable(
config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression
config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
)
cg.add(var.set_value(templ))
return var
@@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
)
INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
def _validate_esp32_variant(config):
@@ -1,7 +1,5 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace ili9xxx {
@@ -229,10 +229,6 @@ void ILI9XXXDisplay::update() {
}
void ILI9XXXDisplay::display_() {
// buffer may be null if allocation failed
if (this->buffer_ == nullptr) {
return;
}
// check if something was displayed
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
return;
+7 -10
View File
@@ -28,6 +28,7 @@ from esphome.const import (
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.final_validate import full_config
_LOGGER = logging.getLogger(__name__)
@@ -675,16 +676,12 @@ def _final_validate(config):
:param config:
:return:
"""
config = config.copy()
for c in config:
if byte_order := c.get(CONF_BYTE_ORDER):
if byte_order == "BIG_ENDIAN":
_LOGGER.warning(
"The image '%s' is configured with big-endian byte order, little-endian is expected",
c.get(CONF_FILE),
)
else:
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
fv = full_config.get()
if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config):
config = config.copy()
for c in config:
if not c.get(CONF_BYTE_ORDER):
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config
+1 -1
View File
@@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const {
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
+6 -57
View File
@@ -58,12 +58,6 @@ void AddressableLightTransformer::start() {
// our transition will handle brightness, disable brightness in correction.
this->light_.correction_.set_local_brightness(255);
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
// Uniformity scan is deferred to the first apply() call. start() can run before the underlying
// LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE
// triggering a transition), and reading through ESPColorView would deref a null buffer.
this->uniform_start_scanned_ = false;
this->uniform_start_is_uniform_ = false;
}
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
@@ -103,57 +97,12 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
// non-linear when applying small deltas.
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
// Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the
// frame buffer is valid. When every LED already has the same color (the common case: plain
// turn_on/turn_off on a uniform strip), interpolate math-only against a single start color.
// Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip
// quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27
// round to stored 0, freezing progress).
if (!this->uniform_start_scanned_) {
this->uniform_start_scanned_ = true;
if (this->light_.size() > 0) {
Color first = this->light_[0].get();
bool uniform = true;
for (int32_t i = 1; i < this->light_.size(); i++) {
if (this->light_[i].get() != first) {
uniform = false;
break;
}
}
if (uniform) {
this->uniform_start_color_ = first;
this->uniform_start_is_uniform_ = true;
}
}
}
if (this->uniform_start_is_uniform_) {
// All LEDs started at the same color: compute the interpolated value once and write it to
// every LED. No read-back, so each LED's stored byte advances through every gamma threshold
// as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
//
// Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be
// overwritten on the next apply() here. The fallback path below would have respected them
// via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we
// support, so this is acceptable.
// lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
const Color &start = this->uniform_start_color_;
int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining);
uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining);
uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining);
uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining);
for (auto led : this->light_) {
led.set_rgbw(r, g, b, w);
}
} else {
int32_t scale =
int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
this->last_transition_progress_ = smoothed_progress;
this->light_.schedule_show();
@@ -115,9 +115,6 @@ class AddressableLightTransformer : public LightTransformer {
AddressableLight &light_;
float last_transition_progress_{0.0f};
Color target_color_{};
Color uniform_start_color_{};
bool uniform_start_scanned_{false};
bool uniform_start_is_uniform_{false};
};
} // namespace esphome::light
+2 -4
View File
@@ -44,7 +44,6 @@ from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from esphome.writer import clean_build
from esphome.yaml_util import load_yaml
from . import defines as df, helpers, lv_validation as lvalid, widgets
@@ -342,7 +341,7 @@ async def to_code(configs):
df.LOGGER.info("LVGL will use hardware rotation via display driver")
else:
rotation_type = RotationType.ROTATION_SOFTWARE
if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32P4:
if get_esp32_variant() == VARIANT_ESP32P4:
df.LOGGER.info("LVGL will use software rotation (PPA accelerated)")
else:
df.LOGGER.info("LVGL will use software rotation")
@@ -452,8 +451,7 @@ async def to_code(configs):
df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1")
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
clean_build(clear_pio_cache=False)
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
cg.add_build_flag("-DLV_CONF_H=1")
# handle windows paths in a way that doesn't break the generated C++
lv_conf_h_path = Path(lv_conf_h_file).as_posix()
+9 -11
View File
@@ -642,28 +642,26 @@ void LvglComponent::write_random_() {
int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000;
if (iterations <= 0)
iterations = 1;
int16_t width = lv_display_get_horizontal_resolution(this->disp_);
int16_t height = lv_display_get_vertical_resolution(this->disp_);
while (iterations-- != 0) {
int32_t col = random_uint32() % width;
int32_t col = random_uint32() % this->width_;
col = col / this->draw_rounding * this->draw_rounding;
int32_t row = random_uint32() % height;
int32_t row = random_uint32() % this->height_;
row = row / this->draw_rounding * this->draw_rounding;
// size will be between 8 and 32, and a multiple of draw_rounding
int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding;
lv_area_t area{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1};
lv_area_t area{col, row, col + size - 1, row + size - 1};
// clip to display bounds just in case
if (area.x2 >= width)
area.x2 = width - 1;
if (area.y2 >= height)
area.y2 = height - 1;
if (area.x2 >= this->width_)
area.x2 = this->width_ - 1;
if (area.y2 >= this->height_)
area.y2 = this->height_ - 1;
// line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer
size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
for (size_t i = 0; i != line_len; i++) {
reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32();
((uint32_t *) (this->draw_buf_))[i] = random_uint32();
}
this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_));
this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_);
}
}
+5 -6
View File
@@ -76,17 +76,16 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
}
#endif
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
#if LV_USE_IMAGE
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
#endif // LV_USE_IMAGE
// Shortcut / overload, so that the source of an image can easily be updated
// from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); }
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
}
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
}
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
+2 -5
View File
@@ -77,11 +77,8 @@ class ArcType(NumberType):
# start_angle and end_angle are mapped to bg_start_angle and bg_end_angle
prop = str(prop)
if prop.endswith("_angle"):
await w.set_property(
"bg_" + prop, await validator.process(config.get(prop))
)
else:
await w.set_property(prop, config, processor=validator)
prop = "bg_" + prop
await w.set_property(prop, config, processor=validator)
if CONF_ADJUSTABLE in config:
if not config[CONF_ADJUSTABLE]:
lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB)
+1 -1
View File
@@ -170,7 +170,7 @@ async def to_code(config):
cg.add_library("LEAmDNS", None)
if CORE.is_esp32:
add_idf_component(name="espressif/mdns", ref="1.11.0")
add_idf_component(name="espressif/mdns", ref="1.10.0")
cg.add_define("USE_MDNS")
@@ -451,8 +451,6 @@ async def to_code(config):
ota.request_ota_state_listeners()
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
@@ -29,6 +29,14 @@ void VADModel::log_model_config() {
bool StreamingModel::load_model_() {
RAMAllocator<uint8_t> arena_allocator;
if (this->tensor_arena_ == nullptr) {
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
if (this->tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
return false;
}
}
if (this->var_arena_ == nullptr) {
this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
if (this->var_arena_ == nullptr) {
@@ -45,26 +53,6 @@ bool StreamingModel::load_model_() {
return false;
}
// Probe for the actual required tensor arena size if not yet determined
if (!this->tensor_arena_size_probed_) {
size_t probed_size = this->probe_arena_size_();
if (probed_size > 0) {
ESP_LOGD(TAG, "Probed tensor arena size: %zu bytes", probed_size);
this->tensor_arena_size_ = probed_size;
} else {
ESP_LOGW(TAG, "Arena size probe failed, using manifest size: %zu bytes", this->tensor_arena_size_);
}
this->tensor_arena_size_probed_ = true;
}
if (this->tensor_arena_ == nullptr) {
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
if (this->tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
return false;
}
}
if (this->interpreter_ == nullptr) {
this->interpreter_ =
make_unique<tflite::MicroInterpreter>(tflite::GetModel(this->model_start_), this->streaming_op_resolver_,
@@ -106,70 +94,6 @@ bool StreamingModel::load_model_() {
return true;
}
size_t StreamingModel::probe_arena_size_() {
RAMAllocator<uint8_t> arena_allocator;
// Try with the manifest size first, then escalates to 1.5, then 2x if it fails. Different platforms and different
// versions of the esp-nn library require different amounts of memory, so the manifest size may not always be correct,
// and probing allows us to find the actual required size for the current build and platform. Aligns test sizes to 16
// bytes.
size_t attempt_sizes[] = {(this->tensor_arena_size_ + 15) & ~15, (this->tensor_arena_size_ * 3 / 2 + 15) & ~15,
(this->tensor_arena_size_ * 2 + 15) & ~15};
for (size_t attempt_size : attempt_sizes) {
uint8_t *probe_arena = arena_allocator.allocate(attempt_size);
if (probe_arena == nullptr) {
continue;
}
// Verify the model works at all with this arena size
auto probe_interpreter = make_unique<tflite::MicroInterpreter>(
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, attempt_size, this->mrv_);
if (probe_interpreter->AllocateTensors() != kTfLiteOk) {
probe_interpreter.reset();
arena_allocator.deallocate(probe_arena, attempt_size);
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
continue;
}
// Try to shrink the arena. Start with arena_used_bytes() + 16 (rounded to 16-byte alignment).
// If that works, use it. Otherwise, try midpoints between that and the full size until one succeeds.
size_t lower = (probe_interpreter->arena_used_bytes() + 16 + 15) & ~15;
probe_interpreter.reset();
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
size_t upper = attempt_size;
while (lower < upper) {
auto test_interpreter = make_unique<tflite::MicroInterpreter>(
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, lower, this->mrv_);
bool ok = test_interpreter->AllocateTensors() == kTfLiteOk;
test_interpreter.reset();
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
if (ok) {
// Found a working size smaller than the full arena
upper = lower + 16; // Pad by 16 bytes to be safe for future allocations
break;
}
// Try the midpoint between current attempt and full size
lower = ((lower + upper) / 2 + 15) & ~15;
}
arena_allocator.deallocate(probe_arena, attempt_size);
return upper;
}
return 0;
}
void StreamingModel::unload_model() {
this->interpreter_.reset();
@@ -63,10 +63,6 @@ class StreamingModel {
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
/// @return True if successful, false otherwise
bool load_model_();
/// @brief Probes the actual required tensor arena size by trial allocation.
/// Tries the manifest size first, then 2x if that fails.
/// @return The required arena size rounded up to 16-byte alignment, or 0 on failure.
size_t probe_arena_size_();
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver);
@@ -74,7 +70,6 @@ class StreamingModel {
bool loaded_{false};
bool enabled_{true};
bool tensor_arena_size_probed_{false};
bool unprocessed_probability_status_{false};
uint8_t current_stride_step_{0};
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
+1 -2
View File
@@ -28,8 +28,7 @@ void AirConditioner::on_status_change() {
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
// Read existing presets (set by codegen), append frost protection, write back
auto traits = this->get_traits();
const auto &existing = traits.get_supported_custom_presets();
const auto &existing = this->get_traits().get_supported_custom_presets();
bool found = false;
for (const char *p : existing) {
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {
+3 -3
View File
@@ -195,7 +195,7 @@ def model_schema(config):
"big_endian", "little_endian", lower=True
),
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
model.option(CONF_DRAW_ROUNDING, 1): power_of_two,
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
*pixel_modes, lower=True
),
@@ -297,9 +297,9 @@ def _final_validate(config):
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
fraction = min(20000.0, buffer_size // 4) / buffer_size
fraction = min(20000.0, buffer_size // 16) / buffer_size
config[CONF_BUFFER_SIZE] = 1.0 / next(
(x for x in range(2, 8) if fraction >= 1 / x), 8
x for x in range(2, 17) if fraction >= 1 / x
)
+11 -11
View File
@@ -234,9 +234,9 @@ class MipiSpi : public display::Display,
}
void dump_config() override {
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
HAS_HARDWARE_ROTATION);
}
@@ -305,7 +305,7 @@ class MipiSpi : public display::Display,
this->write_command_(BRIGHTNESS, this->brightness_.value());
// calculate new madctl value from base value adjusted for rotation
uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only
uint8_t madctl = MADCTL; // lower 8 bits only
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
@@ -546,12 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING;
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) {
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
this->start_line_ += this->get_height_internal() / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal());
this->end_line_ =
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
if (this->auto_clear_enabled_) {
this->clear();
}
@@ -573,13 +574,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
// Some chips require that the drawing window be aligned on certain boundaries
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
this->x_high_ = round_buffer(this->x_high_ + 1) - 1;
this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1);
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_,
round_buffer(this->get_width_internal()) - w - this->x_low_);
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
// invalidate watermarks
this->x_low_ = this->get_width_internal();
this->y_low_ = this->get_height_internal();
@@ -7,7 +7,7 @@ namespace esphome::mitsubishi_cn105 {
static const char *const TAG = "mitsubishi_cn105.climate";
static constexpr std::array MODE_MAP{
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_HEAT_COOL},
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT},
std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY},
std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL},
@@ -76,13 +76,23 @@ void MitsubishiCN105Climate::loop() {
climate::ClimateTraits MitsubishiCN105Climate::traits() {
climate::ClimateTraits traits;
for (const auto &p : MODE_MAP) {
traits.add_supported_mode(p.second);
}
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_DRY,
climate::CLIMATE_MODE_FAN_ONLY,
climate::CLIMATE_MODE_AUTO,
});
for (const auto &p : FAN_MODE_MAP) {
traits.add_supported_fan_mode(p.second);
}
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_MIDDLE,
climate::CLIMATE_FAN_HIGH,
});
traits.set_visual_min_temperature(16.0f);
traits.set_visual_max_temperature(31.0f);
+34 -74
View File
@@ -36,9 +36,8 @@ bool Nextion::send_command_(const std::string &command) {
}
#ifdef USE_NEXTION_COMMAND_SPACING
const uint32_t now = App.get_loop_component_start_time();
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send(now)) {
ESP_LOGN(TAG, "Command spacing: delaying '%s'", command.c_str());
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) {
ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
return false;
}
#endif // USE_NEXTION_COMMAND_SPACING
@@ -49,16 +48,6 @@ bool Nextion::send_command_(const std::string &command) {
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
this->write_array(to_send, sizeof(to_send));
#ifdef USE_NEXTION_COMMAND_SPACING
// Mark sent immediately after writing to UART. The pacer enforces inter-command
// spacing from the transmit side. Marking on ACK (0x01) would leave last_command_time_
// at zero indefinitely, making can_send() always return true and spacing a no-op.
// ignore_is_setup_ commands (setup/init sequence) bypass spacing intentionally.
if (!this->connection_state_.ignore_is_setup_) {
this->command_pacer_.mark_sent(now);
}
#endif // USE_NEXTION_COMMAND_SPACING
return true;
}
@@ -264,8 +253,11 @@ bool Nextion::send_command(const char *command) {
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
return false;
this->add_no_result_to_queue_with_command_("command", command);
return true;
if (this->send_command_(command)) {
this->add_no_result_to_queue_("command");
return true;
}
return false;
}
bool Nextion::send_command_printf(const char *format, ...) {
@@ -282,8 +274,11 @@ bool Nextion::send_command_printf(const char *format, ...) {
return false;
}
this->add_no_result_to_queue_with_command_("command_printf", buffer);
return true;
if (this->send_command_(buffer)) {
this->add_no_result_to_queue_("command_printf");
return true;
}
return false;
}
#ifdef NEXTION_PROTOCOL_LOG
@@ -354,43 +349,25 @@ void Nextion::loop() {
}
#ifdef USE_NEXTION_COMMAND_SPACING
// Try to send any pending commands if spacing allows
this->process_pending_in_queue_();
#ifdef USE_NEXTION_WAVEFORM
if (!this->waveform_queue_.empty()) {
this->check_pending_waveform_();
}
#endif // USE_NEXTION_WAVEFORM
#endif // USE_NEXTION_COMMAND_SPACING
}
#ifdef USE_NEXTION_COMMAND_SPACING
void Nextion::process_pending_in_queue_() {
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
size_t commands_sent = 0;
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) {
return;
}
for (auto *item : this->nextion_queue_) {
if (item == nullptr || item->pending_command.empty()) {
continue; // Already sent, waiting for ACK — skip, don't stop
// Check if first item in queue has a pending command
auto *front_item = this->nextion_queue_.front();
if (front_item && !front_item->pending_command.empty()) {
if (this->send_command_(front_item->pending_command)) {
// Command sent successfully, clear the pending command
front_item->pending_command.clear();
ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str());
}
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (++commands_sent > this->max_commands_per_loop_) {
ESP_LOGV(TAG, "Pending cmds: loop limit reached, deferring");
break;
}
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
const uint32_t now = App.get_loop_component_start_time();
if (!this->command_pacer_.can_send(now)) {
break; // Spacing not elapsed, stop for this loop iteration
}
if (!this->send_command_(item->pending_command)) {
break; // Unexpected send failure, stop
}
item->pending_command.clear();
ESP_LOGVV(TAG, "Pending cmd sent: %s", item->component->get_variable_name().c_str());
}
}
#endif // USE_NEXTION_COMMAND_SPACING
@@ -493,6 +470,10 @@ void Nextion::process_nextion_commands_() {
this->setup_callback_.call();
}
}
#ifdef USE_NEXTION_COMMAND_SPACING
this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent
ESP_LOGN(TAG, "Command spacing: marked command sent");
#endif
break;
case 0x02: // invalid Component ID or name was used
ESP_LOGW(TAG, "Invalid component ID/name");
@@ -1098,18 +1079,10 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
}
/**
* @brief Send a command and enqueue it for response tracking.
* @brief
*
* Callers are responsible for checking is_sleeping() before calling this
* method. The sleep guard is deliberately absent here because some callers
* (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly
* sleep-safe and must bypass it.
*
* If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready,
* the command is saved in the queue entry for retry rather than dropped.
*
* @param variable_name Name of the variable or component associated with the command.
* @param command The raw command string to send.
* @param variable_name Variable name for the queue
* @param command
*/
void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) {
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty())
@@ -1290,22 +1263,9 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
std::string command = "get " + component->get_variable_name_to_send();
#ifdef USE_NEXTION_COMMAND_SPACING
// Always enqueue first so the response handler is present when the command
// is eventually sent. Store the command for retry if spacing blocked it;
// process_pending_in_queue_() will transmit it when the pacer allows.
nextion_queue->pending_command = command;
this->nextion_queue_.push_back(nextion_queue);
if (this->send_command_(command)) {
nextion_queue->pending_command.clear();
}
#else // USE_NEXTION_COMMAND_SPACING
if (this->send_command_(command)) {
this->nextion_queue_.push_back(nextion_queue);
} else {
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
}
#endif // USE_NEXTION_COMMAND_SPACING
}
#ifdef USE_NEXTION_WAVEFORM
@@ -1349,10 +1309,10 @@ void Nextion::check_pending_waveform_() {
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
// If spacing or setup state blocks the send, leave the entry at the front
// of waveform_queue_ for retry on the next loop iteration via
// check_pending_waveform_(). Only pop on a successful send.
this->send_command_(command);
if (!this->send_command_(command)) {
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop();
}
}
#endif // USE_NEXTION_WAVEFORM
+5 -10
View File
@@ -55,20 +55,15 @@ class NextionCommandPacer {
uint8_t get_spacing() const { return spacing_ms_; }
/**
* @brief Check if enough time has passed to send the next command.
* @param now Current timestamp in milliseconds (use App.get_loop_component_start_time()
* for consistency with the rest of the queue timing).
* @return true if the spacing interval has elapsed since the last command was sent.
* @brief Check if enough time has passed to send next command
* @return true if enough time has passed since last command
*/
bool can_send(uint32_t now) const { return (now - last_command_time_) >= spacing_ms_; }
bool can_send() const { return (millis() - last_command_time_) >= spacing_ms_; }
/**
* @brief Record the transmit timestamp for the most recently sent command.
* @param now Current timestamp in milliseconds, as returned by
* App.get_loop_component_start_time(). Must use the same clock
* source as can_send() to avoid unsigned underflow.
* @brief Mark a command as sent, updating the timing
*/
void mark_sent(uint32_t now) { last_command_time_ = now; }
void mark_sent() { last_command_time_ = millis(); }
private:
uint8_t spacing_ms_;
+10 -54
View File
@@ -8,11 +8,8 @@ from typing import Any
from esphome import git, yaml_util
from esphome.components.substitutions import (
ContextVars,
ErrList,
push_context,
raise_first_undefined,
resolve_include,
resolve_substitutions_block,
substitute,
)
from esphome.components.substitutions.jinja import has_jinja
@@ -48,18 +45,6 @@ def is_remote_package(package_config: dict) -> bool:
return CONF_URL in package_config
def is_package_definition(value: object) -> bool:
"""Returns True if the value looks like a package definition rather than a config fragment.
Package definitions are IncludeFile objects, git URL shorthand strings, or
remote package dicts (containing a ``url:`` key). Config fragments are
plain dicts that represent component configuration.
"""
return isinstance(value, (yaml_util.IncludeFile, str)) or (
isinstance(value, dict) and is_remote_package(value)
)
def valid_package_contents(package_config: dict) -> dict:
"""Validate that a package looks like a plausible ESPHome config fragment.
@@ -324,23 +309,20 @@ def _walk_packages(
return config
packages = config[CONF_PACKAGES]
with cv.prepend_path(CONF_PACKAGES):
if isinstance(packages, yaml_util.IncludeFile):
# If the packages key is an IncludeFile, resolve it first before processing.
packages, _ = resolve_include(packages, [], context, strict_undefined=False)
if not isinstance(packages, (dict, list)):
raise cv.Invalid(
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
)
if not isinstance(packages, (dict, list)):
raise cv.Invalid(
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
)
with cv.prepend_path(CONF_PACKAGES):
if not isinstance(packages, dict):
_walk_package_list(packages, callback, context)
elif (result := _walk_package_dict(packages, callback, context)) is not None:
if not validate_deprecated or any(
is_package_definition(v) for v in packages.values()
):
if not validate_deprecated:
raise result
# Fallback: treat the dict as a single deprecated package.
# Note: this catches *any* cv.Invalid from the callback, which may
# mask real validation errors in named package dicts.
# This block can be removed once the single-package
# deprecation period (2026.7.0) is over.
config[CONF_PACKAGES] = [packages]
@@ -362,19 +344,12 @@ def _substitute_package_definition(
if isinstance(package_config, str) or (
isinstance(package_config, dict) and is_remote_package(package_config)
):
# Collect undefined-variable errors (rather than raising strict) so the
# path walked through a remote-package dict is preserved and the user
# sees which field (url / path / ref / ...) referenced the undefined
# variable.
errors: ErrList = []
package_config = substitute(
item=package_config,
path=[],
parent_context=context_vars or ContextVars(),
strict_undefined=False,
errors=errors,
)
raise_first_undefined(errors, package_config, "package definition")
return package_config
@@ -486,9 +461,6 @@ class _PackageProcessor:
self, package_config: dict | str, context_vars: ContextVars | None
) -> dict:
"""Resolve a single package and recurse into any nested packages."""
from_remote = isinstance(package_config, dict) and is_remote_package(
package_config
)
package_config = self.resolve_package(package_config, context_vars)
self.collect_substitutions(package_config)
@@ -498,18 +470,7 @@ class _PackageProcessor:
# Push context from !include vars on the package root and on the packages key
context_vars = push_context(package_config, context_vars)
context_vars = push_context(package_config[CONF_PACKAGES], context_vars)
# Disable the deprecated single-package fallback for remote
# packages. _process_remote_package returns dicts with
# already-resolved values that is_package_definition cannot
# distinguish from config fragments, so the fallback would
# always fire and mask real errors with wrong paths
# (packages->0 instead of packages-><name>).
return _walk_packages(
package_config,
self.process_package,
context_vars,
validate_deprecated=not from_remote,
)
return _walk_packages(package_config, self.process_package, context_vars)
def do_packages_pass(
@@ -526,12 +487,7 @@ def do_packages_pass(
if CONF_PACKAGES not in config:
return config
with cv.prepend_path(CONF_SUBSTITUTIONS):
substitutions = UserDict(
resolve_substitutions_block(
config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions
)
)
substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {}))
processor = _PackageProcessor(
substitutions, command_line_substitutions, skip_update
)
+1 -1
View File
@@ -44,7 +44,7 @@ void PCF85063Component::read_time() {
.year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+1 -1
View File
@@ -44,7 +44,7 @@ void PCF8563Component::read_time() {
.year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+2 -2
View File
@@ -100,7 +100,7 @@ void QMC5883LComponent::update() {
// ROL_PNT in setup and reading 7 bytes starting at the status register.
// If status and all three axes are desired, using ROL_PNT saves you 3 bytes.
// But simply not reading status saves you 4 bytes always and is much simpler.
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) {
err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1);
if (err != i2c::ERROR_OK) {
char buf[32];
@@ -165,7 +165,7 @@ void QMC5883LComponent::update() {
temp = int16_t(raw_temp) * 0.01f;
}
ESP_LOGV(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
temp, status);
if (this->x_sensor_ != nullptr)
+6 -25
View File
@@ -9,7 +9,7 @@
#include <WiFi.h>
#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock)
#elif defined(USE_ETHERNET)
#include <lwip_wrap.h> // For LWIPMutex — LwIPLock mirrors its semantics (see below)
#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock)
#include "esphome/components/ethernet/ethernet_component.h"
#endif
#include <hardware/structs/rosc.h>
@@ -43,18 +43,9 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat
// assertion failures). See esphome#10681.
//
// WiFi uses cyw43_arch_lwip_begin/end.
//
// For wired Ethernet, taking only the async_context lock is NOT enough. The
// W5500 GPIO IRQ path (LwipIntfDev::_irq) checks arduino-pico's `__inLWIP`
// counter to decide whether to defer packet processing. If we hold the
// async_context lock without bumping `__inLWIP`, an interrupt-driven packet
// arrival re-enters lwIP from IRQ context and corrupts pbufs (the `pbuf_cat`
// assertion crash on wiznet-w5500-evb-pico). We mirror arduino-pico's
// LWIPMutex (cores/rp2040/lwip_wrap.h) exactly: bump `__inLWIP`, take the
// lock, and on release re-unmask any GPIO IRQs that were deferred while we
// held it. We can't `using LwIPLock = LWIPMutex;` in helpers.h because
// pulling lwip_wrap.h there poisons many TUs with lwIP types.
// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end.
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
// firing during critical sections.
//
// When neither WiFi nor Ethernet is configured, this is a no-op since
// there's no network stack and no lwip callbacks to race with.
@@ -62,18 +53,8 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
#elif defined(USE_ETHERNET)
LwIPLock::LwIPLock() {
__inLWIP++;
ethernet_arch_lwip_begin();
}
LwIPLock::~LwIPLock() {
ethernet_arch_lwip_end();
__inLWIP--;
if (__needsIRQEN && !__inLWIP) {
__needsIRQEN = false;
ethernet_arch_lwip_gpio_unmask();
}
}
LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); }
#else
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
@@ -127,9 +127,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
uint32_t pos = this->get_position_(x, y);
Color mapped_color = color;
this->map_chroma_key(mapped_color);
this->buffer_[pos + 0] = mapped_color.b;
this->buffer_[pos + 0] = mapped_color.r;
this->buffer_[pos + 1] = mapped_color.g;
this->buffer_[pos + 2] = mapped_color.r;
this->buffer_[pos + 2] = mapped_color.b;
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 3] = color.w;
}
@@ -32,101 +32,40 @@ void RuntimeStatsCollector::log_stats_() {
" Period stats (last %" PRIu32 "ms): %zu active components",
this->log_interval_, count);
// Sum component time so we can derive main-loop overhead
// (active loop time minus time attributable to component loop()s).
// Period sum iterates the active-in-period subset; total sum must iterate
// all components since total_active_time_us_ includes iterations where
// currently-idle components previously ran.
uint64_t period_component_sum_us = 0;
if (count == 0) {
return;
}
// Sort by period runtime (descending)
std::sort(sorted, sorted + count, compare_period_time);
// Log top components by period runtime
for (size_t i = 0; i < count; i++) {
period_component_sum_us += sorted[i]->runtime_stats_.period_time_us;
}
uint64_t total_component_sum_us = 0;
for (auto *component : components) {
total_component_sum_us += component->runtime_stats_.total_time_us;
}
if (count > 0) {
// Sort by period runtime (descending)
std::sort(sorted, sorted + count, compare_period_time);
// Log top components by period runtime
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
}
}
// Main-loop overhead for the period: active wall time minus component time.
// active = sum of per-iteration loop time excluding yield/sleep.
if (this->period_active_count_ > 0) {
uint64_t active = this->period_active_time_us_;
uint64_t overhead = active > period_component_sum_us ? active - period_component_sum_us : 0;
// Use double for µs→ms conversion so multi-day uptimes (where total
// microsecond counters exceed float's ~7-digit mantissa) keep resolution.
ESP_LOGI(TAG,
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
"overhead_total=%.1fms",
this->period_active_count_,
static_cast<double>(active) / static_cast<double>(this->period_active_count_) / 1000.0,
static_cast<double>(this->period_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
static_cast<double>(overhead) / 1000.0);
uint64_t before = this->period_before_time_us_;
uint64_t tail = this->period_tail_time_us_;
uint64_t accounted = before + tail;
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
static_cast<double>(inter) / 1000.0);
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
}
// Log total stats since boot (only for active components - idle ones haven't changed)
ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
if (count > 0) {
// Re-sort by total runtime for all-time stats
std::sort(sorted, sorted + count, compare_total_time);
// Re-sort by total runtime for all-time stats
std::sort(sorted, sorted + count, compare_total_time);
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
}
}
if (this->total_active_count_ > 0) {
uint64_t active = this->total_active_time_us_;
uint64_t overhead = active > total_component_sum_us ? active - total_component_sum_us : 0;
ESP_LOGI(TAG,
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
"overhead_total=%.1fms",
this->total_active_count_,
static_cast<double>(active) / static_cast<double>(this->total_active_count_) / 1000.0,
static_cast<double>(this->total_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
static_cast<double>(overhead) / 1000.0);
uint64_t before = this->total_before_time_us_;
uint64_t tail = this->total_tail_time_us_;
uint64_t accounted = before + tail;
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
static_cast<double>(inter) / 1000.0);
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
}
// Reset period stats
for (auto *component : components) {
component->runtime_stats_.reset_period();
}
this->period_active_count_ = 0;
this->period_active_time_us_ = 0;
this->period_active_max_us_ = 0;
this->period_before_time_us_ = 0;
this->period_tail_time_us_ = 0;
}
bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) {
@@ -29,31 +29,6 @@ class RuntimeStatsCollector {
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
// Record the wall time of one main loop iteration excluding the yield/sleep.
// Called once per loop from Application::loop().
// active_us = total time between loop start and just before yield.
// before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop).
// tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix.
// Residual overhead at log time = active Σ(component) before tail,
// which captures per-iteration inter-component bookkeeping (set_current_component,
// WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls,
// the for-loop itself).
void record_loop_active(uint32_t active_us, uint32_t before_us, uint32_t tail_us) {
this->period_active_count_++;
this->period_active_time_us_ += active_us;
if (active_us > this->period_active_max_us_)
this->period_active_max_us_ = active_us;
this->total_active_count_++;
this->total_active_time_us_ += active_us;
if (active_us > this->total_active_max_us_)
this->total_active_max_us_ = active_us;
this->period_before_time_us_ += before_us;
this->total_before_time_us_ += before_us;
this->period_tail_time_us_ += tail_us;
this->total_tail_time_us_ += tail_us;
}
protected:
void log_stats_();
// Static comparators — member functions have friend access, lambdas do not
@@ -62,22 +37,6 @@ class RuntimeStatsCollector {
uint32_t log_interval_;
uint32_t next_log_time_{0};
// Main loop active-time stats (wall time per iteration, excluding yield/sleep).
// Counters are uint64_t — at sub-millisecond loop times a uint32_t can wrap in
// a few weeks of uptime, which is well within ESPHome device lifetimes.
uint64_t period_active_count_{0};
uint64_t period_active_time_us_{0};
uint32_t period_active_max_us_{0};
uint64_t total_active_count_{0};
uint64_t total_active_time_us_{0};
uint32_t total_active_max_us_{0};
// Split of overhead sections — accumulated per iteration.
uint64_t period_before_time_us_{0};
uint64_t total_before_time_us_{0};
uint64_t period_tail_time_us_{0};
uint64_t total_tail_time_us_{0};
};
} // namespace runtime_stats
+1 -1
View File
@@ -81,7 +81,7 @@ void RX8130Component::read_time() {
.year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+8 -5
View File
@@ -55,11 +55,13 @@ void SafeModeComponent::dump_config() {
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
if (last_invalid != nullptr) {
ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label);
ESP_LOGW(TAG, "The device reset before the boot was marked successful");
ESP_LOGW(TAG,
"OTA rollback detected! Rolled back from partition '%s'\n"
" The device reset before the boot was marked successful",
last_invalid->label);
if (esp_reset_reason() == ESP_RST_BROWNOUT) {
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!");
ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n"
" See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
}
}
#endif
@@ -86,7 +88,8 @@ void SafeModeComponent::mark_successful() {
}
void SafeModeComponent::loop() {
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
if (!this->boot_successful_ &&
(App.get_loop_component_start_time() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
// successful boot, reset counter
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->mark_successful();
@@ -112,6 +112,8 @@ class BSDSocketImpl {
int setblocking(bool blocking);
int loop() { return 0; }
/// Check if the socket has buffered data ready to read.
/// See the ready() contract in socket.h — callers must drain or track remaining data.
bool ready() const;
int get_fd() const { return this->fd_; }
@@ -96,6 +96,8 @@ class LWIPRawImpl : public LWIPRawCommon {
errno = ENOSYS;
return -1;
}
// Check if the socket has buffered data ready to read.
// See the ready() contract in socket.h — callers must drain or track remaining data.
// Intentionally unlocked — this is a polling check called every loop iteration.
// A stale read at worst delays processing by one loop tick; the actual I/O in
// read() holds the lwip lock and re-checks properly. See esphome#10681.
@@ -78,6 +78,8 @@ class LwIPSocketImpl {
int setblocking(bool blocking);
int loop() { return 0; }
/// Check if the socket has buffered data ready to read.
/// See the ready() contract in socket.h — callers must drain or track remaining data.
bool ready() const;
int get_fd() const { return this->fd_; }
+13
View File
@@ -53,6 +53,19 @@ bool socket_ready_fd(int fd, bool loop_monitored);
// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd
// declared above, while the impl headers are included before those declarations.
//
// Contract (applies to ALL socket implementations — each platform implements
// ready() differently, but this contract holds regardless of the mechanism):
// ready() checks if the socket has buffered data ready to read. When it returns
// true, the caller MUST read until it would block (EAGAIN/EWOULDBLOCK), or until
// read() returns 0 to indicate EOF / connection closed, or track that it stopped
// early and retry without calling ready(). The next call to ready() will only
// report new data correctly if all callers fulfill this contract. Failing to
// drain the socket may cause ready() to return false while data remains readable.
//
// In practice each socket is owned by a single component, so this contract is
// straightforward to fulfill — but the owning component must be aware of it,
// especially if it limits how many messages it processes per loop iteration.
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
inline bool Socket::ready() const {
#ifdef USE_LWIP_FAST_SELECT
+10 -10
View File
@@ -45,8 +45,8 @@ MODELS = {
presets={
CONF_HEIGHT: 240,
CONF_WIDTH: 135,
CONF_OFFSET_HEIGHT: 40,
CONF_OFFSET_WIDTH: 52,
CONF_OFFSET_HEIGHT: 52,
CONF_OFFSET_WIDTH: 40,
CONF_CS_PIN: "GPIO5",
CONF_DC_PIN: "GPIO16",
CONF_RESET_PIN: "GPIO23",
@@ -68,8 +68,8 @@ MODELS = {
presets={
CONF_HEIGHT: 280,
CONF_WIDTH: 240,
CONF_OFFSET_HEIGHT: 20,
CONF_OFFSET_WIDTH: 0,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 20,
}
),
"ADAFRUIT_S2_TFT_FEATHER_240X135": model_spec(
@@ -77,8 +77,8 @@ MODELS = {
presets={
CONF_HEIGHT: 240,
CONF_WIDTH: 135,
CONF_OFFSET_HEIGHT: 40,
CONF_OFFSET_WIDTH: 52,
CONF_OFFSET_HEIGHT: 52,
CONF_OFFSET_WIDTH: 40,
CONF_CS_PIN: "GPIO7",
CONF_DC_PIN: "GPIO39",
CONF_RESET_PIN: "GPIO40",
@@ -89,8 +89,8 @@ MODELS = {
presets={
CONF_HEIGHT: 320,
CONF_WIDTH: 170,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 35,
CONF_OFFSET_HEIGHT: 35,
CONF_OFFSET_WIDTH: 0,
CONF_ROTATION: 270,
CONF_CS_PIN: "GPIO10",
CONF_DC_PIN: "GPIO13",
@@ -102,8 +102,8 @@ MODELS = {
presets={
CONF_HEIGHT: 320,
CONF_WIDTH: 172,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 34,
CONF_OFFSET_HEIGHT: 34,
CONF_OFFSET_WIDTH: 0,
CONF_ROTATION: 90,
CONF_CS_PIN: "GPIO21",
CONF_DC_PIN: "GPIO22",
+4 -81
View File
@@ -30,56 +30,6 @@ ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]]
jinja = Jinja()
def raise_first_undefined(
errors: ErrList,
source: Any,
context_label: str,
) -> None:
"""If *errors* is non-empty, raise ``cv.Invalid`` for the first undefined variable.
The raised error names the missing variable, the path walked into *source*
(for nested dicts, e.g. ``url`` or ``ref``), and the YAML source location
when *source* carries one. Only the first error is surfaced; the user will
re-run after fixing it and any remaining undefined variables will be
reported then.
``context_label`` is the noun describing where the undefined variable
appeared (e.g. ``"package definition"``).
"""
if not errors:
return
err, err_path, err_value = errors[0]
if len(errors) > 1:
# Log any further undefined variables so debug-level output covers
# the full set, even though only the first is surfaced to the user.
extras = ", ".join(
f"{e.message} at '{'->'.join(str(p) for p in p_path)}'"
for e, p_path, _ in errors[1:]
)
_LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras)
# Prefer the location of the offending scalar (e.g. the `url:` value) over
# the enclosing package-definition dict so the message points at the exact
# line/column that carries the undefined variable.
location_node = (
err_value
if isinstance(err_value, ESPHomeDataBase) and err_value.esp_range is not None
else source
)
location = ""
if (
isinstance(location_node, ESPHomeDataBase)
and location_node.esp_range is not None
):
mark = location_node.esp_range.start_mark
# DocumentLocation.line/column are 0-based (from the YAML Mark). Render
# as 1-based to match config.line_info() and editor line numbering.
location = f" (in {mark.document} {mark.line + 1}:{mark.column + 1})"
field = f" at '{'->'.join(str(p) for p in err_path)}'" if err_path else ""
raise cv.Invalid(
f"Undefined variable in {context_label}{field}: {err.message}{location}"
)
def validate_substitution_key(value: Any) -> str:
"""Validate and normalize a substitution key, stripping a leading ``$`` if present."""
value = cv.string(value)
@@ -464,34 +414,6 @@ def _warn_unresolved_variables(errors: ErrList) -> None:
)
def resolve_substitutions_block(
substitutions: Any,
command_line_substitutions: dict[str, Any] | None,
) -> dict[str, Any]:
"""Resolve a deferred ``substitutions: !include file.yaml`` and validate the shape.
The caller is responsible for wrapping the call in
``cv.prepend_path(CONF_SUBSTITUTIONS)`` for error reporting.
``command_line_substitutions`` seeds the filename context so
``substitutions: !include ${var}.yaml`` can reference CLI-provided vars.
"""
if isinstance(substitutions, IncludeFile):
# Single-shot resolution — matches ``_walk_packages`` for the
# ``packages: !include`` entry point. Chained includes (an include that
# itself loads another ``!include`` at the top level) are not supported.
substitutions, _ = resolve_include(
substitutions,
[],
ContextVars(command_line_substitutions or {}),
strict_undefined=False,
)
if not isinstance(substitutions, dict):
raise cv.Invalid(
f"Substitutions must be a key to value mapping, got {type(substitutions)}"
)
return substitutions
def do_substitution_pass(
config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None
) -> OrderedDict:
@@ -507,9 +429,10 @@ def do_substitution_pass(
# Use merge_dicts_ordered to preserve OrderedDict type for move_to_end()
substitutions = config.pop(CONF_SUBSTITUTIONS, {})
with cv.prepend_path(CONF_SUBSTITUTIONS):
substitutions = resolve_substitutions_block(
substitutions, command_line_substitutions
)
if not isinstance(substitutions, dict):
raise cv.Invalid(
f"Substitutions must be a key to value mapping, got {type(substitutions)}"
)
substitutions = merge_dicts_ordered(
substitutions, command_line_substitutions or {}
)
+2 -2
View File
@@ -200,11 +200,11 @@ CONFIG_SCHEMA = (
cv.hex_int, cv.Range(min=0, max=0xFFFF)
),
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.int_range(min=0, max=100000)
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_HW_VERSION): cv.one_of(
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
-9
View File
@@ -104,17 +104,11 @@ void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
delayMicroseconds(SWITCHING_DELAY_US);
}
void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
void SX126x::setup() {
// setup pins
this->busy_pin_->setup();
this->rst_pin_->setup();
this->dio1_pin_->setup();
if (this->dio1_pin_->is_internal()) {
static_cast<InternalGPIOPin *>(this->dio1_pin_)
->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
}
// start spi
this->spi_setup();
@@ -354,9 +348,6 @@ void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo
}
void SX126x::loop() {
if (this->dio1_pin_->is_internal()) {
this->disable_loop();
}
if (!this->dio1_pin_->digital_read()) {
return;
}
-2
View File
@@ -3,7 +3,6 @@
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "sx126x_reg.h"
#include <utility>
#include <vector>
@@ -101,7 +100,6 @@ class SX126x : public Component,
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
protected:
static void IRAM_ATTR gpio_intr(SX126x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_packet_params_(uint8_t payload_length);
+2 -2
View File
@@ -197,11 +197,11 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.int_range(min=0, max=100000)
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_MODULATION): cv.enum(MOD),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
+5 -4
View File
@@ -53,8 +53,6 @@ void SX127x::write_fifo_(const std::vector<uint8_t> &packet) {
this->disable();
}
void IRAM_ATTR SX127x::gpio_intr(SX127x *arg) { arg->enable_loop_soon_any_context(); }
void SX127x::setup() {
// setup reset
this->rst_pin_->setup();
@@ -62,7 +60,6 @@ void SX127x::setup() {
// setup dio0
if (this->dio0_pin_) {
this->dio0_pin_->setup();
this->dio0_pin_->attach_interrupt(&SX127x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
}
// start spi
@@ -316,7 +313,6 @@ void SX127x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo
}
void SX127x::loop() {
this->disable_loop();
if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) {
return;
}
@@ -390,6 +386,11 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) {
return;
}
}
if (mode == MODE_RX && (modulation == MOD_LORA || this->packet_mode_)) {
this->enable_loop();
} else {
this->disable_loop();
}
}
void SX127x::set_mode_rx() {
-2
View File
@@ -4,7 +4,6 @@
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include <vector>
namespace esphome {
@@ -87,7 +86,6 @@ class SX127x : public Component,
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
protected:
static void IRAM_ATTR gpio_intr(SX127x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_mode_(uint8_t modulation, uint8_t mode);
+1 -1
View File
@@ -315,7 +315,7 @@ void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration
my_integration_time_regval = integration_time;
this->integration_time_auto_ = false;
}
this->integration_time_ = (256.f - (float) my_integration_time_regval) * 2.4f;
this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f;
ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_);
}
void TCS34725Component::set_gain(TCS34725Gain gain) {
@@ -114,25 +114,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
uint8_t *data, size_t len, bool final) {
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
// First byte of a new upload: index==0 with actual data. (web_server_idf
// fires a separate start-marker call with data==nullptr/len==0 before the
// first real chunk; gate on len>0 so we only trigger once per upload.)
if (index == 0 && len > 0) {
// If a previous upload was interrupted (e.g. client closed the tab, TCP
// reset) the backend from that session may still be open. Tear it down
// so flash state doesn't get concatenated with the new image (which can
// produce a technically-valid-sized but corrupted firmware that bricks
// the device once it reboots).
if (this->ota_backend_) {
ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session");
this->ota_backend_->abort();
#ifdef USE_OTA_STATE_LISTENER
// Notify listeners that the previous session was aborted before the new one starts.
this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
#endif
this->ota_backend_.reset();
}
if (index == 0 && !this->ota_backend_) {
// Initialize OTA on first call
this->ota_init_(filename.c_str());
+1 -20
View File
@@ -943,26 +943,7 @@ def time_period_in_minutes_(value):
def update_interval(value):
if value == "never":
return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN)
result = positive_time_period_milliseconds(value)
# 0ms was historically (mis)used as a pseudo-loop() mechanism for
# PollingComponents. Under the hood it calls set_interval(0), which
# causes Scheduler::call() to spin (WDT reset in the field). Coerce
# to 1ms so existing configs keep working at ~1kHz instead of
# spinning. Don't hard-fail so configs don't break on upgrade;
# authors should migrate to HighFrequencyLoopRequester (C++) for
# true run-every-loop behaviour.
if result.total_milliseconds == 0:
_LOGGER.warning(
"update_interval of 0ms is not supported - coercing to 1ms. "
"A literal 0ms schedule would spin the main loop (the scheduled "
"item would always be due, so the scheduler would never yield "
"back) and trigger a watchdog reset. Set update_interval to a "
"non-zero value such as 1ms or higher. (Custom C++ components "
"that need true run-every-loop behaviour should override loop() "
"with HighFrequencyLoopRequester instead.)"
)
return TimePeriodMilliseconds(milliseconds=1)
return result
return positive_time_period_milliseconds(value)
time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict)
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.4.1"
__version__ = "2026.5.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
+16 -48
View File
@@ -85,12 +85,8 @@ void Application::setup() {
if (component->can_proceed())
continue;
// Force the status LED to blink WARNING while we wait for a slow
// component to come up. Cleared after setup() finishes if no real
// component has warning set.
this->app_state_ |= STATUS_LED_WARNING;
do {
uint8_t new_app_state = STATUS_LED_WARNING;
uint32_t now = millis();
// Process pending loop enables to handle GPIO interrupts during setup
@@ -100,26 +96,17 @@ void Application::setup() {
// Update loop_component_start_time_ right before calling each component
this->loop_component_start_time_ = millis();
this->components_[j]->call();
new_app_state |= this->components_[j]->get_component_state();
this->app_state_ |= new_app_state;
this->feed_wdt();
}
this->after_loop_tasks_();
this->app_state_ = new_app_state;
yield();
} while (!component->can_proceed() && !component->is_failed());
}
// Setup is complete. Reconcile STATUS_LED_WARNING: the slow-setup path
// above may have forced it on, and any status_clear_warning() calls
// from components during setup were intentional no-ops (gated by
// APP_STATE_SETUP_COMPLETE). Walk components once here to pick up the
// real state. STATUS_LED_ERROR is never artificially forced, so its
// clear path always works and needs no reconciliation. Finally, set
// APP_STATE_SETUP_COMPLETE so subsequent warning clears go through
// the normal walk-and-clear path.
if (!this->any_component_has_status_flag_(STATUS_LED_WARNING))
this->app_state_ &= ~STATUS_LED_WARNING;
this->app_state_ |= APP_STATE_SETUP_COMPLETE;
ESP_LOGI(TAG, "setup() finished successfully!");
#ifdef USE_SETUP_PRIORITY_OVERRIDE
@@ -209,40 +196,21 @@ void Application::process_dump_config_() {
this->dump_config_at_++;
}
void Application::feed_wdt() {
// Cold entry: callers without a millis() timestamp in hand. Fetches the
// time and takes the same rate-limit path as feed_wdt_with_time().
uint32_t now = millis();
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
this->feed_wdt_slow_(now);
}
}
void HOT Application::feed_wdt_slow_(uint32_t time) {
// Callers (both feed_wdt() and feed_wdt_with_time()) have already
// confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded.
arch_feed_wdt();
this->last_wdt_feed_ = time;
void HOT Application::feed_wdt(uint32_t time) {
static uint32_t last_feed = 0;
// Use provided time if available, otherwise get current time
uint32_t now = time ? time : millis();
// Compare in milliseconds (3ms threshold)
if (now - last_feed > 3) {
arch_feed_wdt();
last_feed = now;
#ifdef USE_STATUS_LED
if (status_led::global_status_led != nullptr) {
status_led::global_status_led->call();
}
if (status_led::global_status_led != nullptr) {
status_led::global_status_led->call();
}
#endif
}
bool Application::any_component_has_status_flag_(uint8_t flag) const {
// Walk all components (not just looping ones) so non-looping components'
// status bits are respected. Only called from the slow-path clear helpers
// (status_clear_warning_slow_path_ / status_clear_error_slow_path_) on an
// actual set→clear transition, so walking O(N) here is paid once per
// transition — not once per loop iteration.
for (auto *component : this->components_) {
if ((component->get_component_state() & flag) != 0)
return true;
}
return false;
}
void Application::reboot() {
ESP_LOGI(TAG, "Forcing a reboot");
for (auto &component : std::ranges::reverse_view(this->components_)) {
@@ -331,7 +299,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
while (pending_count > 0 && (now - start_time) < timeout_ms) {
// Feed watchdog during teardown to prevent triggering
this->feed_wdt_with_time(now);
this->feed_wdt(now);
// Process components and compact the array, keeping only those still pending
size_t still_pending = 0;
+17 -82
View File
@@ -385,24 +385,7 @@ class Application {
void schedule_dump_config() { this->dump_config_at_ = 0; }
/// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the
/// rate of HAL pokes low while still being small enough that any plausible
/// watchdog timeout (seconds) has orders of magnitude of safety margin.
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3;
/// Feed the task watchdog. Cold entry — callers without a millis()
/// timestamp in hand. Out of line to keep call sites tiny.
void feed_wdt();
/// Feed the task watchdog, hot entry. Callers that already have a
/// millis() timestamp pay only a load + sub + branch on the common
/// (no-op) path. The actual arch feed + status LED update live in
/// feed_wdt_slow_.
void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) {
if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] {
this->feed_wdt_slow_(time);
}
}
void feed_wdt(uint32_t time = 0);
void reboot();
@@ -418,18 +401,7 @@ class Application {
*/
void teardown_components(uint32_t timeout_ms);
/// Return the public app state status bits (STATUS_LED_* only).
/// Internal bookkeeping bits like APP_STATE_SETUP_COMPLETE are masked
/// out so external readers (status_led components, etc.) never see them.
uint8_t get_app_state() const { return this->app_state_ & ~APP_STATE_SETUP_COMPLETE; }
/// True once Application::setup() has finished walking all components
/// and finalized the initial status flags. Before this point, the
/// slow-setup busy-wait may be forcing STATUS_LED_WARNING on, and
/// status_clear_* intentionally skips its walk-and-clear step so the
/// forced bit doesn't get wiped. Stored as a free bit on app_state_
/// (bit 6) to avoid costing additional RAM.
bool is_setup_complete() const { return (this->app_state_ & APP_STATE_SETUP_COMPLETE) != 0; }
uint8_t get_app_state() const { return this->app_state_; }
// Helper macro for entity getter method declarations
#ifdef USE_DEVICES
@@ -605,12 +577,6 @@ class Application {
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
#endif
/// Walk all registered components looking for any whose component_state_
/// has the given flag set. Used by Component::status_clear_*_slow_path_()
/// (which is a friend) to decide whether to clear the corresponding bit on
/// this->app_state_ (the app-wide "any component has this status" indicator).
bool any_component_has_status_flag_(uint8_t flag) const;
/// Register a component, detecting loop() override at compile time.
/// Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance.
template<typename T> void register_component_(T *comp) {
@@ -641,7 +607,7 @@ class Application {
void enable_component_loop_(Component *component);
void enable_pending_loops_();
void activate_looping_component_(uint16_t index);
inline uint32_t ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time);
inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time);
inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; }
/// Process dump_config output one component per loop iteration.
@@ -649,10 +615,7 @@ class Application {
/// Caller must ensure dump_config_at_ < components_.size().
void __attribute__((noinline)) process_dump_config_();
/// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates
/// last_wdt_feed_, and re-dispatches the status LED. Out of line so the
/// inline wrapper stays tiny.
void feed_wdt_slow_(uint32_t time);
void feed_wdt_arch_();
/// Perform a delay while also monitoring socket file descriptors for readiness
#ifdef USE_HOST
@@ -706,7 +669,6 @@ class Application {
// 4-byte members
uint32_t last_loop_{0};
uint32_t loop_component_start_time_{0};
uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path
#ifdef USE_HOST
int max_fd_{-1}; // Highest file descriptor number for select()
@@ -845,15 +807,17 @@ inline void Application::drain_wake_notifications_() {
}
#endif // USE_HOST
inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) {
inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) {
#ifdef USE_HOST
// Drain wake notifications first to clear socket for next wake
this->drain_wake_notifications_();
#endif
// Scheduler::call feeds the WDT per item and returns the timestamp of the
// last fired item, or the input unchanged when nothing ran.
uint32_t last_op_end_time = this->scheduler.call(loop_start_time);
// Process scheduled tasks
this->scheduler.call(loop_start_time);
// Feed the watchdog timer
this->feed_wdt(loop_start_time);
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
@@ -871,35 +835,15 @@ inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t l
// Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
return last_op_end_time;
}
inline void ESPHOME_ALWAYS_INLINE Application::loop() {
#ifdef USE_RUNTIME_STATS
// Capture the start of the active (non-sleeping) portion of this iteration.
// Used to derive main-loop overhead = active time Σ(component time)
// before/tail splits recorded below.
uint32_t loop_active_start_us = micros();
// Snapshot the cumulative component-recorded time so we can subtract the
// slice that the scheduler spends inside its own WarnIfComponentBlockingGuard
// (scheduler.cpp) — that time is already counted in per-component stats,
// so charging it again to "before" would double-count.
uint64_t loop_recorded_snap = ComponentRuntimeStats::global_recorded_us;
#endif
uint8_t new_app_state = 0;
// Get the initial loop time at the start
uint32_t last_op_end_time = millis();
// Returned timestamp keeps us monotonic with last_wdt_feed_ (advanced by
// the scheduler's per-item feeds) without an extra millis() call.
last_op_end_time = this->before_loop_tasks_(last_op_end_time);
// Guarantee a WDT touch every tick — covers configs with no looping
// components and no scheduler work, where the per-item / per-component
// feeds never fire. Rate-limited inline fast path, ~free when unneeded.
this->feed_wdt_with_time(last_op_end_time);
#ifdef USE_RUNTIME_STATS
uint32_t loop_before_end_us = micros();
uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap;
#endif
this->before_loop_tasks_(last_op_end_time);
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
this->current_loop_index_++) {
@@ -915,27 +859,18 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
// Use the finish method to get the current time as the end time
last_op_end_time = guard.finish();
}
this->feed_wdt_with_time(last_op_end_time);
new_app_state |= component->get_component_state();
this->app_state_ |= new_app_state;
this->feed_wdt(last_op_end_time);
}
#ifdef USE_RUNTIME_STATS
uint32_t loop_tail_start_us = micros();
#endif
this->after_loop_tasks_();
this->app_state_ = new_app_state;
#ifdef USE_RUNTIME_STATS
// Process any pending runtime stats printing after all components have run
// This ensures stats printing doesn't affect component timing measurements
if (global_runtime_stats != nullptr) {
uint32_t loop_now_us = micros();
// Subtract scheduled-component time from the "before" bucket so it is
// not double-counted (it is already attributed to per-component stats).
uint32_t loop_before_wall_us = loop_before_end_us - loop_active_start_us;
uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us
? loop_before_wall_us - static_cast<uint32_t>(loop_before_scheduled_us)
: 0;
global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us,
loop_now_us - loop_tail_start_us);
global_runtime_stats->process_pending_stats(last_op_end_time);
}
#endif
-12
View File
@@ -62,18 +62,6 @@ template<typename T, typename... X> class TemplatableFn {
!std::convertible_to<std::invoke_result_t<F, X...>, T> ||
!std::default_initializable<F>) = delete;
// Reject raw (non-callable) values with a helpful diagnostic pointing at the Python-side fix.
// TemplatableFn stores only a function pointer (4 bytes), so constants must be wrapped in a
// stateless lambda by codegen. External components hitting this error should use
// `cg.templatable(value, args, type)` in their Python __init__.py before passing to the setter.
template<typename V> TemplatableFn(V) requires(!std::invocable<V, X...>) && (!std::convertible_to<V, T (*)(X...)>) {
static_assert(sizeof(V) == 0, "Missing cg.templatable(...) in Python codegen for this TEMPLATABLE_VALUE "
"field. The wrapper was always required; it worked by accident because the old "
"TemplatableValue implicitly converted raw constants. TemplatableFn cannot. See "
"https://developers.esphome.io/blog/2026/04/09/"
"templatablefn-4-byte-templatable-storage-for-trivially-copyable-types/");
}
bool has_value() const { return this->f_ != nullptr; }
T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; }
+1 -3
View File
@@ -205,9 +205,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
} else {
// For delays with arguments, capture by value to preserve argument values
// Arguments must be copied because original references may be invalid after delay
// `mutable` is required so captured copies of non-const reference args (e.g. std::string&)
// are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&`
auto f = [this, x...]() mutable { this->play_next_(x...); };
auto f = [this, x...]() { this->play_next_(x...); };
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL,
nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION),
this->delay_.value(x...), std::move(f),
-17
View File
@@ -411,23 +411,10 @@ void Component::status_set_error(const LogString *message) {
}
void Component::status_clear_warning_slow_path_() {
this->component_state_ &= ~STATUS_LED_WARNING;
// Clear the app-wide STATUS_LED_WARNING bit only if setup has finished
// AND no other component still has it set. During setup the forced
// STATUS_LED_WARNING (from the slow-setup busy-wait) must not be wiped
// by a transient component clear — Application::setup() reconciles
// the warning bit once at the end before setting APP_STATE_SETUP_COMPLETE.
// The set path is unchanged (set_status_flag_ still writes directly).
if (App.is_setup_complete() && !App.any_component_has_status_flag_(STATUS_LED_WARNING))
App.app_state_ &= ~STATUS_LED_WARNING;
ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str()));
}
void Component::status_clear_error_slow_path_() {
this->component_state_ &= ~STATUS_LED_ERROR;
// STATUS_LED_ERROR is never artificially forced — it only ever lands
// in app_state_ via a real set_status_flag_ call. So the walk-and-clear
// path is always safe, including during setup.
if (!App.any_component_has_status_flag_(STATUS_LED_ERROR))
App.app_state_ &= ~STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str()));
}
void Component::status_momentary_warning(const char *name, uint32_t length) {
@@ -506,10 +493,6 @@ void PollingComponent::stop_poller() {
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
#ifdef USE_RUNTIME_STATS
uint64_t ComponentRuntimeStats::global_recorded_us = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif
void __attribute__((noinline, cold))
WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) {
bool should_warn;
+1 -14
View File
@@ -89,11 +89,6 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08;
inline constexpr uint8_t STATUS_LED_ERROR = 0x10;
// Component loop override flag uses bit 5 (set at registration time)
inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20;
// Bit 6 on Application::app_state_ (ONLY) — set at the end of
// Application::setup(). Component::status_clear_*_slow_path_() uses this to
// decide whether to propagate clears to App.app_state_. Never set on a
// Component's component_state_.
inline constexpr uint8_t APP_STATE_SETUP_COMPLETE = 0x40;
// Remove before 2026.8.0
enum class RetryResult { DONE, RETRY };
@@ -116,13 +111,6 @@ struct ComponentRuntimeStats {
uint64_t total_time_us{0};
uint32_t total_max_time_us{0};
// Cumulative sum of every record_time() duration since boot, across all
// components. Used by Application::loop() to snapshot time spent inside
// WarnIfComponentBlockingGuard (including guards constructed by the
// scheduler at scheduler.cpp) so main-loop overhead accounting can
// subtract scheduled-callback time from the before_loop_tasks_ wall time.
static uint64_t global_recorded_us; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void record_time(uint32_t duration_us) {
this->period_count++;
this->period_time_us += duration_us;
@@ -132,7 +120,6 @@ struct ComponentRuntimeStats {
this->total_time_us += duration_us;
if (duration_us > this->total_max_time_us)
this->total_max_time_us = duration_us;
global_recorded_us += duration_us;
}
void reset_period() {
this->period_count = 0;
@@ -601,7 +588,7 @@ class Component {
*/
class PollingComponent : public Component {
public:
PollingComponent() : PollingComponent(1) {}
PollingComponent() : PollingComponent(0) {}
/** Initialize this polling component with the given update interval in ms.
*
+2 -24
View File
@@ -144,19 +144,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
return;
}
// An interval of 0 means "fire every tick forever," which is misuse: the
// item would always be due, causing Scheduler::call() to spin and starve
// the main loop (WDT reset in the field). Coerce to 1ms so existing code
// using update_interval=0ms as a pseudo-loop() continues to work at ~1kHz,
// and warn so authors can migrate to HighFrequencyLoopRequester which is
// the intended mechanism for running fast in the main loop. Zero-delay
// timeouts (defer) remain legitimate one-shots and are not affected.
if (type == SchedulerItem::INTERVAL && delay == 0) [[unlikely]] {
ESP_LOGE(TAG, "[%s] set_interval(0) would spin main loop - coercing to 1ms (use HighFrequencyLoopRequester)",
component ? LOG_STR_ARG(component->get_component_log_str()) : LOG_STR_LITERAL("?"));
delay = 1;
}
// Take lock early to protect scheduler_item_pool_ access and retry-cancelled check
LockGuard guard{this->lock_};
@@ -533,7 +520,7 @@ void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) {
}
#endif /* not ESPHOME_THREAD_SINGLE */
uint32_t HOT Scheduler::call(uint32_t now) {
void HOT Scheduler::call(uint32_t now) {
#ifndef ESPHOME_THREAD_SINGLE
this->process_defer_queue_(now);
#endif /* not ESPHOME_THREAD_SINGLE */
@@ -703,9 +690,6 @@ uint32_t HOT Scheduler::call(uint32_t now) {
this->debug_verify_no_leak_();
}
#endif
// execute_item_() advances `now` as items fire; return it so the caller
// stays monotonic with last_wdt_feed_.
return now;
}
void HOT Scheduler::process_to_add_slow_path_() {
LockGuard guard{this->lock_};
@@ -755,13 +739,7 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
App.set_current_component(item->component);
WarnIfComponentBlockingGuard guard{item->component, now};
item->callback();
uint32_t end = guard.finish();
// Feed the watchdog after each scheduled item (both main heap and defer
// queue paths go through here). A run of back-to-back callbacks cannot
// starve the wdt. The inline fast path is a load + sub + branch — nearly
// free when the 3 ms rate limit hasn't elapsed.
App.feed_wdt_with_time(end);
return end;
return guard.finish();
}
// Common implementation for cancel operations - handles locking
+1 -2
View File
@@ -129,8 +129,7 @@ class Scheduler {
// Execute all scheduled items that are ready
// @param now Fresh timestamp from millis() - must not be stale/cached
// @return Timestamp of the last item that ran, or `now` unchanged if none ran.
uint32_t call(uint32_t now);
void call(uint32_t now);
// Move items from to_add_ into the main heap.
// IMPORTANT: This method should only be called from the main thread (loop task).
+2 -6
View File
@@ -76,12 +76,8 @@ struct ESPTime {
/// @copydoc strftime(const std::string &format)
std::string strftime(const char *format);
/// Check if this ESPTime is valid (year >= 2019 and the requested fields are in range).
/// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields)
/// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields)
bool is_valid(bool check_day_of_week = true, bool check_day_of_year = true) const {
return this->year >= 2019 && this->fields_in_range(check_day_of_week, check_day_of_year);
}
/// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019)
bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); }
/// Check if time fields are in range.
/// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields)
+1 -2
View File
@@ -113,8 +113,7 @@ def _generate_source_table_code(
entries = ", ".join(var_names)
lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};")
lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{")
cond = "index == 0" if count >= 255 else f"index == 0 || index > {count}"
lines.append(f' if ({cond}) return LOG_STR("<unknown>");')
lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");')
lines.append(" return reinterpret_cast<const LogString *>(")
lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));")
lines.append("}")
+1 -1
View File
@@ -14,7 +14,7 @@ dependencies:
espressif/esp32-camera:
version: 2.1.6
espressif/mdns:
version: 1.11.0
version: 1.10.0
espressif/esp_wifi_remote:
version: 1.4.0
rules:
+128 -4
View File
@@ -5,15 +5,104 @@ import os
from pathlib import Path
import re
import subprocess
import sys
import time
from typing import Any
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
from esphome.core import CORE, EsphomeError
from esphome.util import run_external_process
from esphome.util import run_external_command, run_external_process
_LOGGER = logging.getLogger(__name__)
def patch_structhash():
# Patch platformio's structhash to not recompile the entire project when files are
# removed/added. This might have unintended consequences, but this improves compile
# times greatly when adding/removing components and a simple clean build solves
# all issues
from platformio.run import cli, helpers
def patched_clean_build_dir(build_dir, *args):
from platformio import fs
from platformio.project.helpers import get_project_dir
platformio_ini = Path(get_project_dir()) / "platformio.ini"
build_dir = Path(build_dir)
# if project's config is modified
if (
build_dir.is_dir()
and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime
):
fs.rmtree(build_dir)
if not build_dir.is_dir():
build_dir.mkdir(parents=True)
helpers.clean_build_dir = patched_clean_build_dir
cli.clean_build_dir = patched_clean_build_dir
def patch_file_downloader():
"""Patch PlatformIO's FileDownloader to retry on PackageException errors.
PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry
for 502/503 errors. We add retries with exponential backoff and close the
session between attempts to force a fresh TCP connection, which may route
to a different CDN edge node.
"""
from platformio.package.download import FileDownloader
from platformio.package.exception import PackageException
if getattr(FileDownloader.__init__, "_esphome_patched", False):
return
original_init = FileDownloader.__init__
def patched_init(self, *args: Any, **kwargs: Any) -> None:
max_retries = 5
for attempt in range(max_retries):
try:
original_init(self, *args, **kwargs)
return
except PackageException as e:
if attempt < max_retries - 1:
# Exponential backoff: 2, 4, 8, 16 seconds
delay = 2 ** (attempt + 1)
_LOGGER.warning(
"Package download failed: %s. "
"Retrying in %d seconds... (attempt %d/%d)",
str(e),
delay,
attempt + 1,
max_retries,
)
# Close the response and session to free resources
# and force a new TCP connection on retry, which may
# route to a different CDN edge node
# pylint: disable=protected-access,broad-except
try:
if (
hasattr(self, "_http_response")
and self._http_response is not None
):
self._http_response.close()
if hasattr(self, "_http_session"):
self._http_session.close()
except Exception:
pass
# pylint: enable=protected-access,broad-except
time.sleep(delay)
else:
# Final attempt - re-raise
raise
patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access
FileDownloader.__init__ = patched_init
IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})"
FILTER_PLATFORMIO_LINES = [
r"Verbose mode can be enabled via `-v, --verbose` option.*",
@@ -53,6 +142,20 @@ FILTER_PLATFORMIO_LINES = [
]
class PlatformioLogFilter(logging.Filter):
"""Filter to suppress noisy platformio log messages."""
_PATTERN = re.compile(
r"|".join(r"(?:" + pattern + r")" for pattern in FILTER_PLATFORMIO_LINES)
)
def filter(self, record: logging.LogRecord) -> bool:
# Only filter messages from platformio-related loggers
if "platformio" not in record.name.lower():
return True
return self._PATTERN.match(record.getMessage()) is None
def run_platformio_cli(*args, **kwargs) -> str | int:
os.environ["PLATFORMIO_FORCE_COLOR"] = "true"
os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute())
@@ -63,9 +166,30 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
# Increase uv retry count to handle transient network errors (default is 3)
os.environ.setdefault("UV_HTTP_RETRIES", "10")
cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args)
cmd = ["platformio"] + list(args)
return run_external_process(*cmd, **kwargs)
if not CORE.verbose:
kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES
if os.environ.get("ESPHOME_USE_SUBPROCESS") is not None:
return run_external_process(*cmd, **kwargs)
import platformio.__main__
patch_structhash()
patch_file_downloader()
# Add log filter to suppress noisy platformio messages
log_filter = PlatformioLogFilter() if not CORE.verbose else None
if log_filter:
for handler in logging.getLogger().handlers:
handler.addFilter(log_filter)
try:
return run_external_command(platformio.__main__.main, *cmd, **kwargs)
finally:
if log_filter:
for handler in logging.getLogger().handlers:
handler.removeFilter(log_filter)
def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int:
-144
View File
@@ -1,144 +0,0 @@
"""Subprocess entry point that applies ESPHome's PlatformIO patches.
Invoked via ``python -m esphome.platformio_runner`` instead of
``python -m platformio`` so that the patches (incremental rebuild
preservation, download retries) apply inside the subprocess. Running
PlatformIO in a subprocess keeps its ``sys.path`` mutations and other
global state from leaking into the ESPHome process.
"""
from __future__ import annotations
import logging
from pathlib import Path
import sys
import time
from typing import Any
_LOGGER = logging.getLogger(__name__)
def patch_structhash() -> None:
"""Avoid full rebuilds when files are added or removed.
PlatformIO clears the build dir whenever its structure hash changes.
We replace that with an mtime check against ``platformio.ini`` so
incremental builds are preserved unless the project config changed.
"""
from platformio.run import cli, helpers
def patched_clean_build_dir(build_dir, *_args):
from platformio import fs
from platformio.project.helpers import get_project_dir
platformio_ini = Path(get_project_dir()) / "platformio.ini"
build_dir = Path(build_dir)
if (
build_dir.is_dir()
and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime
):
fs.rmtree(build_dir)
if not build_dir.is_dir():
build_dir.mkdir(parents=True)
helpers.clean_build_dir = patched_clean_build_dir
cli.clean_build_dir = patched_clean_build_dir
def patch_file_downloader() -> None:
"""Retry PlatformIO package downloads with exponential backoff.
PlatformIO's ``FileDownloader`` uses an ``HTTPSession`` without built-in
retry for 502/503 errors. We wrap ``__init__`` to retry on
``PackageException`` and close the session between attempts so a new
TCP connection can route to a different CDN edge node.
"""
from platformio.package.download import FileDownloader
from platformio.package.exception import PackageException
if getattr(FileDownloader.__init__, "_esphome_patched", False):
return
original_init = FileDownloader.__init__
def patched_init(self, *args: Any, **kwargs: Any) -> None:
max_retries = 5
for attempt in range(max_retries):
try:
original_init(self, *args, **kwargs)
return
except PackageException as e:
if attempt < max_retries - 1:
delay = 2 ** (attempt + 1)
_LOGGER.warning(
"Package download failed: %s. "
"Retrying in %d seconds... (attempt %d/%d)",
str(e),
delay,
attempt + 1,
max_retries,
)
# pylint: disable=protected-access,broad-except
try:
if (
hasattr(self, "_http_response")
and self._http_response is not None
):
self._http_response.close()
if hasattr(self, "_http_session"):
self._http_session.close()
except Exception:
pass
# pylint: enable=protected-access,broad-except
time.sleep(delay)
else:
raise
patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access
FileDownloader.__init__ = patched_init
def main() -> int:
patch_structhash()
patch_file_downloader()
# Wrap stdout/stderr with RedirectText before PlatformIO runs:
#
# 1. RedirectText.isatty() unconditionally returns True. Click, tqdm, and
# PlatformIO's own progress-bar code check ``stream.isatty()`` to
# decide whether to emit TTY-format output (``\r`` cursor moves, ANSI
# colors, fancy progress bars). With the wrapper in place they always
# emit TTY format, even when our real stdout is a pipe to the parent
# process. Downstream consumers (local terminals and the Home
# Assistant dashboard log viewer) render the TTY control sequences
# correctly, so the user sees real progress bars.
#
# 2. FILTER_PLATFORMIO_LINES is applied inside RedirectText.write() in
# this subprocess, so noisy PlatformIO output is dropped before it
# ever leaves the runner. This replaces the parent-side filtering
# that was lost when we switched from in-process to subprocess — the
# parent's ``subprocess.run`` uses ``.fileno()`` on RedirectText and
# bypasses its ``write()`` path entirely.
#
# Filtering is disabled when the user passed -v / --verbose to
# ``esphome compile``, preserving the previous in-process behavior where
# verbose mode let all PlatformIO output through unfiltered.
from esphome.platformio_api import FILTER_PLATFORMIO_LINES
from esphome.util import RedirectText
is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[1:])
filter_lines = None if is_verbose else FILTER_PLATFORMIO_LINES
sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines)
import platformio.__main__
return platformio.__main__.main() or 0
if __name__ == "__main__":
sys.exit(main())
+5 -5
View File
@@ -133,10 +133,10 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
; This are common settings for the ESP32 (all variants) using Arduino.
[common:esp32-arduino]
extends = common:arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz
framework = arduino, espidf ; Arduino as an ESP-IDF component
lib_deps =
@@ -169,9 +169,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
; This are common settings for the ESP32 (all variants) using IDF.
[common:esp32-idf]
extends = common:idf
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
platform_packages =
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz
framework = espidf
lib_deps =
+1
View File
@@ -20,6 +20,7 @@ classifiers = [
"Topic :: Home Automation",
]
# Python 3.14 is not supported on Windows, see https://github.com/zephyrproject-rtos/windows-curses/issues/76
requires-python = ">=3.11.0,<3.15"
dynamic = ["dependencies", "optional-dependencies", "version"]
+1 -1
View File
@@ -12,7 +12,7 @@ platformio==6.1.19
esptool==5.2.0
click==8.3.2
esphome-dashboard==20260408.1
aioesphomeapi==44.16.1
aioesphomeapi==44.13.1
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import
+1 -1
View File
@@ -1,6 +1,6 @@
pylint==4.0.5
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.15.9 # also change in .pre-commit-config.yaml when updating
ruff==0.15.10 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit
+4 -16
View File
@@ -1028,8 +1028,7 @@ class BytesType(TypeInfo):
)
def get_size_calculation(self, name: str, force: bool = False) -> str:
calc_fn = "calc_length_force" if force else "calc_length"
return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
@@ -1110,8 +1109,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
)
def get_size_calculation(self, name: str, force: bool = False) -> str:
calc_fn = "calc_length_force" if force else "calc_length"
return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len);"
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);"
class PointerToStringBufferType(PointerToBufferTypeBase):
@@ -2681,16 +2679,6 @@ def build_message_type(
and get_opt(desc, inline_opt, False)
)
# Check if this message wants speed-optimized encode/calculate_size.
# When set, __attribute__((optimize("O2"))) is added to the definitions
# so GCC inlines the small ProtoEncode helpers even under -Os.
is_speed_optimized = get_opt(desc, pb.speed_optimized, False)
speed_attr = (
'__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)\n'
if is_speed_optimized
else ""
)
# Only generate encode method if this message needs encoding and has fields
if needs_encode and encode and not is_inline_only:
# Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls
@@ -2700,7 +2688,7 @@ def build_message_type(
)
for line in encode
]
o = f"{speed_attr}uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
o += indent("\n".join(encode_debug)) + "\n"
o += " return pos;\n"
@@ -2714,7 +2702,7 @@ def build_message_type(
# Add calculate_size method only if this message needs encoding and has fields
if needs_encode and size_calc and not is_inline_only:
o = f"{speed_attr}uint32_t {desc.name}::calculate_size() const {{\n"
o = f"uint32_t {desc.name}::calculate_size() const {{\n"
o += " uint32_t size = 0;\n"
o += indent("\n".join(size_calc)) + "\n"
o += " return size;\n"
+270 -1
View File
@@ -10,7 +10,6 @@ namespace esphome::benchmarks {
static constexpr int kInnerIterations = 2000;
// --- random_float() ---
// Ported from ol.yaml:148 "Random Float Benchmark"
static void RandomFloat(benchmark::State &state) {
for (auto _ : state) {
@@ -38,4 +37,274 @@ static void RandomUint32(benchmark::State &state) {
}
BENCHMARK(RandomUint32);
// --- format_hex_to() - 6 bytes (MAC address sized) ---
static void FormatHexTo_6Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
char buffer[13]; // 6 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 6);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_6Bytes);
// --- format_hex_to() - 16 bytes (UUID sized) ---
static void FormatHexTo_16Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89,
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10};
char buffer[33]; // 16 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 16);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_16Bytes);
// --- format_hex_to() - 100 bytes (large payload) ---
static void FormatHexTo_100Bytes(benchmark::State &state) {
uint8_t data[100];
for (int i = 0; i < 100; i++) {
data[i] = static_cast<uint8_t>(i);
}
char buffer[201]; // 100 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 100);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_100Bytes);
// --- format_hex_pretty_to() - 6 bytes with ':' separator ---
static void FormatHexPrettyTo_6Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
char buffer[18]; // 6 * 3
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_pretty_to(buffer, data, 6);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexPrettyTo_6Bytes);
// --- format_mac_addr_upper() ---
static void FormatMacAddrUpper(benchmark::State &state) {
const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_mac_addr_upper(mac, buffer);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatMacAddrUpper);
// --- fnv1_hash() - short string ---
static void Fnv1Hash_Short(benchmark::State &state) {
const char *str = "sensor.temperature";
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash(str);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1Hash_Short);
// --- fnv1_hash() - long string ---
static void Fnv1Hash_Long(benchmark::State &state) {
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash(str);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1Hash_Long);
// --- fnv1a_hash() - short string ---
// Use DoNotOptimize on the input pointer to prevent constexpr evaluation
static void Fnv1aHash_Short(benchmark::State &state) {
const char *str = "sensor.temperature";
benchmark::DoNotOptimize(str);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1a_hash(str);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1aHash_Short);
// --- fnv1a_hash() - long string ---
static void Fnv1aHash_Long(benchmark::State &state) {
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
benchmark::DoNotOptimize(str);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1a_hash(str);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1aHash_Long);
// --- fnv1_hash_object_id() - typical entity name ---
static void Fnv1HashObjectId(benchmark::State &state) {
char name[] = "Living Room Temperature Sensor";
size_t len = sizeof(name) - 1;
benchmark::DoNotOptimize(name);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash_object_id(name, len);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1HashObjectId);
// --- parse_hex() - 6 bytes from string ---
static void ParseHex_6Bytes(benchmark::State &state) {
const char *hex_str = "ABCDEF012345";
uint8_t data[6];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
parse_hex(hex_str, data, 6);
}
benchmark::DoNotOptimize(data);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ParseHex_6Bytes);
// --- parse_hex() - 16 bytes from string ---
static void ParseHex_16Bytes(benchmark::State &state) {
const char *hex_str = "ABCDEF0123456789FEDCBA9876543210";
uint8_t data[16];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
parse_hex(hex_str, data, 16);
}
benchmark::DoNotOptimize(data);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ParseHex_16Bytes);
// --- crc8() - 8 bytes ---
static void CRC8_8Bytes(benchmark::State &state) {
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
for (auto _ : state) {
uint8_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= crc8(data, 8);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CRC8_8Bytes);
// --- crc16() - 8 bytes ---
static void CRC16_8Bytes(benchmark::State &state) {
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
for (auto _ : state) {
uint16_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= crc16(data, 8);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CRC16_8Bytes);
// --- value_accuracy_to_buf() - typical sensor value ---
static void ValueAccuracyToBuf(benchmark::State &state) {
char raw_buf[VALUE_ACCURACY_MAX_LEN] = {};
std::span<char, VALUE_ACCURACY_MAX_LEN> buf(raw_buf);
float value = 23.456f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
value_accuracy_to_buf(buf, value, 2);
}
benchmark::DoNotOptimize(raw_buf);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ValueAccuracyToBuf);
// --- int8_to_str() ---
static void Int8ToStr(benchmark::State &state) {
char buffer[5] = {};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
int8_to_str(buffer, static_cast<int8_t>(i & 0xFF));
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Int8ToStr);
// --- base64_decode() - into pre-allocated buffer ---
static void Base64Decode_32Bytes(benchmark::State &state) {
// 32 bytes encoded = 44 base64 chars
const uint8_t encoded[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGx0eHw==";
size_t encoded_len = 44;
uint8_t output[32];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
base64_decode(encoded, encoded_len, output, sizeof(output));
}
benchmark::DoNotOptimize(output);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Base64Decode_32Bytes);
} // namespace esphome::benchmarks
+3 -76
View File
@@ -8,16 +8,10 @@ from typing import Any
import pytest
from esphome.components.esp32 import VARIANT_ESP32, VARIANTS
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT
from esphome.components.esp32.gpio import validate_gpio_pin
from esphome.components.esp32 import VARIANTS
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
import esphome.config_validation as cv
from esphome.const import (
CONF_ESPHOME,
CONF_IGNORE_PIN_VALIDATION_ERROR,
CONF_NUMBER,
PlatformFramework,
)
from esphome.const import CONF_ESPHOME, PlatformFramework
from esphome.core import CORE
from tests.component_tests.types import SetCoreConfigCallable
@@ -155,73 +149,6 @@ def test_execute_from_psram_p4_sdkconfig(
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
def test_ignore_pin_validation_error_on_clean_pin_warns(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A pin that passes validation but sets `ignore_pin_validation_error: true`
should log a warning nudging the user to remove the flag, and not raise."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
with caplog.at_level("WARNING"):
result = validate_gpio_pin(pin)
assert result[CONF_NUMBER] == 4
assert "GPIO4 has no validation errors to ignore" in caplog.text
def test_ignore_pin_validation_error_on_dirty_pin_suppresses(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A pin that fails validation with `ignore_pin_validation_error: true` should
log the suppression warning and not raise (existing behavior)."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
# GPIO6 is a flash pin on ESP32 -> pin_validation raises cv.Invalid
pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
with caplog.at_level("WARNING"):
result = validate_gpio_pin(pin)
assert result[CONF_NUMBER] == 6
assert "Ignoring validation error on pin 6" in caplog.text
def test_dirty_pin_without_ignore_flag_raises(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A pin that fails validation without the ignore flag should still raise."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
with pytest.raises(cv.Invalid, match="flash interface"):
validate_gpio_pin(pin)
def test_clean_pin_without_ignore_flag_does_not_warn(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A clean pin without the ignore flag should pass silently."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
with caplog.at_level("WARNING"):
result = validate_gpio_pin(pin)
assert result[CONF_NUMBER] == 4
assert "has no validation errors to ignore" not in caplog.text
def test_execute_from_psram_disabled_sdkconfig(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
@@ -1,185 +0,0 @@
"""Tests for the _final_validate buffer size calculation in mipi_spi."""
from __future__ import annotations
from typing import Any
import pytest
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
from esphome.const import CONF_BUFFER_SIZE, PlatformFramework
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def _validated(config: ConfigType) -> ConfigType:
"""Run the component config schema followed by the final validation."""
config = CONFIG_SCHEMA(config)
FINAL_VALIDATE_SCHEMA(config)
return config
def _custom_config(
width: int,
height: int,
color_depth: str | int | None = None,
**extra: Any,
) -> ConfigType:
"""Build a minimal valid custom-model config with the given dimensions."""
config: ConfigType = {
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": width, "height": height},
"init_sequence": [[0xA0, 0x01]],
}
if color_depth is not None:
config["color_depth"] = color_depth
config.update(extra)
return config
# The auto buffer-size selection inside _final_validate targets ~20 kB of
# pixel buffer. For a buffer of ``depth_bytes * width * height``, it picks the
# smallest integer ``x`` in range(2, 8) such that
# ``min(20000, buffer // 4) / buffer >= 1 / x`` (falling back to ``x = 8``).
# The test cases below cover the full range of possible outcomes (1/4 .. 1/8).
@pytest.mark.parametrize(
("width", "height", "color_depth", "expected"),
[
# 16-bit color depth -- buffer = 2 * width * height
# 128*160*2 = 40960 B -> fraction = 10240/40960 = 0.25 -> x = 4
pytest.param(128, 160, "16bit", 1.0 / 4, id="16bit_tiny"),
# 200*224*2 = 89600 B -> fraction = 20000/89600 ≈ 0.2232 -> x = 5
pytest.param(200, 224, "16bit", 1.0 / 5, id="16bit_small"),
# 240*224*2 = 107520 B -> fraction ≈ 0.1860 -> x = 6
pytest.param(240, 224, "16bit", 1.0 / 6, id="16bit_medium"),
# 200*320*2 = 128000 B -> fraction = 0.15625 -> x = 7
pytest.param(200, 320, "16bit", 1.0 / 7, id="16bit_large"),
# 240*320*2 = 153600 B -> fraction ≈ 0.1302 -> default x = 8
pytest.param(240, 320, "16bit", 1.0 / 8, id="16bit_xlarge"),
# 320*480*2 = 307200 B -> fraction ≈ 0.0651 -> default x = 8
pytest.param(320, 480, "16bit", 1.0 / 8, id="16bit_huge"),
# 8-bit color depth -- buffer = width * height
# 320*240 = 76800 B -> fraction = 19200/76800 = 0.25 -> x = 4
pytest.param(320, 240, "8bit", 1.0 / 4, id="8bit_tiny"),
# 400*224 = 89600 B -> fraction ≈ 0.2232 -> x = 5
pytest.param(400, 224, "8bit", 1.0 / 5, id="8bit_small"),
# 480*224 = 107520 B -> fraction ≈ 0.1860 -> x = 6
pytest.param(480, 224, "8bit", 1.0 / 6, id="8bit_medium"),
# 400*320 = 128000 B -> fraction = 0.15625 -> x = 7
pytest.param(400, 320, "8bit", 1.0 / 7, id="8bit_large"),
# 480*320 = 153600 B -> fraction ≈ 0.1302 -> default x = 8
pytest.param(480, 320, "8bit", 1.0 / 8, id="8bit_xlarge"),
],
)
def test_buffer_size_auto_selected(
width: int,
height: int,
color_depth: str,
expected: float,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Without PSRAM or an explicit buffer_size, a fraction is chosen from the display size.
Without any drawing method and without LVGL, final validation also auto-enables
``show_test_card``, which in turn makes the component require a buffer and therefore
triggers the buffer-size selection path.
"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = _validated(_custom_config(width, height, color_depth))
# Sanity check: final validation should have enabled the test card for us,
# which is what causes the buffer-size calculation to actually run.
assert config.get(CONF_SHOW_TEST_CARD) is True
assert config[CONF_BUFFER_SIZE] == pytest.approx(expected)
@pytest.mark.parametrize(
"buffer_size",
[0.125, 0.25, 0.5, 1.0],
ids=["one_eighth", "one_quarter", "half", "full"],
)
def test_explicit_buffer_size_is_preserved(
buffer_size: float,
set_core_config: SetCoreConfigCallable,
) -> None:
"""An explicitly configured buffer_size is never overridden by final validation."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = _validated(
_custom_config(240, 320, "16bit", buffer_size=buffer_size),
)
assert config[CONF_BUFFER_SIZE] == pytest.approx(buffer_size)
def test_buffer_size_not_set_when_psram_enabled(
set_core_config: SetCoreConfigCallable,
set_component_config,
) -> None:
"""When PSRAM is enabled the auto buffer-size selection is skipped."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Presence of the psram domain in the full config is what _final_validate checks.
set_component_config("psram", True)
config = _validated(_custom_config(240, 320, "16bit"))
assert CONF_BUFFER_SIZE not in config
def test_buffer_size_not_set_when_buffer_not_required(
set_core_config: SetCoreConfigCallable,
set_component_config,
) -> None:
"""With LVGL present and no drawing methods, no buffer fraction is chosen.
LVGL suppresses the automatic show_test_card injection, which means
``requires_buffer`` is False and the early-return branch fires.
"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("lvgl", [])
config = _validated(_custom_config(240, 320, "16bit"))
assert CONF_BUFFER_SIZE not in config
# And no test card should have been auto-enabled either.
assert not config.get(CONF_SHOW_TEST_CARD)
def test_buffer_size_selected_when_lvgl_with_test_card(
set_core_config: SetCoreConfigCallable,
set_component_config,
) -> None:
"""LVGL present + an explicit drawing method still triggers buffer sizing.
When LVGL is enabled, ``show_test_card`` is not injected automatically,
but users can still request it explicitly -- in that case ``requires_buffer``
is True and the buffer-size heuristic still runs.
"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("lvgl", [])
# 128x160 @ 16bit -> expected 1/4 (see test_buffer_size_auto_selected).
config = _validated(
_custom_config(128, 160, "16bit", show_test_card=True),
)
assert config[CONF_BUFFER_SIZE] == pytest.approx(1.0 / 4)

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