Compare commits

..

44 Commits

Author SHA1 Message Date
J. Nick Koston
4173f4526e DNM: probe integration-test bucketing for cover
Touches cover/cover.h with a comment so determine-jobs.py routes the
cover component's integration tests into the bucketed split job. Used
to validate the matrix expansion in #16152.

Do not merge.
2026-04-29 18:41:22 -05:00
J. Nick Koston
f3b75c0369 [ci] Extract integration test bucketing into a pure function
Pull the run_all glob expansion + bucket computation out of main() into
_compute_integration_test_buckets() returning (run_integration, buckets).
The boundary tests now call this helper directly instead of driving
main() through ~14 patched dependencies, which both shrinks the test
helper and removes duplication with the existing test_main_* fixtures.
2026-04-29 18:41:17 -05:00
J. Nick Koston
37bcd7c59f [ci] Move test helper imports to module level
Hoist `import contextlib` and `import io` from inside
_run_main_for_integration_buckets() up to the top of the file.
2026-04-29 18:38:46 -05:00
J. Nick Koston
8122ae4888 [ci] Address Copilot review on integration-test bucketing
- Emit each bucket's `tests` as a JSON list of file paths instead of a
  space-joined string. The workflow now uses jq to build a bash array,
  removing word-splitting / glob hazards on test paths.
- Guard against an empty integration test list after `run_all` expansion:
  if the glob returns nothing, suppress the run rather than invoking
  pytest with no path argument (which would collect tests outside
  tests/integration/).
- Add boundary-case unit tests for the bucketing decision: empty
  selection, explicit small lists, exactly threshold (single bucket),
  one over threshold (3 buckets), and run_all-with-empty-glob (no run).
2026-04-29 18:38:20 -05:00
J. Nick Koston
c3ebc39262 [ci] Split integration tests into 3 buckets when count > 10
When more than 10 integration tests are scheduled (or any change that
triggers run_all, e.g. core/infra changes that would run all 117 files),
fan out the pytest job into 3 parallel matrix entries. Below the
threshold, a single bucket runs as before, so small targeted PRs see no
extra job overhead.

determine-jobs.py now owns the bucketing end-to-end: it expands run_all
into the explicit glob of tests/integration/test_*.py and pre-splits the
sorted list using the same balanced contiguous-partition formula as
script/clang-tidy. The CI workflow consumes the precomputed buckets via
fromJson() in the matrix, mirroring how component-test-batches works,
so no shell-side splitting is needed.

The previous integration-tests-run-all and integration-test-files
workflow outputs are replaced by a single integration-test-buckets
list-of-objects ({name, tests}); the integration-tests gate boolean is
unchanged.
2026-04-29 18:16:43 -05:00
J. Nick Koston
b8d24c9e49 [mcp23xxx_base] Reject unsupported interrupt_pin options (inverted, allow_other_uses) (#16149) 2026-04-30 11:14:07 +12:00
J. Nick Koston
9b1f5c59bb [core] Fix null deref in WarnIfComponentBlockingGuard for self-keyed scheduler timers (#16150) 2026-04-29 23:05:38 +00:00
Jonathan Swoboda
e4b33fddf5 [esp32] Add ESP-IDF 6.0.1 platform entry (#16146) 2026-04-29 18:43:15 -04:00
Jonathan Swoboda
77da64a367 [sx126x] Add cold sleep option and drop unused RTC wakeup bit (#16144) 2026-04-29 17:05:51 -04:00
J. Nick Koston
cecccebc64 [core] DelayAction: drop Component inheritance, use self-keyed scheduler (#16129) 2026-04-29 20:35:04 +00:00
Jonathan Swoboda
53b682e48f [ci] Bump clang-tidy from 18.1.8 to 22.1.0.1 (#16078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-29 20:19:33 +00:00
Mat931
14910e65d9 [ota] Use WatchdogManager for OTA on ESP32 (#16138)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-29 20:15:21 +00:00
J. Nick Koston
813964714c [esp32] Move HAL bodies into components/esp32/hal.cpp + inline trivial dispatches (#16111) 2026-04-29 20:09:08 +00:00
J. Nick Koston
5a146ab6b7 [valve] Fold ControlAction fields into a single stateless lambda (#16123) 2026-04-29 19:20:15 +00:00
J. Nick Koston
61a41402df [fan] Fold TurnOnAction fields into a single stateless lambda (#16122) 2026-04-29 19:16:05 +00:00
Mat931
59b4cfd07c [watchdog] Use default CHECK_IDLE_TASK and PANIC when configuring the watchdog (#16142) 2026-04-29 18:41:12 +00:00
J. Nick Koston
c41f38e16d [scheduler] Add self-keyed timer API for callers without a Component (#16127) 2026-04-29 13:24:37 -05:00
Clyde Stubbs
0ad8a071a7 [espnow] Cleanup method visibility and naming (#16109) 2026-04-29 14:18:21 -04:00
J. Nick Koston
985dba9332 [core] Defer heavy module-scope imports in __main__, loader, and config (#15955) 2026-04-29 13:17:59 -05:00
GelidusResearch
ca3f7251d4 [ens160] Fix sensor initialization timing (#16024)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-29 14:07:28 -04:00
J. Nick Koston
44cabc191d [core] Catch body-read errors in download_content (#16023) 2026-04-29 14:06:41 -04:00
J. Nick Koston
e5b1991cf7 [fan] Add tests for fan.turn_on action field combinations (#16125) 2026-04-29 12:46:06 -05:00
J. Nick Koston
7fba57ce51 [valve] Add tests for valve.control action field combinations (#16126) 2026-04-29 12:45:30 -05:00
J. Nick Koston
69a33d8ac0 [core] Inline HAL clock wrappers and split hal.h into per-platform headers (#15977) 2026-04-29 12:31:55 -05:00
Jonathan Swoboda
ce61dcf387 [remote_base][core] Drop redundant typename in dependent type contexts (#16137) 2026-04-29 16:54:17 +00:00
Jonathan Swoboda
bae6b51652 [kamstrup_kmp][toshiba] Fix signed/unsigned comparisons against sizeof (#16135) 2026-04-29 11:33:57 -04:00
Jonathan Swoboda
557c3d4436 [aqi] Use std::max initializer-list for non-negative AQI clamp (#16134) 2026-04-29 11:33:29 -04:00
Jonathan Swoboda
bacee89bca [mixer_speaker] NOLINT bugprone-unchecked-optional-access in audio_mixer_task (#16130) 2026-04-29 10:56:13 -04:00
Jonathan Swoboda
2157d11913 [haier] Fix bugprone-unchecked-optional-access; switch HardwareInfo to char[9] (#16124) 2026-04-29 14:26:53 +00:00
Jonathan Swoboda
42b8597719 [api] Extend NOLINT to cover bugprone-random-generator-seed in MAC varint test (#16120) 2026-04-29 13:58:19 +00:00
Jonathan Swoboda
2bd28eee9d [tormatic] Use .value() for checked optional access in read_gate_status_ (#16121) 2026-04-29 09:51:31 -04:00
J. Nick Koston
0a497d3c22 [light] Fold LightControlAction fields into a single stateless lambda (#16118) 2026-04-29 08:35:17 -05:00
Jonathan Swoboda
79da2b9704 [time] Fix bugprone-unchecked-optional-access in CronTrigger::check_time_ (#16107) 2026-04-29 08:30:46 -04:00
Jonathan Swoboda
ae5b211c89 [api] Avoid JsonDocument copy-and-swap operator= in ActionResponse ctor (#16106) 2026-04-29 08:30:35 -04:00
J. Nick Koston
8ceada8d04 [core] Download external_files in parallel (#16021) 2026-04-29 14:32:30 +12:00
J. Nick Koston
49c7a6928e [script] Fix cpp_unit_test crash for non-MULTI_CONF platform components (#16104) 2026-04-29 14:32:13 +12:00
J. Nick Koston
2fce71e0d4 [wifi] Add phy_mode option for ESP8266 (#16055) 2026-04-29 14:31:07 +12:00
J. Nick Koston
80251c54be [climate] Add climate.control coverage to component tests via thermostat (#16052) 2026-04-29 14:27:56 +12:00
J. Nick Koston
0d51a122d0 [cover] Add cover.control / cover.template.publish coverage to template tests (#16051) 2026-04-29 14:27:40 +12:00
J. Nick Koston
5a33c50015 [light] Use constexpr template for DimRelativeAction transition_length (#16038) 2026-04-29 14:26:38 +12:00
J. Nick Koston
0d150dc57e [light] Use constexpr template for ToggleAction transition_length (#16037) 2026-04-29 14:25:18 +12:00
J. Nick Koston
d287876d8d [light] Use bitmask template for LightControlAction unused fields (#16039) 2026-04-29 14:20:37 +12:00
J. Nick Koston
592486ae9a [analyze_memory] Attribute main.cpp setup()/loop() to esphome core (#16033) 2026-04-29 14:06:54 +12:00
Jonathan Swoboda
c3bd38af77 [feedback] Fix bugprone-unchecked-optional-access in start_direction_ (#16103) 2026-04-28 21:54:15 -04:00
108 changed files with 2469 additions and 527 deletions

View File

@@ -5,24 +5,30 @@ Checks: >-
-altera-*,
-android-*,
-boost-*,
-bugprone-derived-method-shadowing-base-method,
-bugprone-easily-swappable-parameters,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-invalid-enum-default-initialization,
-bugprone-multi-level-implicit-pointer-conversion,
-bugprone-narrowing-conversions,
-bugprone-tagged-union-member-count,
-bugprone-signed-char-misuse,
-bugprone-switch-missing-default-case,
-cert-dcl50-cpp,
-cert-err33-c,
-cert-err58-cpp,
-cert-int09-c,
-cert-oop57-cpp,
-cert-str34-c,
-clang-analyzer-optin.core.EnumCastOutOfRange,
-clang-analyzer-optin.cplusplus.UninitializedObject,
-clang-analyzer-osx.*,
-clang-analyzer-security.ArrayBound,
-clang-diagnostic-delete-abstract-non-virtual-dtor,
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
-clang-diagnostic-deprecated-declarations,
-clang-diagnostic-ignored-optimization-argument,
-clang-diagnostic-missing-designated-field-initializers,
-clang-diagnostic-missing-field-initializers,
-clang-diagnostic-shadow-field,
-clang-diagnostic-unused-const-variable,
@@ -42,6 +48,7 @@ Checks: >-
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-prefer-member-initializer,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-pro-type-const-cast,
@@ -54,12 +61,13 @@ Checks: >-
-cppcoreguidelines-rvalue-reference-param-not-moved,
-cppcoreguidelines-special-member-functions,
-cppcoreguidelines-use-default-member-init,
-cppcoreguidelines-use-enum-class,
-cppcoreguidelines-virtual-class-destructor,
-fuchsia-default-arguments-calls,
-fuchsia-default-arguments-declarations,
-fuchsia-multiple-inheritance,
-fuchsia-overloaded-operator,
-fuchsia-statically-constructed-objects,
-fuchsia-default-arguments-declarations,
-fuchsia-default-arguments-calls,
-google-build-using-namespace,
-google-explicit-constructor,
-google-readability-braces-around-statements,
@@ -71,16 +79,23 @@ Checks: >-
-llvm-else-after-return,
-llvm-header-guard,
-llvm-include-order,
-llvm-prefer-static-over-anonymous-namespace,
-llvm-qualified-auto,
-llvm-use-ranges,
-llvmlibc-*,
-misc-const-correctness,
-misc-include-cleaner,
-misc-multiple-inheritance,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
-misc-override-with-different-visibility,
-misc-unused-parameters,
-misc-use-anonymous-namespace,
-misc-use-internal-linkage,
-modernize-avoid-bind,
-modernize-avoid-variadic-functions,
-modernize-avoid-c-arrays,
-modernize-avoid-c-style-cast,
-modernize-concat-nested-namespaces,
-modernize-macro-to-enum,
-modernize-return-braced-init-list,
@@ -88,32 +103,42 @@ Checks: >-
-modernize-use-auto,
-modernize-use-constraints,
-modernize-use-default-member-init,
-modernize-use-designated-initializers,
-modernize-use-equals-default,
-modernize-use-integer-sign-comparison,
-modernize-use-nodiscard,
-modernize-use-nullptr,
-modernize-use-nodiscard,
-modernize-use-nullptr,
-modernize-use-ranges,
-modernize-use-trailing-return-type,
-mpi-*,
-objc-*,
-performance-enum-size,
-portability-avoid-pragma-once,
-portability-template-virtual-member-function,
-readability-ambiguous-smartptr-reset-call,
-readability-avoid-nested-conditional-operator,
-readability-container-contains,
-readability-container-data-pointer,
-readability-convert-member-functions-to-static,
-readability-else-after-return,
-readability-enum-initial-value,
-readability-function-cognitive-complexity,
-readability-implicit-bool-conversion,
-readability-isolate-declaration,
-readability-magic-numbers,
-readability-make-member-function-const,
-readability-math-missing-parentheses,
-readability-named-parameter,
-readability-redundant-casting,
-readability-redundant-inline-specifier,
-readability-redundant-member-init,
-readability-redundant-parentheses,
-readability-redundant-string-init,
-readability-redundant-typename,
-readability-uppercase-literal-suffix,
-readability-use-anyofallof,
-readability-use-std-min-max,
-readability-use-concise-preprocessor-directives,
WarningsAsErrors: '*'
FormatStyle: google
CheckOptions:

View File

@@ -1 +1 @@
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324
0c7f309d70eca8e3efd510092ddb23c530f3934c49371717efa124b788d761f8

View File

@@ -199,8 +199,7 @@ jobs:
- common
outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }}
@@ -243,8 +242,7 @@ jobs:
# Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
@@ -267,12 +265,16 @@ jobs:
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
integration-tests:
name: Run integration tests
name: Run integration tests (${{ matrix.bucket.name }})
runs-on: ubuntu-latest
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.integration-tests == 'true'
strategy:
fail-fast: false
matrix:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -299,19 +301,14 @@ jobs:
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
env:
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
# JSON array of test paths; parsed into a bash array below to avoid
# shell word-splitting / glob hazards.
BUCKET_TESTS: ${{ toJson(matrix.bucket.tests) }}
run: |
. venv/bin/activate
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
echo "Running all integration tests"
pytest -vv --no-cov --tb=native -n auto tests/integration/
else
# Parse JSON array into bash array to avoid shell expansion issues
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
echo "Running ${#test_files[@]} specific integration tests"
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
fi
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
cpp-unit-tests:
name: Run C++ unit tests

View File

@@ -21,7 +21,7 @@ import argcomplete
# Note: Do not import modules from esphome.components here, as this would
# cause them to be loaded before external components are processed, resulting
# in the built-in version being used instead of the external component one.
from esphome import const, writer, yaml_util
from esphome import const
import esphome.codegen as cg
from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import (
@@ -72,7 +72,12 @@ from esphome.util import (
run_external_process,
safe_print,
)
from esphome.zeroconf import discover_mdns_devices
# Keep expensive imports (zeroconf, writer, yaml_util, etc.) out of this
# module's top level. Every `esphome` invocation — including fast paths
# like `esphome version` — pays the cost of what's imported here before
# any command runs. Import inside the function that needs it instead.
# `script/check_import_time.py` enforces a budget in CI.
_LOGGER = logging.getLogger(__name__)
@@ -241,6 +246,8 @@ def _discover_mac_suffix_devices() -> list[str] | None:
"""
if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()):
return None
from esphome.zeroconf import discover_mdns_devices
_LOGGER.info("Discovering devices...")
if not (discovered := discover_mdns_devices(CORE.name)):
_LOGGER.warning(
@@ -660,7 +667,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
return 0
def wrap_to_code(name, comp):
def _wrap_to_code(name, comp, yaml_util):
coro = coroutine(comp.to_code)
@functools.wraps(comp.to_code)
@@ -680,6 +687,8 @@ def wrap_to_code(name, comp):
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
from esphome import writer
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
@@ -691,17 +700,21 @@ def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
def generate_cpp_contents(config: ConfigType) -> None:
from esphome import yaml_util
_LOGGER.info("Generating C++ source...")
for name, component, conf in iter_component_configs(CORE.config):
if component.to_code is not None:
coro = wrap_to_code(name, component)
coro = _wrap_to_code(name, component, yaml_util)
CORE.add_job(coro, conf)
CORE.flush_tasks()
def write_cpp_file(native_idf: bool = False) -> int:
from esphome import writer
code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s)
@@ -1180,6 +1193,8 @@ def command_wizard(args: ArgsProtocol) -> int | None:
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import yaml_util
if not CORE.verbose:
config = strip_default_ids(config)
output = yaml_util.dump(config, args.show_secrets)
@@ -1321,6 +1336,8 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_clean_all(args: ArgsProtocol) -> int | None:
from esphome import writer
try:
writer.clean_all(args.configuration)
except OSError as err:
@@ -1336,6 +1353,8 @@ def command_version(args: ArgsProtocol) -> int | None:
def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import writer
try:
writer.clean_build()
except OSError as err:
@@ -1538,6 +1557,8 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import yaml_util
new_name = args.name
for c in new_name:
if c not in ALLOWED_NAME_CHARS:

View File

@@ -793,8 +793,11 @@ class MemoryAnalyzer:
"""Scan ESPHome source object files to map extern "C" symbols to components.
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
under ``src/`` (including ``src/main.cpp.o`` and everything beneath
``src/esphome/``) and build a symbol-to-component mapping. This catches
``extern "C"`` functions, the ESPHome-generated ``setup()``/``loop()``
entry points in ``main.cpp``, and other symbols that lack C++ namespace
prefixes.
Skips scanning if ``_source_symbol_map`` was already populated by
``_parse_map_file()``.
@@ -806,12 +809,12 @@ class MemoryAnalyzer:
if obj_dir is None:
return
# Find ESPHome source object files
esphome_src_dir = obj_dir / "src" / "esphome"
if not esphome_src_dir.is_dir():
# Scan all ESPHome-owned source object files: src/main.cpp.o and src/esphome/...
src_dir = obj_dir / "src"
if not src_dir.is_dir():
return
obj_files = sorted(esphome_src_dir.rglob("*.o"))
obj_files = sorted(src_dir.rglob("*.o"))
if not obj_files:
return
@@ -1064,6 +1067,10 @@ class MemoryAnalyzer:
if component_name in self.external_components:
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
# ESPHome-generated entry point: src/main.cpp.o (contains setup()/loop())
if len(parts) >= 2 and parts[-2:] == ("src", "main.cpp.o"):
return _COMPONENT_CORE
# ESPHome core: src/esphome/core/... or src/esphome/...
if "core" in parts and "esphome" in parts:
return _COMPONENT_CORE

View File

@@ -127,7 +127,7 @@ def validate_potentially_or_condition(value):
return validate_condition(value)
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
DelayAction = cg.esphome_ns.class_("DelayAction", Action)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action)
@@ -396,7 +396,6 @@ async def delay_action_to_code(
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_component(var, {})
template_ = await cg.templatable(config, args, cg.uint32)
cg.add(var.set_delay(template_))
return var

View File

@@ -129,7 +129,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
uint8_t *led_data = &frame_[6];
for (int led = 0; led < accepted_led_count; led++, led_data += 3) {
auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]);
auto white = std::min({led_data[0], led_data[1], led_data[2]});
it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
}

View File

@@ -78,7 +78,8 @@ class ActionResponse {
: success_(success), error_message_(error_message) {
if (data == nullptr || data_len == 0)
return;
this->json_document_ = json::parse_json(data, data_len);
JsonDocument tmp = json::parse_json(data, data_len);
swap(this->json_document_, tmp);
}
#endif

View File

@@ -424,6 +424,7 @@ class ProtoEncode {
if (len == 0 && !force)
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
// NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute
if (len < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
*pos++ = static_cast<uint8_t>(len);

View File

@@ -14,11 +14,7 @@ class AQICalculator : public AbstractAQICalculator {
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
float aqi = std::max(pm2_5_index, pm10_0_index);
if (aqi < 0.0f) {
aqi = 0.0f;
}
float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
return static_cast<uint16_t>(std::lround(aqi));
}

View File

@@ -12,11 +12,7 @@ class CAQICalculator : public AbstractAQICalculator {
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
float aqi = std::max(pm2_5_index, pm10_0_index);
if (aqi < 0.0f) {
aqi = 0.0f;
}
float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
return static_cast<uint16_t>(std::lround(aqi));
}

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from functools import partial
import hashlib
import logging
from pathlib import Path
@@ -19,7 +20,7 @@ from esphome.const import (
)
from esphome.core import CORE, ID, HexInt
from esphome.cpp_generator import MockObj
from esphome.external_files import download_content
from esphome.external_files import download_web_files_in_config
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -63,15 +64,6 @@ def _compute_local_file_path(value: ConfigType) -> Path:
return base_dir / key
def _download_web_file(value: ConfigType) -> ConfigType:
url = value[CONF_URL]
path = _compute_local_file_path(value)
download_content(url, path)
_LOGGER.debug("download_web_file: path=%s", path)
return value
def _file_schema(value: ConfigType | str) -> ConfigType:
if isinstance(value, str):
return _validate_file_shorthand(value)
@@ -142,11 +134,10 @@ LOCAL_SCHEMA = cv.Schema(
}
)
WEB_SCHEMA = cv.All(
WEB_SCHEMA = cv.Schema(
{
cv.Required(CONF_URL): cv.url,
},
_download_web_file,
}
)
@@ -209,6 +200,7 @@ def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]
CONFIG_SCHEMA = cv.All(
cv.only_on_esp32,
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
partial(download_web_files_in_config, path_for=_compute_local_file_path),
_validate_supported_local_file,
)

View File

@@ -374,7 +374,8 @@ void Climate::save_state_(const ClimateTraits &traits) {
#define TEMP_IGNORE_MEMACCESS
#endif
ClimateDeviceRestoreState state{};
// initialize as zero to prevent random data on stack triggering erase
// initialize as zero (including padding) to prevent random data on stack triggering erase
// NOLINTNEXTLINE(bugprone-raw-memory-call-on-non-trivial-type) -- intentional bytewise zero for RTC save
memset(&state, 0, sizeof(ClimateDeviceRestoreState));
#ifdef TEMP_IGNORE_MEMACCESS
#pragma GCC diagnostic pop

View File

@@ -1,5 +1,6 @@
#pragma once
// DNM: integration-test bucketing CI probe — do not merge.
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"

View File

@@ -17,11 +17,13 @@ constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8;
static inline uint32_t read_mem_u32(uintptr_t addr) {
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
// NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
return *reinterpret_cast<volatile uint32_t *>(addr);
}
static inline uint8_t read_mem_u8(uintptr_t addr) {
return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr)
// NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
return *reinterpret_cast<volatile uint8_t *>(addr);
}
// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information
@@ -98,6 +100,7 @@ void DebugComponent::log_partition_info_() {
#define NRF_PERIPH_ENABLED(periph, reg) \
YESNO(((reg)->ENABLE & periph##_ENABLE_ENABLE_Msk) == (periph##_ENABLE_ENABLE_Enabled << periph##_ENABLE_ENABLE_Pos))
// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- nRF peripheral registers are MMIO at fixed addresses
static void log_peripherals_info() {
// most peripherals are enabled only when in use so ESP_LOGV is enough
ESP_LOGV(TAG, "Peripherals status:");
@@ -131,6 +134,7 @@ static void log_peripherals_info() {
YESNO((NRF_CRYPTOCELL->ENABLE & CRYPTOCELL_ENABLE_ENABLE_Msk) ==
(CRYPTOCELL_ENABLE_ENABLE_Enabled << CRYPTOCELL_ENABLE_ENABLE_Pos)));
}
// NOLINTEND(clang-analyzer-core.FixedAddressDereference)
#undef NRF_PERIPH_ENABLED
#endif
@@ -159,8 +163,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
char *buf = buffer.data();
// Main supply status
const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- NRF_POWER is MMIO at a fixed address
auto regstatus = nrf_power_mainregstatus_get(NRF_POWER);
const char *supply_status = (regstatus == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append_str(buf, size, pos, "|Main supply status: ");
pos = buf_append_str(buf, size, pos, supply_status);

View File

@@ -56,8 +56,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
// limit amount of lights per universe and received
// packet.count is the number of DMX bytes including start code; divide by channels to get the number of lights
int lights_in_packet = (packet.count > 0) ? (packet.count - 1) / channels_ : 0;
int output_end =
std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet));
int output_end = std::min({it->size(), output_offset + get_lights_per_universe(), output_offset + lights_in_packet});
auto *input_data = packet.values + 1;
auto effect_name = get_name();

View File

@@ -5,6 +5,15 @@
// Implementation based on:
// https://github.com/sciosense/ENS160_driver
// For best performance, the sensor shall be operated in normal indoor air in the range -5 to 60°C
// (typical: 25°C); relative humidity: 20 to 80%RH (typical: 50%RH), non-condensing with no aggressive
// or poisonous gases present. Prolonged exposure to environments outside these conditions can affect
// performance and lifetime of the sensor.
// The sensor is designed for indoor use and is not waterproof or dustproof. It should be protected from
// water, condensation, dust, and aggressive gases. Note that the status will only be stored in non-volatile
// memory after an initial 24 h of continuous operation. If unpowered before the conclusion of that period,
// the ENS160 will resume "Initial Start-up" mode after re-powering.
#include "ens160_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
@@ -14,7 +23,9 @@ namespace ens160_base {
static const char *const TAG = "ens160";
static const uint8_t ENS160_BOOTING = 10;
// Datasheet specifies 10ms, but some users report that 10ms is not sufficient for the
// sensor to boot and be ready for commands. 11ms seems to be a safe value.
static const uint8_t ENS160_BOOTING = 11;
static const uint16_t ENS160_PART_ID = 0x0160;
@@ -91,6 +102,8 @@ void ENS160Component::setup() {
this->mark_failed();
return;
}
delay(ENS160_BOOTING);
// clear command
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) {
this->error_code_ = WRITE_FAILED;
@@ -102,6 +115,7 @@ void ENS160Component::setup() {
this->mark_failed();
return;
}
delay(ENS160_BOOTING);
// read firmware version
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) {
@@ -109,6 +123,8 @@ void ENS160Component::setup() {
this->mark_failed();
return;
}
delay(ENS160_BOOTING);
uint8_t version_data[3];
if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) {
this->error_code_ = READ_FAILED;
@@ -223,7 +239,6 @@ void ENS160Component::update() {
if (this->aqi_ != nullptr) {
// remove reserved bits, just in case they are used in future
data_aqi = ENS160_DATA_AQI & data_aqi;
this->aqi_->publish_state(data_aqi);
}

View File

@@ -729,6 +729,9 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 4),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 1
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",

View File

@@ -1,17 +1,8 @@
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "crash_handler.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#include "preferences.h"
#include <esp_clk_tree.h>
#include <esp_cpu.h>
#include <esp_idf_version.h>
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
@@ -22,54 +13,7 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
// yield(), delay(), micros(), millis_64() inlined in hal.h.
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract.
uint32_t IRAM_ATTR HOT millis() {
#if CONFIG_FREERTOS_HZ == 1000
if (xPortInIsrContext()) [[unlikely]] {
return xTaskGetTickCountFromISR();
}
return xTaskGetTickCount();
#else
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
#endif
}
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
esp_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
void arch_init() {
#ifdef USE_ESP32_CRASH_HANDLER
// Read crash data from previous boot before anything else
esp32::crash_handler_read_and_clear();
#endif
// Enable the task watchdog only on the loop task (from which we're currently running)
esp_task_wdt_add(nullptr);
// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled,
// in which case safe_mode will mark it valid after confirming successful boot.
#ifndef USE_OTA_ROLLBACK
esp_ota_mark_app_valid_cancel_rollback();
#endif
}
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
return freq;
}
// HAL functions live in hal.cpp. This file keeps only the loop task setup.
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StackType_t

View File

@@ -0,0 +1,71 @@
#ifdef USE_ESP32
// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set
// before crash_handler.h's #ifdef-guarded namespace block is parsed.
#include "esphome/core/defines.h"
#include "crash_handler.h"
#include "esphome/core/hal.h"
#include <esp_clk_tree.h>
#include <esp_ota_ops.h>
#include <esp_system.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
// Empty esp32 namespace block to satisfy ci-custom's lint_namespace check.
// HAL functions live in namespace esphome (root) — they are not part of the
// esp32 component's API.
namespace esphome::esp32 {} // namespace esphome::esp32
namespace esphome {
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract.
uint32_t IRAM_ATTR HOT millis() {
#if CONFIG_FREERTOS_HZ == 1000
if (xPortInIsrContext()) [[unlikely]] {
return xTaskGetTickCountFromISR();
}
return xTaskGetTickCount();
#else
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
#endif
}
void arch_restart() {
esp_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
void arch_init() {
#ifdef USE_ESP32_CRASH_HANDLER
// Read crash data from previous boot before anything else
esp32::crash_handler_read_and_clear();
#endif
// Enable the task watchdog only on the loop task (from which we're currently running)
esp_task_wdt_add(nullptr);
// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled,
// in which case safe_mode will mark it valid after confirming successful boot.
#ifndef USE_OTA_ROLLBACK
esp_ota_mark_app_valid_cancel_rollback();
#endif
}
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
return freq;
}
} // namespace esphome
#endif // USE_ESP32

View File

@@ -84,7 +84,7 @@ void HOT delay(uint32_t ms) {
optimistic_yield(1000);
}
}
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
// delayMicroseconds(), arch_feed_wdt(), and progmem_read_*() are inlined in hal/hal_esp8266.h.
void arch_restart() {
system_restart();
// restart() doesn't always end execution
@@ -93,17 +93,6 @@ void arch_restart() {
}
}
void arch_init() {}
void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }

View File

@@ -140,6 +140,7 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- GPIO_REG_WRITE is MMIO at a fixed address
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin);
}

View File

@@ -51,7 +51,7 @@ static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false;
}
*dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr)
*dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
return true;
}
@@ -64,7 +64,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
}
auto *ptr = &ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr)
*ptr = value;
*ptr = value; // NOLINT(clang-analyzer-core.FixedAddressDereference)
return true;
}

View File

@@ -26,9 +26,9 @@ espnow_ns = cg.esphome_ns.namespace("espnow")
ESPNowComponent = espnow_ns.class_("ESPNowComponent", cg.Component)
# Handler interfaces that other components can use to register callbacks
ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler")
ESPNowReceivePacketHandler = espnow_ns.class_("ESPNowReceivePacketHandler")
ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler")
ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler")
ESPNowBroadcastHandler = espnow_ns.class_("ESPNowBroadcastHandler")
ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo")
ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref")
@@ -48,10 +48,10 @@ OnUnknownPeerTrigger = espnow_ns.class_(
"OnUnknownPeerTrigger", ESPNowHandlerTrigger, ESPNowUnknownPeerHandler
)
OnReceiveTrigger = espnow_ns.class_(
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivePacketHandler
)
OnBroadcastedTrigger = espnow_ns.class_(
"OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler
OnBroadcastTrigger = espnow_ns.class_(
"OnBroadcastTrigger", ESPNowHandlerTrigger, ESPNowBroadcastHandler
)
@@ -94,7 +94,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger),
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastTrigger),
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
),
@@ -140,11 +140,11 @@ async def to_code(config):
for on_receive in config.get(CONF_ON_RECEIVE, []):
trigger = await _trigger_to_code(on_receive)
cg.add(var.register_received_handler(trigger))
cg.add(var.register_receive_handler(trigger))
for on_receive in config.get(CONF_ON_BROADCAST, []):
trigger = await _trigger_to_code(on_receive)
cg.add(var.register_broadcasted_handler(trigger))
cg.add(var.register_broadcast_handler(trigger))
# ========================================== A C T I O N S ================================================

View File

@@ -67,6 +67,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
}
}
protected:
void play(const Ts &...x) override { /* ignore - see play_complex */
}
@@ -75,7 +76,6 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
this->error_.stop();
}
protected:
ActionList<Ts...> sent_;
ActionList<Ts...> error_;
@@ -89,7 +89,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
public:
protected:
void play(const Ts &...x) override {
peer_address_t address = this->address_.value(x...);
this->parent_->add_peer(address.data());
@@ -99,7 +99,7 @@ template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Pare
template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
public:
protected:
void play(const Ts &...x) override {
peer_address_t address = this->address_.value(x...);
this->parent_->del_peer(address.data());
@@ -107,8 +107,9 @@ template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public P
};
template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> {
public:
TEMPLATABLE_VALUE(uint8_t, channel)
protected:
void play(const Ts &...x) override {
if (this->parent_->is_wifi_enabled()) {
return;
@@ -125,9 +126,9 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *,
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
}
explicit OnReceiveTrigger() : has_address_(false) {}
explicit OnReceiveTrigger() {}
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -138,7 +139,7 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *,
protected:
bool has_address_{false};
uint8_t address_[ESP_NOW_ETH_ALEN];
uint8_t address_[ESP_NOW_ETH_ALEN]{};
};
class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowUnknownPeerHandler {
@@ -148,15 +149,15 @@ class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_
return false; // Return false to continue processing other internal handlers
}
};
class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowBroadcastedHandler {
class OnBroadcastTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowBroadcastHandler {
public:
explicit OnBroadcastedTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
explicit OnBroadcastTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
}
explicit OnBroadcastedTrigger() : has_address_(false) {}
explicit OnBroadcastTrigger() {}
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -167,7 +168,7 @@ class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_
protected:
bool has_address_{false};
uint8_t address_[ESP_NOW_ETH_ALEN];
uint8_t address_[ESP_NOW_ETH_ALEN]{};
};
} // namespace esphome::espnow

View File

@@ -299,13 +299,13 @@ void ESPNowComponent::loop() {
format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size));
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcasted_handlers_) {
if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size))
for (auto *handler : this->broadcast_handlers_) {
if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
}
} else {
for (auto *handler : this->received_handlers_) {
if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size))
for (auto *handler : this->receive_handlers_) {
if (handler->on_receive(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
}
}

View File

@@ -31,8 +31,8 @@ using peer_address_t = std::array<uint8_t, ESP_NOW_ETH_ALEN>;
enum class ESPNowTriggers : uint8_t {
TRIGGER_NONE = 0,
ON_NEW_PEER = 1,
ON_RECEIVED = 2,
ON_BROADCASTED = 3,
ON_RECEIVE = 2,
ON_BROADCAST = 3,
ON_SUCCEED = 10,
ON_FAILED = 11,
};
@@ -74,18 +74,18 @@ class ESPNowReceivedPacketHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
/// Handler interface for receiving broadcasted ESPNow packets
/// Handler interface for receiving ESPNow broadcast packets
/// Components should inherit from this class to handle incoming ESPNow data
class ESPNowBroadcastedHandler {
class ESPNowBroadcastHandler {
public:
/// Called when a broadcasted ESPNow packet is received
/// Called when an ESPNow broadcast packet is received
/// @param info Information about the received packet (sender MAC, etc.)
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
class ESPNowComponent : public Component {
@@ -136,13 +136,11 @@ class ESPNowComponent : public Component {
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
const send_callback_t &callback = nullptr);
void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); }
void register_receive_handler(ESPNowReceivedPacketHandler *handler) { this->receive_handlers_.push_back(handler); }
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
this->unknown_peer_handlers_.push_back(handler);
}
void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) {
this->broadcasted_handlers_.push_back(handler);
}
void register_broadcast_handler(ESPNowBroadcastHandler *handler) { this->broadcast_handlers_.push_back(handler); }
protected:
friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size);
@@ -156,8 +154,8 @@ class ESPNowComponent : public Component {
void send_();
std::vector<ESPNowUnknownPeerHandler *> unknown_peer_handlers_;
std::vector<ESPNowReceivedPacketHandler *> received_handlers_;
std::vector<ESPNowBroadcastedHandler *> broadcasted_handlers_;
std::vector<ESPNowReceivedPacketHandler *> receive_handlers_;
std::vector<ESPNowBroadcastHandler *> broadcast_handlers_;
std::vector<ESPNowPeer> peers_{};

View File

@@ -26,10 +26,10 @@ void ESPNowTransport::setup() {
this->peer_address_[5]);
// Register received handler
this->parent_->register_received_handler(this);
this->parent_->register_receive_handler(this);
// Register broadcasted handler
this->parent_->register_broadcasted_handler(this);
// Register broadcast handler
this->parent_->register_broadcast_handler(this);
}
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
@@ -56,7 +56,7 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
});
}
bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
@@ -71,7 +71,7 @@ bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *dat
return false; // Allow other handlers to run
}
bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);

View File

@@ -15,7 +15,7 @@ namespace espnow {
class ESPNowTransport : public packet_transport::PacketTransport,
public Parented<ESPNowComponent>,
public ESPNowReceivedPacketHandler,
public ESPNowBroadcastedHandler {
public ESPNowBroadcastHandler {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
@@ -25,8 +25,8 @@ class ESPNowTransport : public packet_transport::PacketTransport,
}
// ESPNow handler interface
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;

View File

@@ -31,17 +31,19 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import LambdaExpression
IS_PLATFORM_COMPONENT = True
fan_ns = cg.esphome_ns.namespace("fan")
Fan = fan_ns.class_("Fan", cg.EntityBase)
FanCall = fan_ns.class_("FanCall")
FanDirection = fan_ns.enum("FanDirection", is_class=True)
FAN_DIRECTION_ENUM = {
@@ -347,17 +349,38 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
)
async def fan_turn_on_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (oscillating := config.get(CONF_OSCILLATING)) is not None:
template_ = await cg.templatable(oscillating, args, cg.bool_)
cg.add(var.set_oscillating(template_))
if (speed := config.get(CONF_SPEED)) is not None:
template_ = await cg.templatable(speed, args, cg.int_)
cg.add(var.set_speed(template_))
if (direction := config.get(CONF_DIRECTION)) is not None:
template_ = await cg.templatable(direction, args, FanDirection)
cg.add(var.set_direction(template_))
return var
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
FIELDS = (
(CONF_OSCILLATING, "set_oscillating", cg.bool_),
(CONF_SPEED, "set_speed", cg.int_),
(CONF_DIRECTION, "set_direction", FanDirection),
)
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for conf_key, setter, type_ in FIELDS:
if (value := config.get(conf_key)) is None:
continue
if isinstance(value, Lambda):
inner = await cg.process_lambda(value, args, return_type=type_)
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
# Match TurnOnAction::ApplyFn signature: const Ts &... for trigger args.
apply_args = [
(FanCall.operator("ref"), "call"),
*((t.operator("const").operator("ref"), n) for t, n in args),
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
@automation.register_action(

View File

@@ -7,29 +7,24 @@
namespace esphome {
namespace fan {
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. The action only stores one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `speed: !lambda "return x;"`) keep working.
template<typename... Ts> class TurnOnAction : public Action<Ts...> {
public:
explicit TurnOnAction(Fan *state) : state_(state) {}
TEMPLATABLE_VALUE(bool, oscillating)
TEMPLATABLE_VALUE(int, speed)
TEMPLATABLE_VALUE(FanDirection, direction)
using ApplyFn = void (*)(FanCall &, const Ts &...);
TurnOnAction(Fan *state, ApplyFn apply) : state_(state), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->state_->turn_on();
if (this->oscillating_.has_value()) {
call.set_oscillating(this->oscillating_.value(x...));
}
if (this->speed_.has_value()) {
call.set_speed(this->speed_.value(x...));
}
if (this->direction_.has_value()) {
call.set_direction(this->direction_.value(x...));
}
this->apply_(call, x...);
call.perform();
}
Fan *state_;
ApplyFn apply_;
};
template<typename... Ts> class TurnOffAction : public Action<Ts...> {

View File

@@ -375,12 +375,10 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
// check if we have a wait time
if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
const uint32_t waittime = *this->direction_change_waittime_;
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, *this->direction_change_waittime_,
[this, dir]() { this->start_direction_(dir); });
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, waittime, [this, dir]() { this->start_direction_(dir); });
} else {
this->set_current_operation_(dir, true);
this->prev_command_trigger_ = trig;

View File

@@ -85,7 +85,7 @@ void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direct
this->force_send_control_ = true;
}
std::string HonClimate::get_cleaning_status_text() const {
const char *HonClimate::get_cleaning_status_text() const {
switch (this->cleaning_status_) {
case CleaningState::SELF_CLEAN:
return "Self clean";
@@ -134,29 +134,22 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haie
}
// All OK
hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
char tmp[9];
tmp[8] = 0;
strncpy(tmp, answr->protocol_version, 8);
this->hvac_hardware_info_ = HardwareInfo();
this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp);
strncpy(tmp, answr->software_version, 8);
this->hvac_hardware_info_.value().software_version_ = std::string(tmp);
strncpy(tmp, answr->hardware_version, 8);
this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp);
strncpy(tmp, answr->device_name, 8);
this->hvac_hardware_info_.value().device_name_ = std::string(tmp);
HardwareInfo info{}; // zero-init guarantees null-termination
strncpy(info.protocol_version_, answr->protocol_version, HARDWARE_INFO_STR_SIZE - 1);
strncpy(info.software_version_, answr->software_version, HARDWARE_INFO_STR_SIZE - 1);
strncpy(info.hardware_version_, answr->hardware_version, HARDWARE_INFO_STR_SIZE - 1);
strncpy(info.device_name_, answr->device_name, HARDWARE_INFO_STR_SIZE - 1);
info.functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
info.functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support
info.functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
info.functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
info.functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
this->use_crc_ = info.functions_[2];
#ifdef USE_TEXT_SENSOR
this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, this->hvac_hardware_info_.value().device_name_);
this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION,
this->hvac_hardware_info_.value().protocol_version_);
this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, info.device_name_);
this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, info.protocol_version_);
#endif
this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
this->hvac_hardware_info_.value().functions_[1] =
(answr->functions[1] & 0x02) != 0; // controller-device mode support
this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
this->use_crc_ = this->hvac_hardware_info_.value().functions_[2];
this->hvac_hardware_info_ = info;
this->set_phase(ProtocolPhases::SENDING_INIT_2);
return result;
} else {
@@ -347,10 +340,9 @@ void HonClimate::dump_config() {
" Device software version: %s\n"
" Device hardware version: %s\n"
" Device name: %s",
this->hvac_hardware_info_.value().protocol_version_.c_str(),
this->hvac_hardware_info_.value().software_version_.c_str(),
this->hvac_hardware_info_.value().hardware_version_.c_str(),
this->hvac_hardware_info_.value().device_name_.c_str());
this->hvac_hardware_info_.value().protocol_version_,
this->hvac_hardware_info_.value().software_version_,
this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_);
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
(this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
@@ -460,7 +452,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
if (this->action_request_.has_value()) {
if (this->action_request_.value().message.has_value()) {
this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
this->action_request_.value().message.reset();
this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access)
} else {
// Message already sent, reseting request and return to idle
this->action_request_.reset();
@@ -796,7 +788,7 @@ void HonClimate::set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSe
}
}
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::string &value) {
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const char *value) {
size_t index = (size_t) type;
if (this->sub_text_sensors_[index] != nullptr)
this->sub_text_sensors_[index]->publish_state(value);

View File

@@ -90,7 +90,7 @@ class HonClimate : public HaierClimateBase {
void set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens);
protected:
void update_sub_text_sensor_(SubTextSensorType type, const std::string &value);
void update_sub_text_sensor_(SubTextSensorType type, const char *value);
text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr};
#endif
#ifdef USE_SWITCH
@@ -116,7 +116,7 @@ class HonClimate : public HaierClimateBase {
void set_vertical_airflow(hon_protocol::VerticalSwingMode direction);
esphome::optional<hon_protocol::HorizontalSwingMode> get_horizontal_airflow() const;
void set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction);
std::string get_cleaning_status_text() const;
const char *get_cleaning_status_text() const;
CleaningState get_cleaning_status() const;
void start_self_cleaning();
void start_steri_cleaning();
@@ -166,11 +166,12 @@ class HonClimate : public HaierClimateBase {
void fill_control_messages_queue_();
void clear_control_messages_queue_();
static constexpr size_t HARDWARE_INFO_STR_SIZE = 9;
struct HardwareInfo {
std::string protocol_version_;
std::string software_version_;
std::string hardware_version_;
std::string device_name_;
char protocol_version_[HARDWARE_INFO_STR_SIZE];
char software_version_[HARDWARE_INFO_STR_SIZE];
char hardware_version_[HARDWARE_INFO_STR_SIZE];
char device_name_[HARDWARE_INFO_STR_SIZE];
bool functions_[5];
};

View File

@@ -191,7 +191,7 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
if (this->action_request_.has_value()) {
if (this->action_request_.value().message.has_value()) {
this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
this->action_request_.value().message.reset();
this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access)
} else {
// Message already sent, reseting request and return to idle
this->action_request_.reset();

View File

@@ -243,7 +243,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
// Non-chunked path
int available_data = stream_ptr->available();
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
int bufsize = std::min({max_len, remaining, (size_t) available_data});
if (bufsize == 0) {
this->duration_ms += (millis() - start);

View File

@@ -139,12 +139,12 @@ void KamstrupKMPComponent::clear_uart_rx_buffer_() {
void KamstrupKMPComponent::read_command_(uint16_t command) {
uint8_t buffer[20] = {0};
int buffer_len = 0;
size_t buffer_len = 0;
int data;
int timeout = 250; // ms
// Read the data from the UART
while (timeout > 0 && buffer_len < static_cast<int>(sizeof(buffer))) {
while (timeout > 0 && buffer_len < sizeof(buffer)) {
if (this->available()) {
data = this->read();
if (data > -1) {
@@ -183,7 +183,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) {
// Decode
uint8_t msg[20] = {0};
int msg_len = 0;
for (int i = 1; i < buffer_len - 1; i++) {
for (size_t i = 1; i < buffer_len - 1; i++) {
if (buffer[i] == 0x1B) {
msg[msg_len++] = buffer[i + 1] ^ 0xFF;
i++;

View File

@@ -8,84 +8,59 @@ namespace esphome::light {
enum class LimitMode { CLAMP, DO_NOTHING };
template<typename... Ts> class ToggleAction : public Action<Ts...> {
template<bool HasTransitionLength, typename... Ts> class ToggleAction : public Action<Ts...> {
public:
explicit ToggleAction(LightState *state) : state_(state) {}
TEMPLATABLE_VALUE(uint32_t, transition_length)
template<typename V> void set_transition_length(V value) requires(HasTransitionLength) {
this->transition_length_ = value;
}
void play(const Ts &...x) override {
auto call = this->state_->toggle();
call.set_transition_length(this->transition_length_.optional_value(x...));
if constexpr (HasTransitionLength) {
call.set_transition_length(this->transition_length_.optional_value(x...));
}
call.perform();
}
protected:
LightState *state_;
struct NoTransition {};
[[no_unique_address]] std::conditional_t<HasTransitionLength, TemplatableFn<uint32_t, Ts...>, NoTransition>
transition_length_{};
};
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. The action only stores one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `brightness: !lambda "return x;"`) keep working.
template<typename... Ts> class LightControlAction : public Action<Ts...> {
public:
explicit LightControlAction(LightState *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(ColorMode, color_mode)
TEMPLATABLE_VALUE(bool, state)
TEMPLATABLE_VALUE(uint32_t, transition_length)
TEMPLATABLE_VALUE(uint32_t, flash_length)
TEMPLATABLE_VALUE(float, brightness)
TEMPLATABLE_VALUE(float, color_brightness)
TEMPLATABLE_VALUE(float, red)
TEMPLATABLE_VALUE(float, green)
TEMPLATABLE_VALUE(float, blue)
TEMPLATABLE_VALUE(float, white)
TEMPLATABLE_VALUE(float, color_temperature)
TEMPLATABLE_VALUE(float, cold_white)
TEMPLATABLE_VALUE(float, warm_white)
TEMPLATABLE_VALUE(uint32_t, effect)
using ApplyFn = void (*)(LightState *, LightCall &, const Ts &...);
LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
if (this->color_mode_.has_value())
call.set_color_mode(this->color_mode_.value(x...));
if (this->state_.has_value())
call.set_state(this->state_.value(x...));
if (this->transition_length_.has_value())
call.set_transition_length(this->transition_length_.value(x...));
if (this->flash_length_.has_value())
call.set_flash_length(this->flash_length_.value(x...));
if (this->brightness_.has_value())
call.set_brightness(this->brightness_.value(x...));
if (this->color_brightness_.has_value())
call.set_color_brightness(this->color_brightness_.value(x...));
if (this->red_.has_value())
call.set_red(this->red_.value(x...));
if (this->green_.has_value())
call.set_green(this->green_.value(x...));
if (this->blue_.has_value())
call.set_blue(this->blue_.value(x...));
if (this->white_.has_value())
call.set_white(this->white_.value(x...));
if (this->color_temperature_.has_value())
call.set_color_temperature(this->color_temperature_.value(x...));
if (this->cold_white_.has_value())
call.set_cold_white(this->cold_white_.value(x...));
if (this->warm_white_.has_value())
call.set_warm_white(this->warm_white_.value(x...));
if (this->effect_.has_value())
call.set_effect(this->effect_.value(x...));
this->apply_(this->parent_, call, x...);
call.perform();
}
protected:
LightState *parent_;
ApplyFn apply_;
};
template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : public Action<Ts...> {
public:
explicit DimRelativeAction(LightState *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(float, relative_brightness)
TEMPLATABLE_VALUE(uint32_t, transition_length)
template<typename V> void set_transition_length(V value) requires(HasTransitionLength) {
this->transition_length_ = value;
}
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
@@ -99,7 +74,9 @@ template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
call.set_state(new_brightness != 0.0f);
call.set_brightness(new_brightness);
call.set_transition_length(this->transition_length_.optional_value(x...));
if constexpr (HasTransitionLength) {
call.set_transition_length(this->transition_length_.optional_value(x...));
}
call.perform();
}
@@ -115,6 +92,9 @@ template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
float min_brightness_{0.0};
float max_brightness_{1.0};
LimitMode limit_mode_{LimitMode::CLAMP};
struct NoTransition {};
[[no_unique_address]] std::conditional_t<HasTransitionLength, TemplatableFn<uint32_t, Ts...>, NoTransition>
transition_length_{};
};
template<typename... Ts> class LightIsOnCondition : public Condition<Ts...> {

View File

@@ -37,6 +37,7 @@ from .types import (
AddressableSet,
ColorMode,
DimRelativeAction,
LightCall,
LightControlAction,
LightIsOffCondition,
LightIsOnCondition,
@@ -60,8 +61,10 @@ from .types import (
)
async def light_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_TRANSITION_LENGTH in config:
has_transition_length = CONF_TRANSITION_LENGTH in config
toggle_template_arg = cg.TemplateArguments(has_transition_length, *template_arg)
var = cg.new_Pvariable(action_id, toggle_template_arg, paren)
if has_transition_length:
template_ = await cg.templatable(
config[CONF_TRANSITION_LENGTH], args, cg.uint32
)
@@ -178,9 +181,9 @@ def _resolve_effect_index(config: ConfigType) -> int:
)
async def light_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
# (config_key, setter_name, c++ type)
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
FIELDS = (
(CONF_COLOR_MODE, "set_color_mode", ColorMode),
(CONF_STATE, "set_state", cg.bool_),
@@ -196,38 +199,50 @@ async def light_control_to_code(config, action_id, template_arg, args):
(CONF_COLD_WHITE, "set_cold_white", cg.float_),
(CONF_WARM_WHITE, "set_warm_white", cg.float_),
)
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for conf_key, setter, type_ in FIELDS:
if conf_key in config:
template_ = await cg.templatable(config[conf_key], args, type_)
cg.add(getattr(var, setter)(template_))
if conf_key not in config:
continue
value = config[conf_key]
if isinstance(value, Lambda):
inner = await cg.process_lambda(value, args, return_type=type_)
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
if CONF_EFFECT in config:
if isinstance(config[CONF_EFFECT], Lambda):
# Lambda returns a string — wrap in a C++ lambda that resolves
# the effect name to its uint32_t index at runtime
inner_lambda = await cg.process_lambda(
config[CONF_EFFECT], args, return_type=cg.std_string
)
fwd_args = ", ".join(n for _, n in args)
# capture="" is correct: paren is a global variable name
# string-interpolated into the body at codegen time, not a
# C++ runtime capture.
wrapper = LambdaExpression(
f"auto __effect_s = ({inner_lambda})({fwd_args});\n"
f"return {paren}->get_effect_index("
f"__effect_s.c_str(), __effect_s.size());",
args,
capture="",
return_type=cg.uint32,
body_lines.append(
f"{{ auto __effect_s = ({inner_lambda})({fwd_args});\n"
f"call.set_effect(parent->get_effect_index("
f"__effect_s.c_str(), __effect_s.size())); }}"
)
cg.add(var.set_effect(wrapper))
else:
# Static string — resolve effect name to index at codegen time
template_ = await cg.templatable(
_resolve_effect_index(config), args, cg.uint32
# Cast disambiguates between set_effect(uint32_t) and
# set_effect(optional<uint32_t>) when the literal is an int.
body_lines.append(
f"call.set_effect(static_cast<uint32_t>({_resolve_effect_index(config)}));"
)
cg.add(var.set_effect(template_))
return var
# Match LightControlAction::ApplyFn signature: const Ts &... for trigger args.
apply_args = [
(LightState.operator("ptr"), "parent"),
(LightCall.operator("ref"), "call"),
*((t.operator("const").operator("ref"), n) for t, n in args),
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
CONF_RELATIVE_BRIGHTNESS = "relative_brightness"
@@ -261,10 +276,12 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema(
)
async def light_dim_relative_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
has_transition_length = CONF_TRANSITION_LENGTH in config
dim_template_arg = cg.TemplateArguments(has_transition_length, *template_arg)
var = cg.new_Pvariable(action_id, dim_template_arg, paren)
templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, cg.float_)
cg.add(var.set_relative_brightness(templ))
if CONF_TRANSITION_LENGTH in config:
if has_transition_length:
templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
cg.add(var.set_transition_length(templ))
if conf := config.get(CONF_BRIGHTNESS_LIMITS):

View File

@@ -13,6 +13,7 @@ Color = cg.esphome_ns.class_("Color")
LightColorValues = light_ns.class_("LightColorValues")
LightStateRTCState = light_ns.struct("LightStateRTCState")
LightCall = light_ns.class_("LightCall")
# Color modes
ColorMode = light_ns.enum("ColorMode", is_class=True)

View File

@@ -2,6 +2,7 @@ from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ALLOW_OTHER_USES,
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT,
@@ -30,10 +31,29 @@ MCP23XXX_INTERRUPT_MODES = {
"FALLING": MCP23XXXInterruptMode.MCP23XXX_FALLING,
}
def _validate_interrupt_pin(value):
# The MCP component owns INT polarity (active-low, hardcoded falling-edge ISR)
# and installs a single ISR per GPIO, so neither inversion nor sharing is supported.
value = pins.internal_gpio_input_pin_schema(value)
if value.get(CONF_INVERTED):
raise cv.Invalid(
f"'{CONF_INVERTED}: true' is not supported on '{CONF_INTERRUPT_PIN}'; "
"the MCP23xxx INT line is fixed active-low"
)
if value.get(CONF_ALLOW_OTHER_USES):
raise cv.Invalid(
f"'{CONF_ALLOW_OTHER_USES}: true' is not supported on '{CONF_INTERRUPT_PIN}'; "
"sharing the interrupt pin between multiple MCP23xxx (or other components) "
"is not implemented. Remove the interrupt_pin to fall back to polling."
)
return value
MCP23XXX_CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_INTERRUPT_PIN): _validate_interrupt_pin,
}
).extend(cv.COMPONENT_SCHEMA)

View File

@@ -588,6 +588,7 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio
}
}
// NOLINTBEGIN(bugprone-unchecked-optional-access) -- audio_stream_info_ always set before this task is created
void MixerSpeaker::audio_mixer_task(void *params) {
MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
@@ -764,6 +765,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
// NOLINTEND(bugprone-unchecked-optional-access)
} // namespace esphome::mixer_speaker

View File

@@ -11,6 +11,7 @@ void nvmc_wait();
nrfx_err_t nrfx_nvmc_uicr_erase();
}
// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- NRF_UICR / NRF_TIMER2 are MMIO at fixed addresses
namespace esphome::nrf52 {
enum class StatusFlags : uint8_t {
@@ -113,6 +114,7 @@ static int board_esphome_init() {
return 0;
}
} // namespace esphome::nrf52
// NOLINTEND(clang-analyzer-core.FixedAddressDereference)
static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); }

View File

@@ -24,6 +24,8 @@ def AUTO_LOAD() -> list[str]:
components = ["safe_mode"]
if not CORE.using_zephyr:
components.extend(["md5"])
if CORE.is_esp32:
components.extend(["watchdog"])
return components

View File

@@ -60,6 +60,7 @@ OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) {
// Check boot mode - if boot mode is UART download mode,
// we will not be able to reset into normal mode once update is done
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- GPI is MMIO at a fixed address
int boot_mode = (GPI >> BOOT_MODE_SHIFT) & BOOT_MODE_MASK;
if (boot_mode == BOOT_MODE_UART_DOWNLOAD) {
return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING;

View File

@@ -2,6 +2,7 @@
#include "ota_backend_esp_idf.h"
#include "esphome/components/md5/md5.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
@@ -28,29 +29,9 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION;
}
#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15
// The following function takes longer than the 5 seconds timeout of WDT
esp_task_wdt_config_t wdtc;
wdtc.idle_core_mask = 0;
#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0
wdtc.idle_core_mask |= (1 << 0);
#endif
#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1
wdtc.idle_core_mask |= (1 << 1);
#endif
wdtc.timeout_ms = 15000;
wdtc.trigger_panic = false;
esp_task_wdt_reconfigure(&wdtc);
#endif
watchdog::WatchdogManager watchdog(15000);
esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_);
#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15
// Set the WDT back to the configured timeout
wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000;
esp_task_wdt_reconfigure(&wdtc);
#endif
if (err != ESP_OK) {
esp_ota_abort(this->update_handle_);
this->update_handle_ = 0;

View File

@@ -164,7 +164,7 @@ class RemoteTransmitterBase : public RemoteComponentBase {
return TransmitCall(this);
}
template<typename Protocol>
void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
auto call = this->transmit();
Protocol().encode(call.get_data(), data);
call.set_send_times(send_times);
@@ -250,10 +250,10 @@ template<typename T> class RemoteReceiverBinarySensor : public RemoteReceiverBin
}
public:
void set_data(typename T::ProtocolData data) { data_ = data; }
void set_data(T::ProtocolData data) { data_ = data; }
protected:
typename T::ProtocolData data_;
T::ProtocolData data_;
};
template<typename T>
@@ -278,7 +278,7 @@ class RemoteTransmittable {
protected:
template<typename Protocol>
void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
this->transmitter_->transmit<Protocol>(data, send_times, send_wait);
}
RemoteTransmitterBase *transmitter_;

View File

@@ -1,5 +1,6 @@
"""Speaker Media Player Setup."""
from functools import partial
import hashlib
import logging
from pathlib import Path
@@ -32,7 +33,7 @@ from esphome.const import (
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.external_files import download_content
from esphome.external_files import download_web_files_in_config
_LOGGER = logging.getLogger(__name__)
@@ -92,15 +93,6 @@ def _compute_local_file_path(value: dict) -> Path:
return base_dir / key
def _download_web_file(value):
url = value[CONF_URL]
path = _compute_local_file_path(value)
download_content(url, path)
_LOGGER.debug("download_web_file: path=%s", path)
return value
_PURPOSE_MAP = {
"MEDIA": media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"],
"ANNOUNCEMENT": media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"],
@@ -229,11 +221,10 @@ LOCAL_SCHEMA = cv.Schema(
}
)
WEB_SCHEMA = cv.All(
WEB_SCHEMA = cv.Schema(
{
cv.Required(CONF_URL): cv.url,
},
_download_web_file,
}
)
@@ -285,7 +276,12 @@ CONFIG_SCHEMA = cv.All(
),
# Remove before 2026.10.0
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string),
cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
cv.Optional(CONF_FILES): cv.All(
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
partial(
download_web_files_in_config, path_for=_compute_local_file_path
),
),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All(
cv.boolean, cv.requires_component(psram.DOMAIN)
),

View File

@@ -669,7 +669,7 @@ uint32_t Sprinkler::valve_run_duration_adjusted(const size_t valve_number) {
// run_duration must not be less than any of these
if ((run_duration < this->start_delay_) || (run_duration < this->stop_delay_) ||
(run_duration < this->switching_delay_.value_or(0) * 2)) {
return std::max(this->switching_delay_.value_or(0) * 2, std::max(this->start_delay_, this->stop_delay_));
return std::max({this->switching_delay_.value_or(0) * 2, this->start_delay_, this->stop_delay_});
}
return run_duration;
}

View File

@@ -1,3 +1,5 @@
from typing import Any
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import spi
@@ -5,6 +7,8 @@ from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import ID, TimePeriod
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
MULTI_CONF = True
CODEOWNERS = ["@swoboda1337"]
@@ -15,6 +19,7 @@ CONF_SX126X_ID = "sx126x_id"
CONF_BANDWIDTH = "bandwidth"
CONF_BITRATE = "bitrate"
CONF_CODING_RATE = "coding_rate"
CONF_COLD = "cold"
CONF_CRC_INVERTED = "crc_inverted"
CONF_CRC_SIZE = "crc_size"
CONF_CRC_POLYNOMIAL = "crc_polynomial"
@@ -144,7 +149,7 @@ SetModeStandbyAction = sx126x_ns.class_(
)
def validate_raw_data(value):
def validate_raw_data(value: Any) -> bytes | list[int]:
if isinstance(value, str):
return value.encode("utf-8")
if isinstance(value, list):
@@ -154,7 +159,7 @@ def validate_raw_data(value):
)
def validate_config(config):
def validate_config(config: ConfigType) -> ConfigType:
lora_bws = [
"7_8kHz",
"10_4kHz",
@@ -235,7 +240,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
@@ -307,24 +312,50 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id(
NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"sx126x.set_mode_sleep",
SetModeSleepAction,
NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"sx126x.set_mode_standby",
SetModeStandbyAction,
NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
async def no_args_action_to_code(config, action_id, template_arg, args):
async def no_args_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
SET_MODE_SLEEP_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(SX126x),
cv.Optional(CONF_COLD, default=False): cv.templatable(cv.boolean),
}
)
@automation.register_action(
"sx126x.set_mode_sleep",
SetModeSleepAction,
SET_MODE_SLEEP_ACTION_SCHEMA,
synchronous=True,
)
async def set_mode_sleep_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_COLD], args, bool)
cg.add(var.set_cold(template_))
return var
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(SX126x),
@@ -340,7 +371,12 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
SEND_PACKET_ACTION_SCHEMA,
synchronous=True,
)
async def send_packet_action_to_code(config, action_id, template_arg, args):
async def send_packet_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_DATA]

View File

@@ -56,7 +56,8 @@ template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Pa
template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(const Ts &...x) override { this->parent_->set_mode_sleep(); }
TEMPLATABLE_VALUE(bool, cold)
void play(const Ts &...x) override { this->parent_->set_mode_sleep(this->cold_.value(x...)); }
};
template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX126x> {

View File

@@ -459,9 +459,10 @@ void SX126x::set_mode_tx() {
this->write_opcode_(RADIO_SET_TX, buf, 3);
}
void SX126x::set_mode_sleep() {
void SX126x::set_mode_sleep(bool cold) {
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
uint8_t buf[1];
buf[0] = 0x05;
buf[0] = cold ? 0x00 : 0x04;
this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
}

View File

@@ -79,7 +79,7 @@ class SX126x : public Component,
void set_mode_rx();
void set_mode_tx();
void set_mode_standby(SX126xStandbyMode mode);
void set_mode_sleep();
void set_mode_sleep(bool cold = false);
void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
void set_pa_power(int8_t power) { this->pa_power_ = power; }
void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }

View File

@@ -31,13 +31,14 @@ void CronTrigger::check_time_() {
return;
if (this->last_check_.has_value()) {
if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) {
auto &last_check = *this->last_check_;
if (last_check > time && last_check.timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) {
// We went back in time (a lot), probably caused by time synchronization
ESP_LOGW(TAG, "Time has jumped back!");
} else if (*this->last_check_ >= time) {
} else if (last_check >= time) {
// already handled this one
return;
} else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) {
} else if (time > last_check && time.timestamp - last_check.timestamp > MAX_TIMESTAMP_DRIFT) {
// We went ahead in time (a lot), probably caused by time synchronization
ESP_LOGW(TAG, "Time has jumped ahead!");
this->last_check_ = time;
@@ -45,11 +46,11 @@ void CronTrigger::check_time_() {
}
while (true) {
this->last_check_->increment_second();
if (*this->last_check_ >= time)
last_check.increment_second();
if (last_check >= time)
break;
if (this->matches(*this->last_check_))
if (this->matches(last_check))
this->trigger();
}
}

View File

@@ -282,12 +282,13 @@ optional<GateStatus> Tormatic::read_gate_status_() {
}
}
auto hdr = this->pending_hdr_.value();
// Wait for all payload bytes to arrive before processing.
if (this->available() < this->pending_hdr_->payload_size()) {
if (this->available() < hdr.payload_size()) {
return {};
}
auto hdr = *this->pending_hdr_;
this->pending_hdr_.reset();
switch (hdr.type) {

View File

@@ -275,7 +275,7 @@ static Ras2819tSecondPacketCodes get_ras_2819t_second_packet_codes(climate::Clim
*/
static uint8_t get_ras_2819t_temp_code(float temperature) {
int temp_index = static_cast<int>(temperature) - 18;
if (temp_index < 0 || temp_index >= static_cast<int>(sizeof(RAS_2819T_TEMP_CODES))) {
if (temp_index < 0 || static_cast<size_t>(temp_index) >= sizeof(RAS_2819T_TEMP_CODES)) {
ESP_LOGW(TAG, "Temperature %.1f°C out of range [18-30°C], defaulting to 24°C", temperature);
return 0x40; // Default to 24°C
}

View File

@@ -21,14 +21,14 @@ from esphome.const import (
DEVICE_CLASS_GAS,
DEVICE_CLASS_WATER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import LambdaExpression, MockObjClass
IS_PLATFORM_COMPONENT = True
@@ -43,6 +43,7 @@ DEVICE_CLASSES = [
valve_ns = cg.esphome_ns.namespace("valve")
Valve = valve_ns.class_("Valve", cg.EntityBase)
ValveCall = valve_ns.class_("ValveCall")
VALVE_OPEN = valve_ns.VALVE_OPEN
VALVE_CLOSED = valve_ns.VALVE_CLOSED
@@ -228,17 +229,40 @@ VALVE_CONTROL_ACTION_SCHEMA = cv.Schema(
)
async def valve_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if stop_config := config.get(CONF_STOP):
template_ = await cg.templatable(stop_config, args, cg.bool_)
cg.add(var.set_stop(template_))
if state_config := config.get(CONF_STATE):
template_ = await cg.templatable(state_config, args, cg.float_)
cg.add(var.set_position(template_))
if (position_config := config.get(CONF_POSITION)) is not None:
template_ = await cg.templatable(position_config, args, cg.float_)
cg.add(var.set_position(template_))
return var
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most
# one is present and both dispatch to set_position.
FIELDS = (
(CONF_STOP, "set_stop", cg.bool_),
(CONF_STATE, "set_position", cg.float_),
(CONF_POSITION, "set_position", cg.float_),
)
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for conf_key, setter, type_ in FIELDS:
if (value := config.get(conf_key)) is None:
continue
if isinstance(value, Lambda):
inner = await cg.process_lambda(value, args, return_type=type_)
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
# Match ControlAction::ApplyFn signature: const Ts &... for trigger args.
apply_args = [
(ValveCall.operator("ref"), "call"),
*((t.operator("const").operator("ref"), n) for t, n in args),
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
@coroutine_with_priority(CoroPriority.CORE)

View File

@@ -47,24 +47,25 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
Valve *valve_;
};
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. The action only stores one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `position: !lambda "return x;"`) keep working.
template<typename... Ts> class ControlAction : public Action<Ts...> {
public:
explicit ControlAction(Valve *valve) : valve_(valve) {}
TEMPLATABLE_VALUE(bool, stop)
TEMPLATABLE_VALUE(float, position)
using ApplyFn = void (*)(ValveCall &, const Ts &...);
ControlAction(Valve *valve, ApplyFn apply) : valve_(valve), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->valve_->make_call();
if (this->stop_.has_value())
call.set_stop(this->stop_.value(x...));
if (this->position_.has_value())
call.set_position(this->position_.value(x...));
this->apply_(call, x...);
call.perform();
}
protected:
Valve *valve_;
ApplyFn apply_;
};
template<typename... Ts> class ValveIsOpenCondition : public Condition<Ts...> {

View File

@@ -39,9 +39,18 @@ void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
#ifdef USE_ESP32
esp_task_wdt_config_t wdt_config = {
.timeout_ms = timeout_ms,
.idle_core_mask = (1U << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1U,
.trigger_panic = true,
.idle_core_mask = 0,
.trigger_panic = false,
};
#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0
wdt_config.idle_core_mask |= (1U << 0U);
#endif
#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1
wdt_config.idle_core_mask |= (1U << 1U);
#endif
#if CONFIG_ESP_TASK_WDT_PANIC
wdt_config.trigger_panic = true;
#endif
esp_task_wdt_reconfigure(&wdt_config);
#endif // USE_ESP32

View File

@@ -73,6 +73,7 @@ NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
CONF_SAVE = "save"
CONF_BAND_MODE = "band_mode"
CONF_MIN_AUTH_MODE = "min_auth_mode"
CONF_PHY_MODE = "phy_mode"
CONF_POST_CONNECT_ROAMING = "post_connect_roaming"
# Maximum number of WiFi networks that can be configured
@@ -112,6 +113,14 @@ WIFI_MIN_AUTH_MODES = {
"WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3,
}
VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True)
WiFi8266PhyMode = wifi_ns.enum("WiFi8266PhyMode")
WIFI_8266_PHY_MODES = {
"AUTO": WiFi8266PhyMode.WIFI_8266_PHY_MODE_AUTO,
"11B": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11B,
"11G": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11G,
"11N": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11N,
}
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition)
@@ -406,6 +415,10 @@ CONFIG_SCHEMA = cv.All(
cv.only_on_esp32,
only_on_variant(supported=[const.VARIANT_ESP32C5]),
),
cv.Optional(CONF_PHY_MODE): cv.All(
cv.enum(WIFI_8266_PHY_MODES, upper=True),
cv.only_on_esp8266,
),
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean,
@@ -569,6 +582,9 @@ async def to_code(config):
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
if CONF_PHY_MODE in config:
cg.add_define("USE_WIFI_PHY_MODE")
cg.add(var.set_phy_mode(config[CONF_PHY_MODE]))
elif CORE.is_rp2040:
cg.add_library("WiFi", None)

View File

@@ -309,6 +309,18 @@ bool CompactString::operator==(const StringRef &other) const {
/// └──────────────────────────────────────────────────────────────────────┘
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
#ifdef USE_WIFI_PHY_MODE
// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
static const LogString *phy_mode_to_log_string(WiFi8266PhyMode mode) {
if (mode == WIFI_8266_PHY_MODE_11B)
return LOG_STR("11B");
if (mode == WIFI_8266_PHY_MODE_11G)
return LOG_STR("11G");
if (mode == WIFI_8266_PHY_MODE_11N)
return LOG_STR("11N");
return LOG_STR("Auto");
}
#endif
// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
if (phase == WiFiRetryPhase::INITIAL_CONNECT)
@@ -1535,6 +1547,9 @@ void WiFiComponent::dump_config() {
break;
}
ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s);
#endif
#ifdef USE_WIFI_PHY_MODE
ESP_LOGCONFIG(TAG, " PHY Mode: %s", LOG_STR_ARG(phy_mode_to_log_string(this->phy_mode_)));
#endif
if (this->is_connected()) {
this->print_connect_params_();

View File

@@ -345,6 +345,17 @@ enum WifiMinAuthMode : uint8_t {
WIFI_MIN_AUTH_MODE_WPA3,
};
#ifdef USE_WIFI_PHY_MODE
// Values 1-3 match ESP8266 SDK phy_mode_t (PHY_MODE_11B=1, PHY_MODE_11G=2, PHY_MODE_11N=3).
// AUTO leaves the SDK at its default (no wifi_set_phy_mode() call).
enum WiFi8266PhyMode : uint8_t {
WIFI_8266_PHY_MODE_AUTO = 0,
WIFI_8266_PHY_MODE_11B = 1,
WIFI_8266_PHY_MODE_11G = 2,
WIFI_8266_PHY_MODE_11N = 3,
};
#endif
#ifdef USE_ESP32
struct IDFWiFiEvent;
#endif
@@ -455,6 +466,9 @@ class WiFiComponent final : public Component {
#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
void set_band_mode(wifi_band_mode_t band_mode) { this->band_mode_ = band_mode; }
#endif
#ifdef USE_WIFI_PHY_MODE
void set_phy_mode(WiFi8266PhyMode phy_mode) { this->phy_mode_ = phy_mode; }
#endif
void set_passive_scan(bool passive);
@@ -672,6 +686,9 @@ class WiFiComponent final : public Component {
bool wifi_apply_power_save_();
#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
bool wifi_apply_band_mode_();
#endif
#ifdef USE_WIFI_PHY_MODE
bool wifi_apply_phy_mode_();
#endif
bool wifi_sta_ip_config_(const optional<ManualIP> &manual_ip);
bool wifi_apply_hostname_();
@@ -810,6 +827,9 @@ class WiFiComponent final : public Component {
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO};
#endif
#ifdef USE_WIFI_PHY_MODE
WiFi8266PhyMode phy_mode_{WIFI_8266_PHY_MODE_AUTO};
#endif
WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2};
WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT};

View File

@@ -621,10 +621,25 @@ bool WiFiComponent::wifi_sta_pre_setup_() {
ESP_LOGV(TAG, "Disabling Auto-Connect failed");
}
#ifdef USE_WIFI_PHY_MODE
if (!this->wifi_apply_phy_mode_()) {
ESP_LOGV(TAG, "Setting PHY Mode failed");
}
#endif
delay(10);
return true;
}
#ifdef USE_WIFI_PHY_MODE
bool WiFiComponent::wifi_apply_phy_mode_() {
if (this->phy_mode_ == WIFI_8266_PHY_MODE_AUTO)
return true;
// Values of WiFi8266PhyMode are aligned with the SDK's phy_mode_t enum.
return wifi_set_phy_mode(static_cast<phy_mode_t>(this->phy_mode_));
}
#endif
void WiFiComponent::wifi_pre_setup_() {
wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback);

View File

@@ -25,7 +25,10 @@ from esphome.const import (
CONF_SUBSTITUTIONS,
)
from esphome.core import CORE, DocumentRange, EsphomeError
import esphome.core.config as core_config
# `esphome.core.config` is imported lazily at its two use sites below.
# It pulls in `esphome.automation` and `esphome.config_validation`, which
# dominate `esphome.__main__` startup cost when loaded eagerly here.
import esphome.final_validate as fv
from esphome.helpers import indent
from esphome.loader import ComponentManifest, get_component, get_platform
@@ -968,6 +971,8 @@ class CoreFinalValidateStep(ConfigValidationStep):
if result.errors:
return
import esphome.core.config as core_config
token = fv.full_config.set(result)
with result.catch_error([CONF_ESPHOME]):
if CONF_ESPHOME in result:
@@ -1073,6 +1078,8 @@ def validate_config(
return result
# 2. Load partial core config
import esphome.core.config as core_config
result[CONF_ESPHOME] = config[CONF_ESPHOME]
result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
try:

View File

@@ -178,7 +178,7 @@ class ProjectUpdateTrigger : public Trigger<std::string>, public Component {
};
#endif
template<typename... Ts> class DelayAction : public Action<Ts...>, public Component {
template<typename... Ts> class DelayAction : public Action<Ts...> {
public:
explicit DelayAction() = default;
@@ -198,8 +198,8 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
// to avoid overhead from capturing arguments by value
if constexpr (sizeof...(Ts) == 0) {
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(),
/* component= */ nullptr, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::SELF_POINTER,
/* static_name= */ reinterpret_cast<const char *>(this), /* hash_or_id= */ 0, this->delay_.value(),
[this]() { this->play_next_(); },
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
} else {
@@ -208,18 +208,18 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
// `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...); };
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),
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
App.scheduler.set_timer_common_(
/* component= */ nullptr, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::SELF_POINTER,
/* static_name= */ reinterpret_cast<const char *>(this), /* hash_or_id= */ 0, this->delay_.value(x...),
std::move(f),
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
}
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void play(const Ts &...x) override { /* ignore - see play_complex */
}
void stop() override { this->cancel_timeout(InternalSchedulerID::DELAY_ACTION); }
void stop() override { App.scheduler.cancel_timeout(this); }
};
template<typename... Ts> class LambdaAction : public Action<Ts...> {

View File

@@ -169,7 +169,7 @@ struct Color {
uint8_t r = rand >> 16;
uint8_t g = rand >> 8;
uint8_t b = rand >> 0;
const uint16_t max_rgb = std::max(r, std::max(g, b));
const uint16_t max_rgb = std::max({r, g, b});
return Color(uint8_t((uint16_t(r) * 255U / max_rgb)), uint8_t((uint16_t(g) * 255U / max_rgb)),
uint8_t((uint16_t(b) * 255U / max_rgb)), w);
}

View File

@@ -655,7 +655,15 @@ class WarnIfComponentBlockingGuard {
// Inlined: the fast path is just millis() + subtract + compare
inline uint32_t HOT finish() {
#ifdef USE_RUNTIME_STATS
this->component_->runtime_stats_.record_time(micros() - this->started_us_);
uint32_t elapsed_us = micros() - this->started_us_;
// component_ is nullptr for self-keyed scheduler items (set_timeout/set_interval(self, ...))
if (this->component_ != nullptr) {
this->component_->runtime_stats_.record_time(elapsed_us);
} else {
// Still accumulate into the global counter so Application::loop() can subtract
// this time from before_loop_tasks_ wall time.
ComponentRuntimeStats::global_recorded_us += elapsed_us;
}
#endif
uint32_t curr_time = MillisInternal::get();
#ifndef USE_BENCHMARK

View File

@@ -297,6 +297,7 @@
#define USE_CAPTIVE_PORTAL_GZIP
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_PHY_MODE
#define USE_WIFI_IP_STATE_LISTENERS
#define USE_WIFI_SCAN_RESULTS_LISTENERS
#define USE_WIFI_CONNECT_STATE_LISTENERS

View File

@@ -55,7 +55,7 @@ template<typename ValueType, int MaxBits> struct DefaultBitPolicy {
///
template<typename ValueType, typename BitPolicy = DefaultBitPolicy<ValueType, 16>> class FiniteSetMask {
public:
using bitmask_t = typename BitPolicy::mask_t;
using bitmask_t = BitPolicy::mask_t;
constexpr FiniteSetMask() = default;

View File

@@ -30,11 +30,11 @@
namespace esphome {
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
// Cross-platform declarations. delayMicroseconds(), arch_feed_wdt(),
// arch_get_cpu_cycle_count() vary per platform (some inline, some
// out-of-line) so they live in hal/hal_<platform>.h.
void __attribute__((noreturn)) arch_restart();
void arch_init();
void arch_feed_wdt();
uint32_t arch_get_cpu_cycle_count();
uint32_t arch_get_cpu_freq_hz();
#ifndef USE_ESP8266

View File

@@ -4,6 +4,8 @@
#include <cstdint>
#include <esp_attr.h>
#include <esp_cpu.h>
#include <esp_task_wdt.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
@@ -15,6 +17,11 @@
namespace esphome {
// Forward decl from helpers.h (esphome/core/helpers.h) — kept here so this
// header does not need to pull the rest of helpers.h.
// NOLINTNEXTLINE(readability-redundant-declaration)
void delay_microseconds_safe(uint32_t us);
/// Returns true when executing inside an interrupt handler.
__attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrContext() != 0; }
@@ -30,6 +37,11 @@ __attribute__((always_inline)) inline uint64_t millis_64() {
return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time()));
}
// NOLINTNEXTLINE(readability-identifier-naming)
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
__attribute__((always_inline)) inline void arch_feed_wdt() { esp_task_wdt_reset(); }
__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
} // namespace esphome
#endif // USE_ESP32

View File

@@ -4,6 +4,7 @@
#include <c_types.h>
#include <cstdint>
#include <pgmspace.h>
#include "esphome/core/time_64.h"
@@ -19,8 +20,16 @@ extern "C" unsigned long micros(void);
extern "C" unsigned long millis(void);
// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
// Forward decl from <user_interface.h> for arch_feed_wdt() inline below.
// NOLINTNEXTLINE(readability-redundant-declaration)
extern "C" void system_soft_wdt_feed(void);
namespace esphome {
// Forward decl from helpers.h so this header stays cheap.
// NOLINTNEXTLINE(readability-redundant-declaration)
void delay_microseconds_safe(uint32_t us);
/// Returns true when executing inside an interrupt handler.
/// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
/// non-zero both in a real ISR and when user code masks interrupts. The
@@ -34,10 +43,24 @@ void delay(uint32_t ms);
uint32_t millis();
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
// ESP8266: pgm_read_* does real flash reads on Harvard architecture
uint8_t progmem_read_byte(const uint8_t *addr);
const char *progmem_read_ptr(const char *const *addr);
uint16_t progmem_read_uint16(const uint16_t *addr);
// ESP8266: pgm_read_* does aligned 32-bit flash reads on Harvard architecture.
// Inline-forward to the platform macros so the wrappers themselves don't
// occupy IRAM/flash on every call site.
__attribute__((always_inline)) inline uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
__attribute__((always_inline)) inline const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
__attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
// NOLINTNEXTLINE(readability-identifier-naming)
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
__attribute__((always_inline)) inline void arch_feed_wdt() { system_soft_wdt_feed(); }
uint32_t arch_get_cpu_cycle_count();
} // namespace esphome

View File

@@ -19,6 +19,10 @@ uint32_t micros();
uint32_t millis();
uint64_t millis_64();
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
void arch_feed_wdt();
uint32_t arch_get_cpu_cycle_count();
} // namespace esphome
#endif // USE_HOST

View File

@@ -88,6 +88,10 @@ __attribute__((always_inline)) inline uint32_t millis() { return static_cast<uin
#endif
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
void arch_feed_wdt();
uint32_t arch_get_cpu_cycle_count();
} // namespace esphome
#endif // USE_LIBRETINY

View File

@@ -35,6 +35,10 @@ __attribute__((always_inline)) inline uint32_t micros() { return static_cast<uin
__attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); }
__attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis<uint64_t>(::time_us_64()); }
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
void arch_feed_wdt();
uint32_t arch_get_cpu_cycle_count();
} // namespace esphome
#endif // USE_RP2040

View File

@@ -19,6 +19,10 @@ uint32_t micros();
uint32_t millis();
uint64_t millis_64();
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
void arch_feed_wdt();
uint32_t arch_get_cpu_cycle_count();
} // namespace esphome
#endif // USE_ZEPHYR

View File

@@ -663,8 +663,8 @@ float gamma_uncorrect(float value, float gamma) {
}
void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) {
float max_color_value = std::max(std::max(red, green), blue);
float min_color_value = std::min(std::min(red, green), blue);
float max_color_value = std::max({red, green, blue});
float min_color_value = std::min({red, green, blue});
float delta = max_color_value - min_color_value;
if (delta == 0) {

View File

@@ -69,6 +69,10 @@ template<typename Derived> class PreferencesMixin {
ESPPreferenceObject make_preference(uint32_t type) {
return static_cast<Derived *>(this)->make_preference(sizeof(T), type);
}
private:
PreferencesMixin() = default;
friend Derived;
};
// Macro for platform preferences.h headers to declare the standard aliases.

View File

@@ -35,7 +35,9 @@ static constexpr uint32_t MAX_INTERVAL_DELAY = 5000;
// Uses a stack buffer to avoid heap allocation
// Uses ESPHOME_snprintf_P/ESPHOME_PSTR for ESP8266 to keep format strings in flash
struct SchedulerNameLog {
char buffer[20]; // Enough for "id:4294967295" or "hash:0xFFFFFFFF" or "(null)"
// Sized for the widest formatted output: "self:0x" + 16 hex digits (64-bit pointer) + nul.
// Also covers "id:4294967295", "hash:0xFFFFFFFF", "iid:4294967295", "(null)".
char buffer[28];
// Format a scheduler item name for logging
// Returns pointer to formatted string (either static_name or internal buffer)
@@ -53,9 +55,15 @@ struct SchedulerNameLog {
} else if (name_type == NameType::NUMERIC_ID) {
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("id:%" PRIu32), hash_or_id);
return buffer;
} else { // NUMERIC_ID_INTERNAL
} else if (name_type == NameType::NUMERIC_ID_INTERNAL) {
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("iid:%" PRIu32), hash_or_id);
return buffer;
} else { // SELF_POINTER
// static_name carries the void* key for SELF_POINTER (pointer-width union slot).
// %p is specified as void* (not const void*), so strip const for the varargs call.
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("self:%p"),
const_cast<void *>(static_cast<const void *>(static_name)));
return buffer;
}
}
};
@@ -293,6 +301,27 @@ bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) {
return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL);
}
// Self-keyed scheduler API. The cancellation key is `self` (typically the caller's `this`),
// passed through the existing static_name pointer slot. Matching is by raw pointer equality
// (see matches_item_locked_'s SELF_POINTER branch). No Component pointer is stored, so
// is_failed() skip and component-based log attribution don't apply.
void HOT Scheduler::set_timeout(const void *self, uint32_t timeout, std::function<void()> &&func) {
this->set_timer_common_(nullptr, SchedulerItem::TIMEOUT, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
timeout, std::move(func));
}
void HOT Scheduler::set_interval(const void *self, uint32_t interval, std::function<void()> &&func) {
this->set_timer_common_(nullptr, SchedulerItem::INTERVAL, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
interval, std::move(func));
}
bool HOT Scheduler::cancel_timeout(const void *self) {
return this->cancel_item_(nullptr, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
SchedulerItem::TIMEOUT);
}
bool HOT Scheduler::cancel_interval(const void *self) {
return this->cancel_item_(nullptr, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
SchedulerItem::INTERVAL);
}
// Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation.
// Remove before 2026.8.0 along with all retry code.
#pragma GCC diagnostic push

View File

@@ -146,22 +146,43 @@ class Scheduler {
}
// Name storage type discriminator for SchedulerItem
// Used to distinguish between static strings, hashed strings, numeric IDs, and internal numeric IDs
// Used to distinguish between static strings, hashed strings, numeric IDs, internal numeric IDs,
// and self-keyed pointers (caller-supplied `void *`, typically `this`).
enum class NameType : uint8_t {
STATIC_STRING = 0, // const char* pointer to static/flash storage
HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string
NUMERIC_ID = 2, // uint32_t numeric identifier (component-level)
NUMERIC_ID_INTERNAL = 3 // uint32_t numeric identifier (core/internal, separate namespace)
STATIC_STRING = 0, // const char* pointer to static/flash storage
HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string
NUMERIC_ID = 2, // uint32_t numeric identifier (component-level)
NUMERIC_ID_INTERNAL = 3, // uint32_t numeric identifier (core/internal, separate namespace)
SELF_POINTER = 4 // void* caller-supplied key (typically `this`); pointer equality
};
/** Self-keyed timeout. The cancellation key is `self` (typically the caller's `this`).
*
* Use this when the caller schedules at most one timer of a single purpose at a time and
* does not need a `Component` for `is_failed()` skip or log source attribution. Lets
* small classes drop `Component` inheritance entirely when their only Component dependency
* was the per-instance scheduler key.
*
* NOT applied for self-keyed items:
* - `is_failed()` skip — callbacks always fire (no Component to consult).
* - Log source attribution — logs use a generic "self:0x…" label.
*
* If you need either of those, use the existing `(Component *, id)` overloads.
*/
void set_timeout(const void *self, uint32_t timeout, std::function<void()> &&func);
/// Self-keyed interval. See set_timeout(const void *, ...) for semantics.
void set_interval(const void *self, uint32_t interval, std::function<void()> &&func);
bool cancel_timeout(const void *self);
bool cancel_interval(const void *self);
protected:
struct SchedulerItem {
// Ordered by size to minimize padding
Component *component;
// Optimized name storage using tagged union - zero heap allocation
union {
const char *static_name; // For STATIC_STRING (string literals, no allocation)
uint32_t hash_or_id; // For HASHED_STRING or NUMERIC_ID
const char *static_name; // For STATIC_STRING (string literals) and SELF_POINTER (caller's `this`)
uint32_t hash_or_id; // For HASHED_STRING, NUMERIC_ID, and NUMERIC_ID_INTERNAL
} name_;
uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
@@ -182,19 +203,19 @@ class Scheduler {
// std::atomic<uint8_t> inlines correctly on all platforms.
std::atomic<uint8_t> remove{0};
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
NameType name_type_ : 2; // Discriminator for name_ union (03, see NameType enum)
bool is_retry : 1; // True if this is a retry timeout
// 4 bits padding
#else
// Single-threaded or multi-threaded without atomics: can pack all fields together
// Bit-packed fields (5 bits used, 3 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool remove : 1;
NameType name_type_ : 2; // Discriminator for name_ union (03, see NameType enum)
NameType name_type_ : 3; // Discriminator for name_ union (04, see NameType enum)
bool is_retry : 1; // True if this is a retry timeout
// 3 bits padding
#else
// Single-threaded or multi-threaded without atomics: can pack all fields together
// Bit-packed fields (6 bits used, 2 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool remove : 1;
NameType name_type_ : 3; // Discriminator for name_ union (04, see NameType enum)
bool is_retry : 1; // True if this is a retry timeout
// 2 bits padding
#endif
// Constructor
@@ -228,19 +249,26 @@ class Scheduler {
SchedulerItem(SchedulerItem &&) = delete;
SchedulerItem &operator=(SchedulerItem &&) = delete;
// Helper to get the static name (only valid for STATIC_STRING type)
const char *get_name() const { return (name_type_ == NameType::STATIC_STRING) ? name_.static_name : nullptr; }
// Helper to get the pointer-slot value (valid for STATIC_STRING and SELF_POINTER types).
// Both share the same union member, so callers (e.g. log formatters) can read either uniformly.
const char *get_name() const {
return (name_type_ == NameType::STATIC_STRING || name_type_ == NameType::SELF_POINTER) ? name_.static_name
: nullptr;
}
// Helper to get the hash or numeric ID (only valid for HASHED_STRING or NUMERIC_ID types)
uint32_t get_name_hash_or_id() const { return (name_type_ != NameType::STATIC_STRING) ? name_.hash_or_id : 0; }
// Helper to get the hash or numeric ID (only valid for HASHED_STRING / NUMERIC_ID / NUMERIC_ID_INTERNAL types)
uint32_t get_name_hash_or_id() const {
return (name_type_ != NameType::STATIC_STRING && name_type_ != NameType::SELF_POINTER) ? name_.hash_or_id : 0;
}
// Helper to get the name type
NameType get_name_type() const { return name_type_; }
// Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id.
// Both union members occupy the same offset, so only one store is needed.
// Set name storage. STATIC_STRING/SELF_POINTER use the static_name pointer slot
// (both are pointer-width); other types use hash_or_id. Both union members occupy
// the same offset, so only one store is needed.
void set_name(NameType type, const char *static_name, uint32_t hash_or_id) {
if (type == NameType::STATIC_STRING) {
if (type == NameType::STATIC_STRING || type == NameType::SELF_POINTER) {
name_.static_name = static_name;
} else {
name_.hash_or_id = hash_or_id;
@@ -367,10 +395,14 @@ class Scheduler {
// Name type must match
if (item->get_name_type() != name_type)
return false;
// For static strings, compare the string content; for hash/ID, compare the value
// STATIC_STRING: compare string content. SELF_POINTER: raw pointer equality (no strcmp).
// Other types: compare hash/ID value.
if (name_type == NameType::STATIC_STRING) {
return this->names_match_static_(item->get_name(), static_name);
}
if (name_type == NameType::SELF_POINTER) {
return item->name_.static_name == static_name;
}
return item->get_name_hash_or_id() == hash_or_id;
}

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
from collections.abc import Callable, Iterable
from concurrent.futures import ThreadPoolExecutor
import contextlib
from datetime import UTC, datetime
import logging
@@ -9,9 +11,10 @@ from pathlib import Path
import requests
import esphome.config_validation as cv
from esphome.const import __version__
from esphome.const import CONF_FILE, CONF_TYPE, CONF_URL, __version__
from esphome.core import CORE, EsphomeError, TimePeriodSeconds
from esphome.helpers import write_file
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@landonr"]
@@ -85,7 +88,9 @@ def _write_etag(local_file_path: Path, etag: str | None) -> None:
)
def has_remote_file_changed(url: str, local_file_path: Path) -> bool:
def has_remote_file_changed(
url: str, local_file_path: Path, timeout: int = NETWORK_TIMEOUT
) -> bool:
if local_file_path.exists():
_LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path)
try:
@@ -101,7 +106,7 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool:
if etag := _read_etag(local_file_path):
headers[IF_NONE_MATCH] = etag
response = requests.head(
url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True
url, headers=headers, timeout=timeout, allow_redirects=True
)
_LOGGER.debug(
@@ -153,7 +158,7 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by
if CORE.skip_external_update and path.exists():
_LOGGER.debug("Skipping update for %s (refresh disabled)", url)
return path.read_bytes()
if not has_remote_file_changed(url, path):
if not has_remote_file_changed(url, path, timeout):
_LOGGER.debug("Remote file has not changed %s", url)
return path.read_bytes()
@@ -170,6 +175,11 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
)
req.raise_for_status()
# `.content` reads the body lazily; chunked-decode, gzip-decode,
# and mid-stream connection errors all surface here as
# RequestException subclasses, so this needs the same fall-back
# treatment as the request itself.
data = req.content
except requests.exceptions.RequestException as e:
if path.exists():
_LOGGER.warning(
@@ -180,7 +190,91 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by
return path.read_bytes()
raise cv.Invalid(f"Could not download from {url}: {e}") from e
data = req.content
write_file(path, data)
_write_etag(path, req.headers.get(ETAG))
return data
# Cap concurrent connections so a config with hundreds of remote files doesn't
# open hundreds of sockets at once. 8 matches the requests connection-pool
# default and the per-host connection limit browsers use, which keeps us
# polite to the upstream host while still cutting wall time roughly 8x for
# typical configs (a couple dozen files).
DEFAULT_DOWNLOAD_WORKERS = 8
def download_content_many(
items: Iterable[tuple[str, Path]],
timeout: int = NETWORK_TIMEOUT,
max_workers: int = DEFAULT_DOWNLOAD_WORKERS,
) -> None:
"""Run `download_content` for each (url, path) pair concurrently.
Wall time drops from `sum(latency)` to roughly `max(latency)` for cached
files where the HEAD round-trip dominates. All workers run to
completion before this returns; every `cv.Invalid` raised by a worker
is collected and surfaced together as `cv.MultipleInvalid` so the user
sees every broken file in a single validation pass instead of fixing
them one round-trip at a time.
Items are de-duplicated by `path` -- two callers asking for the same
cache file (e.g. the same URL referenced twice in a config) would
otherwise race on `download_content`'s non-atomic write. When the
same `path` appears more than once, the last URL wins (standard dict
comprehension semantics); in practice duplicate paths only arise when
the URL is duplicated, so the choice doesn't matter.
"""
seen: dict[Path, str] = {path: url for url, path in items}
if not seen:
return
if len(seen) == 1:
path, url = next(iter(seen.items()))
download_content(url, path, timeout)
return
def _download_one(path_url: tuple[Path, str]) -> None:
# `seen` stores entries as (path, url) so the dict can dedupe by
# path; flip them back to download_content's (url, path) order.
path, url = path_url
download_content(url, path, timeout)
workers = max(1, min(max_workers, len(seen)))
errors: list[cv.Invalid] = []
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = [ex.submit(_download_one, item) for item in seen.items()]
for future in futures:
try:
future.result()
except cv.Invalid as e:
errors.append(e)
if not errors:
return
if len(errors) == 1:
raise errors[0]
raise cv.MultipleInvalid(errors)
# Each component that uses external_files defines its own local
# `TYPE_WEB = "web"`; the string is repeated here rather than imported
# because there is no canonical `TYPE_WEB` in `esphome.const` to share.
WEB_TYPE = "web"
def download_web_files_in_config(
config: list[ConfigType],
path_for: Callable[[ConfigType], Path],
) -> list[ConfigType]:
"""Voluptuous-friendly validator that downloads any web-sourced files in
`config` in parallel.
Each entry is expected to contain a `file` key whose value is a dict
that may be `{type: "web", url: ...}`; `path_for(file_dict)` returns
the cache path for that file. Returns `config` unchanged so it can be
slotted directly into a `cv.All(...)` chain.
"""
download_content_many(
(conf_file[CONF_URL], path_for(conf_file))
for entry in config
if (conf_file := entry.get(CONF_FILE, {})).get(CONF_TYPE) == WEB_TYPE
)
return config

View File

@@ -9,14 +9,23 @@ import logging
from pathlib import Path
import sys
from types import ModuleType
from typing import Any
from typing import TYPE_CHECKING, Any
from esphome.const import SOURCE_FILE_EXTENSIONS
from esphome.core import CORE
import esphome.core.config
from esphome.cpp_generator import MockObjClass
from esphome.types import ConfigType
if TYPE_CHECKING:
from esphome.cpp_generator import MockObjClass
# `esphome.core.config` is imported lazily in `_lookup_module` when the
# "esphome" pseudo-component is first resolved. It pulls in
# `esphome.automation` and `esphome.config_validation`, which together
# dominate `esphome.__main__` startup cost when loaded eagerly.
# `esphome.cpp_generator` is similarly avoided at module scope; it pulls
# in `esphome.yaml_util` and is only needed for the `MockObjClass` type
# annotation, which is resolved lazily via `TYPE_CHECKING`.
_LOGGER = logging.getLogger(__name__)
@@ -94,7 +103,7 @@ class ComponentManifest:
return getattr(self.module, "CODEOWNERS", [])
@property
def instance_type(self) -> MockObjClass | None:
def instance_type(self) -> "MockObjClass | None":
return getattr(self.module, "INSTANCE_TYPE", None)
@property
@@ -213,6 +222,13 @@ def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None:
if domain in _COMPONENT_CACHE:
return _COMPONENT_CACHE[domain]
if domain == "esphome":
import esphome.core.config
manif = ComponentManifest(esphome.core.config, recursive_sources=True)
_COMPONENT_CACHE[domain] = manif
return manif
try:
module = importlib.import_module(f"esphome.components.{domain}")
except ImportError as e:
@@ -248,9 +264,6 @@ def get_platform(domain: str, platform: str) -> ComponentManifest | None:
_COMPONENT_CACHE: dict[str, ComponentManifest] = {}
CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve()
_COMPONENT_CACHE["esphome"] = ComponentManifest(
esphome.core.config, recursive_sources=True
)
def _replace_component_manifest(domain: str, manifest: ComponentManifest) -> None:

View File

@@ -1,4 +1,4 @@
# Useful stuff when working in a development environment
clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating
clang-tidy==18.1.8 # When updating clang-tidy, also update Dockerfile
clang-tidy==22.1.0.1
yamllint==1.38.0 # also change in .pre-commit-config.yaml when updating

View File

@@ -57,6 +57,59 @@ def hash_components(components: list[str]) -> str:
return hashlib.sha256(key.encode()).hexdigest()[:16]
def populate_dependency_config(
config: dict,
component_names: list[str],
*,
get_component_fn: Callable[[str], object | None] = get_component,
register_platform_fn: Callable[[str], None] | None = None,
) -> None:
"""Populate ``config`` with empty entries for transitive dependencies.
For every name in ``component_names``:
* ``domain.platform`` form (e.g. ``sensor.gpio``) appends
``{platform: <name>}`` to ``config[domain]``, creating the list if needed.
* Bare components are looked up via ``get_component_fn``. Platform
components (``IS_PLATFORM_COMPONENT``) and ``MULTI_CONF`` components are
initialised as ``[]`` so the sibling ``domain.platform`` branch can
``append`` into them. Everything else is populated by running the
component's schema with ``{}`` so defaults exist; if the schema requires
explicit input, an empty ``{}`` is used as a fallback.
Platform components must always be a list here even when no
``domain.platform`` entry follows, because the ``domain.platform`` branch
does ``config.setdefault(domain, []).append(...)`` and would crash on a
leftover dict.
"""
if register_platform_fn is None:
register_platform_fn = CORE.testing_ensure_platform_registered
for component_name in component_names:
if "." in component_name:
domain, component = component_name.split(".", maxsplit=1)
domain_list = config.setdefault(domain, [])
register_platform_fn(domain)
domain_list.append({CONF_PLATFORM: component})
continue
# Skip "core" — it's a pseudo-component handled by the build
# system, not a real loadable component (get_component returns None)
component = get_component_fn(component_name)
if component is None:
continue
if component.multi_conf or component.is_platform_component:
config.setdefault(component_name, [])
elif component_name not in config:
schema = component.config_schema
try:
config[component_name] = schema({}) if schema is not None else {}
except Exception: # noqa: BLE001
# Schema requires explicit input we can't synthesize; fall
# back to an empty mapping so subscripting at least returns
# KeyError on missing keys rather than crashing on the
# wrong type.
config[component_name] = {}
def filter_components_with_files(components: list[str], tests_dir: Path) -> list[str]:
"""Filter out components that do not have .cpp or .h files in the tests dir.
@@ -316,31 +369,7 @@ def compile_and_get_binary(
# Add remaining components and dependencies to the configuration after
# validation, so their source files are included in the build.
for component_name in components_with_dependencies:
if "." in component_name:
domain, component = component_name.split(".", maxsplit=1)
domain_list = config.setdefault(domain, [])
CORE.testing_ensure_platform_registered(domain)
domain_list.append({CONF_PLATFORM: component})
# Skip "core" — it's a pseudo-component handled by the build
# system, not a real loadable component (get_component returns None)
elif (component := get_component(component_name)) is not None:
# MULTI_CONF components store their config as a list of dicts,
# everything else stores a single dict. Run the component's
# schema with {} so defaults get populated -- code paths like
# socket.FILTER_SOURCE_FILES expect a fully-populated mapping.
if component.multi_conf:
config.setdefault(component_name, [])
elif component_name not in config:
schema = component.config_schema
try:
config[component_name] = schema({}) if schema is not None else {}
except Exception: # noqa: BLE001
# Schema requires explicit input we can't synthesize; fall
# back to an empty mapping so subscripting at least returns
# KeyError on missing keys rather than crashing on the
# wrong type.
config[component_name] = {}
populate_dependency_config(config, components_with_dependencies)
# Register platforms from the extra config (benchmark.yaml) so
# USE_SENSOR, USE_LIGHT, etc. defines are emitted without needing

View File

@@ -295,7 +295,7 @@ def main():
failed_files = []
try:
executable = get_binary("clang-tidy", 18)
executable = get_binary("clang-tidy", 22)
task_queue = queue.Queue(args.jobs)
lock = threading.Lock()
for _ in range(args.jobs):
@@ -341,13 +341,13 @@ def main():
try:
try:
subprocess.call(
["clang-apply-replacements-18", tmpdir], close_fds=False
["clang-apply-replacements-22", tmpdir], close_fds=False
)
except FileNotFoundError:
subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False)
except FileNotFoundError:
print(
"Error please install clang-apply-replacements-18 or clang-apply-replacements.\n",
"Error please install clang-apply-replacements-22 or clang-apply-replacements.\n",
file=sys.stderr,
)
except:

View File

@@ -6,8 +6,7 @@ what files have changed. It outputs JSON with the following structure:
{
"integration_tests": true/false,
"integration_tests_run_all": true/false,
"integration_test_files": ["tests/integration/test_foo.py", ...],
"integration_test_buckets": [{"name": "1/3", "tests": ["tests/integration/test_foo.py", ...]}, ...],
"clang_tidy": true/false,
"clang_format": true/false,
"python_linters": true/false,
@@ -81,6 +80,62 @@ CLANG_TIDY_SPLIT_THRESHOLD = 65
# Isolated components count as 10x, groupable components count as 1x
COMPONENT_TEST_BATCH_SIZE = 40
# Integration test bucketing: when more than the threshold tests are scheduled,
# fan out across this many parallel jobs. Below the threshold, a single job runs.
INTEGRATION_TESTS_SPLIT_THRESHOLD = 10
INTEGRATION_TESTS_SPLIT_BUCKETS = 3
def _split_list(items: list[str], n: int) -> list[list[str]]:
"""Split a list into n roughly-equal contiguous parts (matches script/clang-tidy)."""
k, m = divmod(len(items), n)
return [items[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n)]
def _all_integration_test_files() -> list[str]:
"""Return all integration test file paths, sorted, relative to repo root."""
return sorted(
str(p.relative_to(root_path))
for p in (Path(root_path) / "tests" / "integration").glob("test_*.py")
)
def _compute_integration_test_buckets(
integration_run_all: bool,
integration_test_files: list[str],
) -> tuple[bool, list[dict[str, Any]]]:
"""Compute (run_integration, buckets) from the determine_integration_tests result.
Pure function for unit testing — no I/O beyond `_all_integration_test_files`
when `integration_run_all` is set.
`buckets` is a list of `{name, tests}` dicts where `tests` is a JSON-friendly
list of file paths so the workflow can build a bash array via jq, avoiding
shell word-splitting / glob hazards.
"""
if integration_run_all:
files = _all_integration_test_files()
else:
files = sorted(integration_test_files)
# Empty list (e.g. run_all expansion with no files on disk) would otherwise
# cause the workflow to invoke pytest with no path argument and collect
# tests outside tests/integration/. Suppress the run instead.
if not files:
return False, []
if len(files) > INTEGRATION_TESTS_SPLIT_THRESHOLD:
parts = [
part for part in _split_list(files, INTEGRATION_TESTS_SPLIT_BUCKETS) if part
]
buckets = [
{"name": f"{i + 1}/{len(parts)}", "tests": part}
for i, part in enumerate(parts)
]
else:
buckets = [{"name": "1/1", "tests": files}]
return True, buckets
class Platform(StrEnum):
"""Platform identifiers for memory impact analysis."""
@@ -812,7 +867,9 @@ def main() -> None:
integration_run_all, integration_test_files = determine_integration_tests(
args.branch
)
run_integration = integration_run_all or bool(integration_test_files)
run_integration, integration_test_buckets = _compute_integration_test_buckets(
integration_run_all, integration_test_files
)
run_clang_tidy = should_run_clang_tidy(args.branch)
run_clang_format = should_run_clang_format(args.branch)
run_python_linters = should_run_python_linters(args.branch)
@@ -944,8 +1001,7 @@ def main() -> None:
output: dict[str, Any] = {
"integration_tests": run_integration,
"integration_tests_run_all": integration_run_all,
"integration_test_files": integration_test_files,
"integration_test_buckets": integration_test_buckets,
"clang_tidy": run_clang_tidy,
"clang_tidy_mode": clang_tidy_mode,
"clang_format": run_clang_format,

View File

@@ -1,5 +1,5 @@
{
"target_module": "esphome.__main__",
"margin_pct": 15,
"cumulative_us": 123000
"cumulative_us": 91000
}

View File

@@ -112,7 +112,7 @@ TEST(ProtoMacVarint, AllOnes) { verify_mac(0xFFFFFFFFFFFFULL, 7); } // F
// 100 deterministic-random 48-bit MACs to catch regressions across the space.
TEST(ProtoMacVarint, RandomSample) {
// NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp) -- intentional fixed seed for reproducibility.
// NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) -- fixed seed for reproducibility
std::mt19937_64 rng(0xC0FFEE);
for (int i = 0; i < 100; i++) {
uint64_t mac = rng() & 0xFFFFFFFFFFFFULL;

View File

@@ -29,3 +29,59 @@ climate:
heat_action:
- switch.turn_on: climate_heater_switch
- switch.turn_off: climate_cooler_switch
# Thermostat-based climate so climate.control: action variants get build
# coverage (bang_bang doesn't support fan modes, presets, etc.). Climate
# has no template platform, so thermostat is the right vehicle.
- platform: thermostat
id: climate_test_thermostat
name: Test Thermostat
sensor: climate_temperature_sensor
min_idle_time: 30s
min_heating_off_time: 300s
min_heating_run_time: 300s
min_cooling_off_time: 300s
min_cooling_run_time: 300s
heat_action:
- logger.log: heating
idle_action:
- logger.log: idle
cool_action:
- logger.log: cooling
auto_mode:
- logger.log: auto
heat_cool_mode:
- logger.log: heat_cool
preset:
- name: Default
default_target_temperature_low: 18°C
default_target_temperature_high: 22°C
button:
# Exercise the climate.control: action so ControlAction templates get
# build coverage. Various field combinations are tested.
- platform: template
name: "Climate Control Mode"
on_press:
- climate.control:
id: climate_test_thermostat
mode: HEAT
- platform: template
name: "Climate Control Mode And Temps"
on_press:
- climate.control:
id: climate_test_thermostat
mode: HEAT_COOL
target_temperature_low: 19.0°C
target_temperature_high: 23.0°C
- platform: template
name: "Climate Control Lambda Temp"
on_press:
- climate.control:
id: climate_test_thermostat
target_temperature_high: !lambda "return 21.5;"
- platform: template
name: "Climate Control Off"
on_press:
- climate.control:
id: climate_test_thermostat
mode: "OFF"

View File

@@ -57,3 +57,34 @@ binary_sensor:
return true;
}
return false;
# Exercise fan.turn_on with various field combinations so the
# TurnOnAction codegen paths get build coverage.
button:
- platform: template
name: "Fan Speed Only"
on_press:
- fan.turn_on:
id: test_fan
speed: 2
- platform: template
name: "Fan Oscillating + Direction"
on_press:
- fan.turn_on:
id: test_fan
oscillating: true
direction: REVERSE
- platform: template
name: "Fan All Fields"
on_press:
- fan.turn_on:
id: test_fan
oscillating: false
speed: 3
direction: FORWARD
- platform: template
name: "Fan Lambda Speed"
on_press:
- fan.turn_on:
id: test_fan
speed: !lambda 'return 1;'

View File

@@ -108,6 +108,10 @@ esphome:
relative_brightness: 5%
brightness_limits:
max_brightness: 90%
- light.dim_relative:
id: test_monochromatic_light
relative_brightness: -5%
transition_length: 250ms
- light.turn_on:
id: test_addressable_transition
brightness: 50%

View File

@@ -293,6 +293,60 @@ cover:
cover.is_closed: template_cover_with_triggers
then:
logger.log: Cover is closed
# Exercise cover.control / cover.template.publish action variants so they
# get build coverage in CI (and so memory-impact analysis on PRs that
# touch ControlAction / CoverPublishAction sees real instances).
- platform: template
name: "Template Cover Actions"
id: template_cover_actions
has_position: true
optimistic: true
open_action:
# CONF_STATE alias for the position bit
- cover.template.publish:
id: template_cover_actions
state: OPEN
- cover.template.publish:
id: template_cover_actions
position: 1.0
- cover.template.publish:
id: template_cover_actions
current_operation: IDLE
close_action:
- cover.template.publish:
id: template_cover_actions
position: 0.0
tilt: 0.0
stop_action:
- cover.template.publish:
id: template_cover_actions
current_operation: IDLE
tilt_action:
- lambda: |-
id(template_cover_actions).tilt = tilt;
id(template_cover_actions).publish_state();
on_idle:
# position only
- cover.control:
id: template_cover_actions
position: 50%
# tilt only
- cover.control:
id: template_cover_actions
tilt: 75%
# position + tilt
- cover.control:
id: template_cover_actions
position: 25%
tilt: 30%
# stop
- cover.control:
id: template_cover_actions
stop: true
# CONF_STATE alias for position
- cover.control:
id: template_cover_actions
state: OPEN
number:
- platform: template
@@ -388,6 +442,20 @@ valve:
state: CLOSED
stop_action:
- logger.log: stop_action
# Exercise valve.control with various field combinations so the
# ControlAction codegen paths get build coverage.
- valve.control:
id: template_valve
stop: true
- valve.control:
id: template_valve
position: 50%
- valve.control:
id: template_valve
state: OPEN
- valve.control:
id: template_valve
position: !lambda 'return 0.25f;'
optimistic: true
text:

View File

@@ -1,6 +1,7 @@
wifi:
min_auth_mode: WPA2
post_connect_roaming: true
phy_mode: 11G
packages:
- !include common.yaml

View File

@@ -0,0 +1,59 @@
esphome:
name: fan-turn-on-action-test
host:
api:
logger:
level: DEBUG
globals:
- id: test_speed
type: int
initial_value: "2"
fan:
- platform: template
id: test_fan
name: "Test Fan"
has_oscillating: true
has_direction: true
speed_count: 5
button:
# fan.turn_on: speed only
- platform: template
id: btn_speed
name: "Set Speed"
on_press:
- fan.turn_on:
id: test_fan
speed: 3
# fan.turn_on: oscillating + direction (no speed)
- platform: template
id: btn_oscillate_direction
name: "Set Oscillate Direction"
on_press:
- fan.turn_on:
id: test_fan
oscillating: true
direction: REVERSE
# fan.turn_on: all three fields
- platform: template
id: btn_all_fields
name: "Set All Fields"
on_press:
- fan.turn_on:
id: test_fan
oscillating: false
speed: 4
direction: FORWARD
# fan.turn_on: lambda for speed (exercises lambda path)
- platform: template
id: btn_lambda_speed
name: "Lambda Speed"
on_press:
- fan.turn_on:
id: test_fan
speed: !lambda "return id(test_speed);"

View File

@@ -0,0 +1,60 @@
esphome:
name: light-dim-relative-action-test
host:
api:
logger:
level: DEBUG
output:
- platform: template
id: test_out
type: float
write_action:
- lambda: ""
light:
- platform: monochromatic
name: "Test Light"
id: test_light
output: test_out
default_transition_length: 0s
button:
# Set up: turn on at 50% brightness
- platform: template
id: btn_setup
name: "Setup"
on_press:
- light.turn_on:
id: test_light
brightness: 50%
# Test 1: dim_relative without transition_length (HasTransitionLength=false)
- platform: template
id: btn_dim_up
name: "Dim Up"
on_press:
- light.dim_relative:
id: test_light
relative_brightness: 25%
# Test 2: dim_relative with transition_length (HasTransitionLength=true)
- platform: template
id: btn_dim_down
name: "Dim Down"
on_press:
- light.dim_relative:
id: test_light
relative_brightness: -10%
transition_length: 0s
# Test 3: dim_relative with brightness limits
- platform: template
id: btn_dim_clamp
name: "Dim Clamp"
on_press:
- light.dim_relative:
id: test_light
relative_brightness: 50%
brightness_limits:
max_brightness: 80%

View File

@@ -0,0 +1,37 @@
esphome:
name: light-toggle-action-test
host:
api:
logger:
level: DEBUG
output:
- platform: template
id: test_out
type: float
write_action:
- lambda: ""
light:
- platform: monochromatic
name: "Test Light"
id: test_light
output: test_out
default_transition_length: 0s
button:
# Test 1: light.toggle without transition_length (HasTransitionLength=false)
- platform: template
id: btn_toggle
name: "Toggle"
on_press:
- light.toggle: test_light
# Test 2: light.toggle with transition_length (HasTransitionLength=true)
- platform: template
id: btn_toggle_with_trans
name: "Toggle With Trans"
on_press:
- light.toggle:
id: test_light
transition_length: 0s

View File

@@ -0,0 +1,112 @@
esphome:
debug_scheduler: true # Enable scheduler leak detection
name: scheduler-self-keyed-test
on_boot:
priority: -100
then:
- logger.log: "Starting scheduler self-keyed tests"
host:
api:
logger:
level: VERBOSE
globals:
- id: tests_done
type: bool
initial_value: 'false'
script:
- id: test_self_keyed
then:
- logger.log: "Testing self-keyed scheduler API"
- lambda: |-
// Two distinct keys backed by addresses of static markers — they
// must not collide even though both are self-keyed and share no
// Component pointer. Static storage gives them stable, unique
// addresses for the lifetime of the program.
static int key_a_marker = 0;
static int key_b_marker = 0;
void *key_a = &key_a_marker;
void *key_b = &key_b_marker;
// ---- Test 1: Self-keyed timeout fires ----
App.scheduler.set_timeout(key_a, 50, []() {
ESP_LOGI("test", "Self timeout A fired");
});
// ---- Test 2: Self-keyed cancel cancels only that key ----
App.scheduler.set_timeout(key_b, 100, []() {
ESP_LOGE("test", "ERROR: Self timeout B should have been cancelled");
});
App.scheduler.cancel_timeout(key_b);
// ---- Test 3: Two independent self keys don't collide ----
// Using fresh static markers so neither matches key_a / key_b.
static int key_c_marker = 0;
static int key_d_marker = 0;
void *key_c = &key_c_marker;
void *key_d = &key_d_marker;
App.scheduler.set_timeout(key_c, 150, []() {
ESP_LOGI("test", "Self timeout C fired");
});
App.scheduler.set_timeout(key_d, 150, []() {
ESP_LOGI("test", "Self timeout D fired");
});
// ---- Test 4: Self-keyed and component-keyed don't collide ----
// Use a self pointer that happens to look like a Component-attached id.
// The scheduler must treat them as separate namespaces.
static int shared_marker = 0;
void *self_shared = &shared_marker;
App.scheduler.set_timeout(self_shared, 200, []() {
ESP_LOGI("test", "Self timeout shared fired");
});
App.scheduler.set_timeout(id(test_sensor), 7777U, 200, []() {
ESP_LOGI("test", "Component timeout 7777 fired");
});
// ---- Test 5: Self-keyed interval fires multiple times then cancels ----
static int interval_count = 0;
static int key_e_marker = 0;
void *key_e = &key_e_marker;
App.scheduler.set_interval(key_e, 80, [key_e]() {
interval_count++;
if (interval_count == 2) {
ESP_LOGI("test", "Self interval E fired twice");
App.scheduler.cancel_interval(key_e);
}
});
// ---- Test 6: Re-registering same self-key replaces the timer ----
// The old timer must NOT fire; only the new one does.
static int key_f_marker = 0;
void *key_f = &key_f_marker;
App.scheduler.set_timeout(key_f, 250, []() {
ESP_LOGE("test", "ERROR: Self timeout F first registration should have been replaced");
});
App.scheduler.set_timeout(key_f, 300, []() {
ESP_LOGI("test", "Self timeout F replacement fired");
});
// Log completion after all timers should have fired
App.scheduler.set_timeout(id(test_sensor), 9999U, 1500, []() {
ESP_LOGI("test", "All self-keyed tests complete");
});
sensor:
- platform: template
name: Test Sensor
id: test_sensor
lambda: return 1.0;
update_interval: never
interval:
- interval: 0.1s
then:
- if:
condition:
lambda: 'return id(tests_done) == false;'
then:
- lambda: 'id(tests_done) = true;'
- script.execute: test_self_keyed

View File

@@ -0,0 +1,69 @@
esphome:
name: valve-control-action-test
host:
api:
logger:
level: DEBUG
globals:
- id: test_position
type: float
initial_value: "0.42"
valve:
- platform: template
name: "Test Valve"
id: test_valve
has_position: true
optimistic: true
assumed_state: true
open_action:
- valve.template.publish:
id: test_valve
position: 1.0
close_action:
- valve.template.publish:
id: test_valve
position: 0.0
stop_action:
- valve.template.publish:
id: test_valve
current_operation: IDLE
button:
# valve.control: position only
- platform: template
id: btn_position
name: "Set Position"
on_press:
- valve.control:
id: test_valve
position: 50%
# valve.control: state alias for position 1.0
- platform: template
id: btn_open_state
name: "Open State"
on_press:
- valve.control:
id: test_valve
state: OPEN
# valve.control: lambda position (exercises lambda path)
- platform: template
id: btn_lambda_position
name: "Lambda Position"
on_press:
- valve.control:
id: test_valve
position: !lambda "return id(test_position);"
# valve.control: stop only — template valve's stop_action publishes
# current_operation: IDLE.
- platform: template
id: btn_stop
name: "Stop Valve"
on_press:
- valve.control:
id: test_valve
stop: true

View File

@@ -0,0 +1,75 @@
"""Integration test for fan TurnOnAction.
Tests that fan.turn_on automation actions work correctly across multiple
field combinations and the lambda path.
"""
from __future__ import annotations
import asyncio
from aioesphomeapi import ButtonInfo, EntityState, FanDirection, FanInfo, FanState
import pytest
from .state_utils import InitialStateHelper, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_fan_turn_on_action(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test fan TurnOnAction with constants and a lambda."""
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client:
fan_state_future: asyncio.Future[FanState] | None = None
def on_state(state: EntityState) -> None:
if (
isinstance(state, FanState)
and fan_state_future is not None
and not fan_state_future.done()
):
fan_state_future.set_result(state)
async def wait_for_fan_state(timeout: float = 5.0) -> FanState:
nonlocal fan_state_future
fan_state_future = loop.create_future()
try:
return await asyncio.wait_for(fan_state_future, timeout)
finally:
fan_state_future = None
entities, _ = await client.list_entities_services()
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
await initial_state_helper.wait_for_initial_states()
require_entity(entities, "test_fan", FanInfo)
async def press_and_wait(name: str) -> FanState:
btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo)
client.button_command(btn.key)
return await wait_for_fan_state()
# speed only
state = await press_and_wait("Set Speed")
assert state.state is True
assert state.speed_level == 3
# oscillating + direction
state = await press_and_wait("Set Oscillate Direction")
assert state.oscillating is True
assert state.direction == FanDirection.REVERSE
# all three fields
state = await press_and_wait("Set All Fields")
assert state.oscillating is False
assert state.speed_level == 4
assert state.direction == FanDirection.FORWARD
# lambda path: speed computed at runtime (test_speed global = 2)
state = await press_and_wait("Lambda Speed")
assert state.speed_level == 2

View File

@@ -0,0 +1,72 @@
"""Integration test for light::DimRelativeAction.
Tests both DimRelativeAction<HasTransitionLength=false> and
DimRelativeAction<HasTransitionLength=true> instantiations.
"""
from __future__ import annotations
import asyncio
from aioesphomeapi import ButtonInfo, EntityState, LightInfo, LightState
import pytest
from .state_utils import InitialStateHelper, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_light_dim_relative_action(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test light.dim_relative with and without transition_length."""
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client:
light_state_future: asyncio.Future[LightState] | None = None
def on_state(state: EntityState) -> None:
if (
isinstance(state, LightState)
and light_state_future is not None
and not light_state_future.done()
):
light_state_future.set_result(state)
async def wait_for_light_state(timeout: float = 5.0) -> LightState:
nonlocal light_state_future
light_state_future = loop.create_future()
try:
return await asyncio.wait_for(light_state_future, timeout)
finally:
light_state_future = None
entities, _ = await client.list_entities_services()
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
await initial_state_helper.wait_for_initial_states()
require_entity(entities, "test_light", LightInfo)
async def press_and_wait(name: str) -> LightState:
btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo)
client.button_command(btn.key)
return await wait_for_light_state()
# Setup: turn on at 50%
state = await press_and_wait("Setup")
assert state.state is True
assert state.brightness == pytest.approx(0.5, abs=0.05)
# Test 1: dim_relative without transition_length: 50% + 25% = 75%
state = await press_and_wait("Dim Up")
assert state.brightness == pytest.approx(0.75, abs=0.05)
# Test 2: dim_relative with transition_length: 75% - 10% = 65%
state = await press_and_wait("Dim Down")
assert state.brightness == pytest.approx(0.65, abs=0.05)
# Test 3: dim_relative with max_brightness limit: 65% + 50% clamped to 80%
state = await press_and_wait("Dim Clamp")
assert state.brightness == pytest.approx(0.80, abs=0.05)

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