mirror of
https://github.com/esphome/esphome.git
synced 2026-06-28 15:24:51 +00:00
Compare commits
7 Commits
dnm_test_c
...
split-hal-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36250682b0 | ||
|
|
4f75647f63 | ||
|
|
9f5121e271 | ||
|
|
a1e3ec7118 | ||
|
|
d3bae21d13 | ||
|
|
9d138e73c9 | ||
|
|
e23a6bf59f |
33
.clang-tidy
33
.clang-tidy
@@ -5,30 +5,24 @@ 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,
|
||||
@@ -48,7 +42,6 @@ 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,
|
||||
@@ -61,13 +54,12 @@ 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,
|
||||
@@ -79,23 +71,16 @@ 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,
|
||||
@@ -103,42 +88,32 @@ 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-ranges,
|
||||
-modernize-use-nodiscard,
|
||||
-modernize-use-nullptr,
|
||||
-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:
|
||||
|
||||
@@ -1 +1 @@
|
||||
0c7f309d70eca8e3efd510092ddb23c530f3934c49371717efa124b788d761f8
|
||||
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324
|
||||
|
||||
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -199,7 +199,8 @@ jobs:
|
||||
- common
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
|
||||
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
|
||||
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
@@ -242,7 +243,8 @@ jobs:
|
||||
|
||||
# Extract individual fields
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $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 "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
|
||||
@@ -265,16 +267,12 @@ jobs:
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests (${{ matrix.bucket.name }})
|
||||
name: Run integration tests
|
||||
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
|
||||
@@ -301,14 +299,19 @@ jobs:
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
env:
|
||||
# JSON array of test paths; parsed into a bash array below to avoid
|
||||
# shell word-splitting / glob hazards.
|
||||
BUCKET_TESTS: ${{ toJson(matrix.bucket.tests) }}
|
||||
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
|
||||
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
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[@]}"
|
||||
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
|
||||
|
||||
cpp-unit-tests:
|
||||
name: Run C++ unit tests
|
||||
|
||||
@@ -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
|
||||
from esphome import const, writer, yaml_util
|
||||
import esphome.codegen as cg
|
||||
from esphome.config import iter_component_configs, read_config, strip_default_ids
|
||||
from esphome.const import (
|
||||
@@ -72,12 +72,7 @@ from esphome.util import (
|
||||
run_external_process,
|
||||
safe_print,
|
||||
)
|
||||
|
||||
# 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.
|
||||
from esphome.zeroconf import discover_mdns_devices
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -246,8 +241,6 @@ 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(
|
||||
@@ -667,7 +660,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _wrap_to_code(name, comp, yaml_util):
|
||||
def wrap_to_code(name, comp):
|
||||
coro = coroutine(comp.to_code)
|
||||
|
||||
@functools.wraps(comp.to_code)
|
||||
@@ -687,8 +680,6 @@ def _wrap_to_code(name, comp, yaml_util):
|
||||
|
||||
|
||||
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
|
||||
from esphome import writer
|
||||
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
@@ -700,21 +691,17 @@ 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, yaml_util)
|
||||
coro = wrap_to_code(name, component)
|
||||
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)
|
||||
|
||||
@@ -1193,8 +1180,6 @@ 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)
|
||||
@@ -1336,8 +1321,6 @@ 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:
|
||||
@@ -1353,8 +1336,6 @@ 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:
|
||||
@@ -1557,8 +1538,6 @@ 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:
|
||||
|
||||
@@ -793,11 +793,8 @@ 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/`` (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.
|
||||
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
|
||||
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
|
||||
|
||||
Skips scanning if ``_source_symbol_map`` was already populated by
|
||||
``_parse_map_file()``.
|
||||
@@ -809,12 +806,12 @@ class MemoryAnalyzer:
|
||||
if obj_dir is None:
|
||||
return
|
||||
|
||||
# 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():
|
||||
# Find ESPHome source object files
|
||||
esphome_src_dir = obj_dir / "src" / "esphome"
|
||||
if not esphome_src_dir.is_dir():
|
||||
return
|
||||
|
||||
obj_files = sorted(src_dir.rglob("*.o"))
|
||||
obj_files = sorted(esphome_src_dir.rglob("*.o"))
|
||||
if not obj_files:
|
||||
return
|
||||
|
||||
@@ -1067,10 +1064,6 @@ 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
|
||||
|
||||
@@ -127,7 +127,7 @@ def validate_potentially_or_condition(value):
|
||||
return validate_condition(value)
|
||||
|
||||
|
||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action)
|
||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
|
||||
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
||||
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
|
||||
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
||||
@@ -396,6 +396,7 @@ 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
|
||||
|
||||
@@ -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({led_data[0], led_data[1], led_data[2]});
|
||||
auto white = std::min(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));
|
||||
}
|
||||
|
||||
@@ -78,8 +78,7 @@ class ActionResponse {
|
||||
: success_(success), error_message_(error_message) {
|
||||
if (data == nullptr || data_len == 0)
|
||||
return;
|
||||
JsonDocument tmp = json::parse_json(data, data_len);
|
||||
swap(this->json_document_, tmp);
|
||||
this->json_document_ = json::parse_json(data, data_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -424,7 +424,6 @@ 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);
|
||||
|
||||
@@ -14,7 +14,11 @@ 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, 0.0f});
|
||||
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ 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, 0.0f});
|
||||
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -20,7 +19,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, HexInt
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.external_files import download_web_files_in_config
|
||||
from esphome.external_files import download_content
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -64,6 +63,15 @@ 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)
|
||||
@@ -134,10 +142,11 @@ LOCAL_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
WEB_SCHEMA = cv.Schema(
|
||||
WEB_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
}
|
||||
},
|
||||
_download_web_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -200,7 +209,6 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@@ -374,8 +374,7 @@ void Climate::save_state_(const ClimateTraits &traits) {
|
||||
#define TEMP_IGNORE_MEMACCESS
|
||||
#endif
|
||||
ClimateDeviceRestoreState state{};
|
||||
// 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
|
||||
// initialize as zero to prevent random data on stack triggering erase
|
||||
memset(&state, 0, sizeof(ClimateDeviceRestoreState));
|
||||
#ifdef TEMP_IGNORE_MEMACCESS
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#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"
|
||||
|
||||
@@ -17,13 +17,11 @@ 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) {
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
|
||||
return *reinterpret_cast<volatile uint32_t *>(addr);
|
||||
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
||||
}
|
||||
|
||||
static inline uint8_t read_mem_u8(uintptr_t addr) {
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
|
||||
return *reinterpret_cast<volatile uint8_t *>(addr);
|
||||
return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
||||
}
|
||||
|
||||
// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information
|
||||
@@ -100,7 +98,6 @@ 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:");
|
||||
@@ -134,7 +131,6 @@ 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
|
||||
|
||||
@@ -163,9 +159,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
char *buf = buffer.data();
|
||||
|
||||
// Main supply status
|
||||
// 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.";
|
||||
const char *supply_status =
|
||||
(nrf_power_mainregstatus_get(NRF_POWER) == 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);
|
||||
|
||||
@@ -56,7 +56,8 @@ 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(), output_offset + get_lights_per_universe(), output_offset + lights_in_packet});
|
||||
int output_end =
|
||||
std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet));
|
||||
auto *input_data = packet.values + 1;
|
||||
|
||||
auto effect_name = get_name();
|
||||
|
||||
@@ -5,15 +5,6 @@
|
||||
// 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"
|
||||
@@ -23,9 +14,7 @@ namespace ens160_base {
|
||||
|
||||
static const char *const TAG = "ens160";
|
||||
|
||||
// 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 uint8_t ENS160_BOOTING = 10;
|
||||
|
||||
static const uint16_t ENS160_PART_ID = 0x0160;
|
||||
|
||||
@@ -102,8 +91,6 @@ 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;
|
||||
@@ -115,7 +102,6 @@ void ENS160Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(ENS160_BOOTING);
|
||||
|
||||
// read firmware version
|
||||
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) {
|
||||
@@ -123,8 +109,6 @@ 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;
|
||||
@@ -239,6 +223,7 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -729,9 +729,6 @@ 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",
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#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 "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>
|
||||
|
||||
@@ -13,7 +22,54 @@ extern "C" __attribute__((weak)) void initArduino() {}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// HAL functions live in hal.cpp. This file keeps only the loop task setup.
|
||||
// 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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#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
|
||||
@@ -84,7 +84,7 @@ void HOT delay(uint32_t ms) {
|
||||
optimistic_yield(1000);
|
||||
}
|
||||
}
|
||||
// delayMicroseconds(), arch_feed_wdt(), and progmem_read_*() are inlined in hal/hal_esp8266.h.
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
void arch_restart() {
|
||||
system_restart();
|
||||
// restart() doesn't always end execution
|
||||
@@ -93,6 +93,17 @@ 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; }
|
||||
|
||||
|
||||
@@ -140,7 +140,6 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,clang-analyzer-core.FixedAddressDereference)
|
||||
*dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr)
|
||||
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; // NOLINT(clang-analyzer-core.FixedAddressDereference)
|
||||
*ptr = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
ESPNowReceivePacketHandler = espnow_ns.class_("ESPNowReceivePacketHandler")
|
||||
ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler")
|
||||
ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler")
|
||||
ESPNowBroadcastHandler = espnow_ns.class_("ESPNowBroadcastHandler")
|
||||
ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler")
|
||||
|
||||
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, ESPNowReceivePacketHandler
|
||||
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler
|
||||
)
|
||||
OnBroadcastTrigger = espnow_ns.class_(
|
||||
"OnBroadcastTrigger", ESPNowHandlerTrigger, ESPNowBroadcastHandler
|
||||
OnBroadcastedTrigger = espnow_ns.class_(
|
||||
"OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler
|
||||
)
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastTrigger),
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger),
|
||||
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_receive_handler(trigger))
|
||||
cg.add(var.register_received_handler(trigger))
|
||||
|
||||
for on_receive in config.get(CONF_ON_BROADCAST, []):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_broadcast_handler(trigger))
|
||||
cg.add(var.register_broadcasted_handler(trigger))
|
||||
|
||||
|
||||
# ========================================== A C T I O N S ================================================
|
||||
|
||||
@@ -67,7 +67,6 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override { /* ignore - see play_complex */
|
||||
}
|
||||
|
||||
@@ -76,6 +75,7 @@ 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);
|
||||
|
||||
protected:
|
||||
public:
|
||||
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);
|
||||
|
||||
protected:
|
||||
public:
|
||||
void play(const Ts &...x) override {
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
this->parent_->del_peer(address.data());
|
||||
@@ -107,9 +107,8 @@ 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;
|
||||
@@ -126,9 +125,9 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *,
|
||||
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
|
||||
explicit OnReceiveTrigger() {}
|
||||
explicit OnReceiveTrigger() : has_address_(false) {}
|
||||
|
||||
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool on_received(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;
|
||||
@@ -139,7 +138,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 {
|
||||
@@ -149,15 +148,15 @@ class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_
|
||||
return false; // Return false to continue processing other internal handlers
|
||||
}
|
||||
};
|
||||
class OnBroadcastTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowBroadcastHandler {
|
||||
class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowBroadcastedHandler {
|
||||
public:
|
||||
explicit OnBroadcastTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
|
||||
explicit OnBroadcastedTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
|
||||
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
explicit OnBroadcastTrigger() {}
|
||||
explicit OnBroadcastedTrigger() : has_address_(false) {}
|
||||
|
||||
bool on_broadcast(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 match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
|
||||
if (!match)
|
||||
return false;
|
||||
@@ -168,7 +167,7 @@ class OnBroadcastTrigger : 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];
|
||||
};
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
@@ -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->broadcast_handlers_) {
|
||||
if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
for (auto *handler : this->broadcasted_handlers_) {
|
||||
if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
} else {
|
||||
for (auto *handler : this->receive_handlers_) {
|
||||
if (handler->on_receive(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
for (auto *handler : this->received_handlers_) {
|
||||
if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_RECEIVE = 2,
|
||||
ON_BROADCAST = 3,
|
||||
ON_RECEIVED = 2,
|
||||
ON_BROADCASTED = 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_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
virtual bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
/// Handler interface for receiving ESPNow broadcast packets
|
||||
/// Handler interface for receiving broadcasted ESPNow packets
|
||||
/// Components should inherit from this class to handle incoming ESPNow data
|
||||
class ESPNowBroadcastHandler {
|
||||
class ESPNowBroadcastedHandler {
|
||||
public:
|
||||
/// Called when an ESPNow broadcast packet is received
|
||||
/// Called when a broadcasted ESPNow 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_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
virtual bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
|
||||
class ESPNowComponent : public Component {
|
||||
@@ -136,11 +136,13 @@ 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_receive_handler(ESPNowReceivedPacketHandler *handler) { this->receive_handlers_.push_back(handler); }
|
||||
void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); }
|
||||
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
|
||||
this->unknown_peer_handlers_.push_back(handler);
|
||||
}
|
||||
void register_broadcast_handler(ESPNowBroadcastHandler *handler) { this->broadcast_handlers_.push_back(handler); }
|
||||
void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) {
|
||||
this->broadcasted_handlers_.push_back(handler);
|
||||
}
|
||||
|
||||
protected:
|
||||
friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size);
|
||||
@@ -154,8 +156,8 @@ class ESPNowComponent : public Component {
|
||||
void send_();
|
||||
|
||||
std::vector<ESPNowUnknownPeerHandler *> unknown_peer_handlers_;
|
||||
std::vector<ESPNowReceivedPacketHandler *> receive_handlers_;
|
||||
std::vector<ESPNowBroadcastHandler *> broadcast_handlers_;
|
||||
std::vector<ESPNowReceivedPacketHandler *> received_handlers_;
|
||||
std::vector<ESPNowBroadcastedHandler *> broadcasted_handlers_;
|
||||
|
||||
std::vector<ESPNowPeer> peers_{};
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ void ESPNowTransport::setup() {
|
||||
this->peer_address_[5]);
|
||||
|
||||
// Register received handler
|
||||
this->parent_->register_receive_handler(this);
|
||||
this->parent_->register_received_handler(this);
|
||||
|
||||
// Register broadcast handler
|
||||
this->parent_->register_broadcast_handler(this);
|
||||
// Register broadcasted handler
|
||||
this->parent_->register_broadcasted_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_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||
bool ESPNowTransport::on_received(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_receive(const ESPNowRecvInfo &info, const uint8_t *data
|
||||
return false; // Allow other handlers to run
|
||||
}
|
||||
|
||||
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||
bool ESPNowTransport::on_broadcasted(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]);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace espnow {
|
||||
class ESPNowTransport : public packet_transport::PacketTransport,
|
||||
public Parented<ESPNowComponent>,
|
||||
public ESPNowReceivedPacketHandler,
|
||||
public ESPNowBroadcastHandler {
|
||||
public ESPNowBroadcastedHandler {
|
||||
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_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;
|
||||
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;
|
||||
|
||||
protected:
|
||||
void send_packet(const std::vector<uint8_t> &buf) const override;
|
||||
|
||||
@@ -31,19 +31,17 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, 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 = {
|
||||
@@ -349,38 +347,17 @@ 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])
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -7,24 +7,29 @@
|
||||
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:
|
||||
using ApplyFn = void (*)(FanCall &, const Ts &...);
|
||||
TurnOnAction(Fan *state, ApplyFn apply) : state_(state), apply_(apply) {}
|
||||
explicit TurnOnAction(Fan *state) : state_(state) {}
|
||||
|
||||
TEMPLATABLE_VALUE(bool, oscillating)
|
||||
TEMPLATABLE_VALUE(int, speed)
|
||||
TEMPLATABLE_VALUE(FanDirection, direction)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->state_->turn_on();
|
||||
this->apply_(call, x...);
|
||||
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...));
|
||||
}
|
||||
call.perform();
|
||||
}
|
||||
|
||||
Fan *state_;
|
||||
ApplyFn apply_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class TurnOffAction : public Action<Ts...> {
|
||||
|
||||
@@ -375,10 +375,12 @@ 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, waittime, [this, dir]() { this->start_direction_(dir); });
|
||||
|
||||
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, *this->direction_change_waittime_,
|
||||
[this, dir]() { this->start_direction_(dir); });
|
||||
|
||||
} else {
|
||||
this->set_current_operation_(dir, true);
|
||||
this->prev_command_trigger_ = trig;
|
||||
|
||||
@@ -85,7 +85,7 @@ void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direct
|
||||
this->force_send_control_ = true;
|
||||
}
|
||||
|
||||
const char *HonClimate::get_cleaning_status_text() const {
|
||||
std::string HonClimate::get_cleaning_status_text() const {
|
||||
switch (this->cleaning_status_) {
|
||||
case CleaningState::SELF_CLEAN:
|
||||
return "Self clean";
|
||||
@@ -134,22 +134,29 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haie
|
||||
}
|
||||
// All OK
|
||||
hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
|
||||
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];
|
||||
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);
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, info.device_name_);
|
||||
this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, info.protocol_version_);
|
||||
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_);
|
||||
#endif
|
||||
this->hvac_hardware_info_ = info;
|
||||
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->set_phase(ProtocolPhases::SENDING_INIT_2);
|
||||
return result;
|
||||
} else {
|
||||
@@ -340,9 +347,10 @@ void HonClimate::dump_config() {
|
||||
" Device software version: %s\n"
|
||||
" Device hardware version: %s\n"
|
||||
" Device name: %s",
|
||||
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_);
|
||||
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());
|
||||
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" : ""),
|
||||
@@ -452,7 +460,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(); // NOLINT(bugprone-unchecked-optional-access)
|
||||
this->action_request_.value().message.reset();
|
||||
} else {
|
||||
// Message already sent, reseting request and return to idle
|
||||
this->action_request_.reset();
|
||||
@@ -788,7 +796,7 @@ void HonClimate::set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSe
|
||||
}
|
||||
}
|
||||
|
||||
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const char *value) {
|
||||
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::string &value) {
|
||||
size_t index = (size_t) type;
|
||||
if (this->sub_text_sensors_[index] != nullptr)
|
||||
this->sub_text_sensors_[index]->publish_state(value);
|
||||
|
||||
@@ -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 char *value);
|
||||
void update_sub_text_sensor_(SubTextSensorType type, const std::string &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);
|
||||
const char *get_cleaning_status_text() const;
|
||||
std::string get_cleaning_status_text() const;
|
||||
CleaningState get_cleaning_status() const;
|
||||
void start_self_cleaning();
|
||||
void start_steri_cleaning();
|
||||
@@ -166,12 +166,11 @@ 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 {
|
||||
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];
|
||||
std::string protocol_version_;
|
||||
std::string software_version_;
|
||||
std::string hardware_version_;
|
||||
std::string device_name_;
|
||||
bool functions_[5];
|
||||
};
|
||||
|
||||
|
||||
@@ -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(); // NOLINT(bugprone-unchecked-optional-access)
|
||||
this->action_request_.value().message.reset();
|
||||
} else {
|
||||
// Message already sent, reseting request and return to idle
|
||||
this->action_request_.reset();
|
||||
|
||||
@@ -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, remaining, (size_t) available_data});
|
||||
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
|
||||
@@ -139,12 +139,12 @@ void KamstrupKMPComponent::clear_uart_rx_buffer_() {
|
||||
|
||||
void KamstrupKMPComponent::read_command_(uint16_t command) {
|
||||
uint8_t buffer[20] = {0};
|
||||
size_t buffer_len = 0;
|
||||
int buffer_len = 0;
|
||||
int data;
|
||||
int timeout = 250; // ms
|
||||
|
||||
// Read the data from the UART
|
||||
while (timeout > 0 && buffer_len < sizeof(buffer)) {
|
||||
while (timeout > 0 && buffer_len < static_cast<int>(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 (size_t i = 1; i < buffer_len - 1; i++) {
|
||||
for (int i = 1; i < buffer_len - 1; i++) {
|
||||
if (buffer[i] == 0x1B) {
|
||||
msg[msg_len++] = buffer[i + 1] ^ 0xFF;
|
||||
i++;
|
||||
|
||||
@@ -8,59 +8,84 @@ namespace esphome::light {
|
||||
|
||||
enum class LimitMode { CLAMP, DO_NOTHING };
|
||||
|
||||
template<bool HasTransitionLength, typename... Ts> class ToggleAction : public Action<Ts...> {
|
||||
template<typename... Ts> class ToggleAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit ToggleAction(LightState *state) : state_(state) {}
|
||||
|
||||
template<typename V> void set_transition_length(V value) requires(HasTransitionLength) {
|
||||
this->transition_length_ = value;
|
||||
}
|
||||
TEMPLATABLE_VALUE(uint32_t, transition_length)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->state_->toggle();
|
||||
if constexpr (HasTransitionLength) {
|
||||
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||
}
|
||||
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:
|
||||
using ApplyFn = void (*)(LightState *, LightCall &, const Ts &...);
|
||||
LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {}
|
||||
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)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->parent_->make_call();
|
||||
this->apply_(this->parent_, call, x...);
|
||||
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...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
LightState *parent_;
|
||||
ApplyFn apply_;
|
||||
};
|
||||
|
||||
template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : public Action<Ts...> {
|
||||
template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit DimRelativeAction(LightState *parent) : parent_(parent) {}
|
||||
|
||||
TEMPLATABLE_VALUE(float, relative_brightness)
|
||||
|
||||
template<typename V> void set_transition_length(V value) requires(HasTransitionLength) {
|
||||
this->transition_length_ = value;
|
||||
}
|
||||
TEMPLATABLE_VALUE(uint32_t, transition_length)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->parent_->make_call();
|
||||
@@ -74,9 +99,7 @@ template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : pub
|
||||
call.set_state(new_brightness != 0.0f);
|
||||
call.set_brightness(new_brightness);
|
||||
|
||||
if constexpr (HasTransitionLength) {
|
||||
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||
}
|
||||
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
@@ -92,9 +115,6 @@ template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : pub
|
||||
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...> {
|
||||
|
||||
@@ -37,7 +37,6 @@ from .types import (
|
||||
AddressableSet,
|
||||
ColorMode,
|
||||
DimRelativeAction,
|
||||
LightCall,
|
||||
LightControlAction,
|
||||
LightIsOffCondition,
|
||||
LightIsOnCondition,
|
||||
@@ -61,10 +60,8 @@ from .types import (
|
||||
)
|
||||
async def light_toggle_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
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:
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if CONF_TRANSITION_LENGTH in config:
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_TRANSITION_LENGTH], args, cg.uint32
|
||||
)
|
||||
@@ -181,9 +178,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)
|
||||
|
||||
# All configured fields are folded into a single stateless lambda whose
|
||||
# constants live in flash; the action stores only a function pointer.
|
||||
# (config_key, setter_name, c++ type)
|
||||
FIELDS = (
|
||||
(CONF_COLOR_MODE, "set_color_mode", ColorMode),
|
||||
(CONF_STATE, "set_state", cg.bool_),
|
||||
@@ -199,50 +196,38 @@ 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 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_key in config:
|
||||
template_ = await cg.templatable(config[conf_key], args, type_)
|
||||
cg.add(getattr(var, setter)(template_))
|
||||
|
||||
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
|
||||
)
|
||||
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())); }}"
|
||||
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,
|
||||
)
|
||||
cg.add(var.set_effect(wrapper))
|
||||
else:
|
||||
# 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)}));"
|
||||
# Static string — resolve effect name to index at codegen time
|
||||
template_ = await cg.templatable(
|
||||
_resolve_effect_index(config), args, cg.uint32
|
||||
)
|
||||
|
||||
# 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)
|
||||
cg.add(var.set_effect(template_))
|
||||
return var
|
||||
|
||||
|
||||
CONF_RELATIVE_BRIGHTNESS = "relative_brightness"
|
||||
@@ -276,12 +261,10 @@ 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])
|
||||
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)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, cg.float_)
|
||||
cg.add(var.set_relative_brightness(templ))
|
||||
if has_transition_length:
|
||||
if CONF_TRANSITION_LENGTH in config:
|
||||
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):
|
||||
|
||||
@@ -13,7 +13,6 @@ 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)
|
||||
|
||||
@@ -2,7 +2,6 @@ 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,
|
||||
@@ -31,29 +30,10 @@ 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): _validate_interrupt_pin,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -588,7 +588,6 @@ 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);
|
||||
|
||||
@@ -765,7 +764,6 @@ 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
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ 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 {
|
||||
@@ -114,7 +113,6 @@ 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(); }
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ 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;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#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"
|
||||
|
||||
@@ -29,9 +28,29 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
|
||||
return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION;
|
||||
}
|
||||
|
||||
watchdog::WatchdogManager watchdog(15000);
|
||||
#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
|
||||
|
||||
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;
|
||||
|
||||
@@ -164,7 +164,7 @@ class RemoteTransmitterBase : public RemoteComponentBase {
|
||||
return TransmitCall(this);
|
||||
}
|
||||
template<typename Protocol>
|
||||
void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
void transmit(const typename 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(T::ProtocolData data) { data_ = data; }
|
||||
void set_data(typename T::ProtocolData data) { data_ = data; }
|
||||
|
||||
protected:
|
||||
T::ProtocolData data_;
|
||||
typename T::ProtocolData data_;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
@@ -278,7 +278,7 @@ class RemoteTransmittable {
|
||||
|
||||
protected:
|
||||
template<typename Protocol>
|
||||
void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
this->transmitter_->transmit<Protocol>(data, send_times, send_wait);
|
||||
}
|
||||
RemoteTransmitterBase *transmitter_;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Speaker Media Player Setup."""
|
||||
|
||||
from functools import partial
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -33,7 +32,7 @@ from esphome.const import (
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.external_files import download_web_files_in_config
|
||||
from esphome.external_files import download_content
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -93,6 +92,15 @@ 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"],
|
||||
@@ -221,10 +229,11 @@ LOCAL_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
WEB_SCHEMA = cv.Schema(
|
||||
WEB_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
}
|
||||
},
|
||||
_download_web_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -276,12 +285,7 @@ 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.All(
|
||||
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
|
||||
partial(
|
||||
download_web_files_in_config, path_for=_compute_local_file_path
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All(
|
||||
cv.boolean, cv.requires_component(psram.DOMAIN)
|
||||
),
|
||||
|
||||
@@ -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, this->start_delay_, this->stop_delay_});
|
||||
return std::max(this->switching_delay_.value_or(0) * 2, std::max(this->start_delay_, this->stop_delay_));
|
||||
}
|
||||
return run_duration;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
@@ -7,8 +5,6 @@ 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"]
|
||||
@@ -19,7 +15,6 @@ 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"
|
||||
@@ -149,7 +144,7 @@ SetModeStandbyAction = sx126x_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
def validate_raw_data(value):
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
if isinstance(value, list):
|
||||
@@ -159,7 +154,7 @@ def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
)
|
||||
|
||||
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
def validate_config(config):
|
||||
lora_bws = [
|
||||
"7_8kHz",
|
||||
"10_4kHz",
|
||||
@@ -240,7 +235,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
@@ -312,50 +307,24 @@ 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: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
async def no_args_action_to_code(config, action_id, template_arg, args):
|
||||
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),
|
||||
@@ -371,12 +340,7 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
SEND_PACKET_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def send_packet_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
data = config[CONF_DATA]
|
||||
|
||||
@@ -56,8 +56,7 @@ template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Pa
|
||||
|
||||
template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX126x> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(bool, cold)
|
||||
void play(const Ts &...x) override { this->parent_->set_mode_sleep(this->cold_.value(x...)); }
|
||||
void play(const Ts &...x) override { this->parent_->set_mode_sleep(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX126x> {
|
||||
|
||||
@@ -459,10 +459,9 @@ void SX126x::set_mode_tx() {
|
||||
this->write_opcode_(RADIO_SET_TX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126x::set_mode_sleep(bool cold) {
|
||||
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
|
||||
void SX126x::set_mode_sleep() {
|
||||
uint8_t buf[1];
|
||||
buf[0] = cold ? 0x00 : 0x04;
|
||||
buf[0] = 0x05;
|
||||
this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(bool cold = false);
|
||||
void set_mode_sleep();
|
||||
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; }
|
||||
|
||||
@@ -31,14 +31,13 @@ void CronTrigger::check_time_() {
|
||||
return;
|
||||
|
||||
if (this->last_check_.has_value()) {
|
||||
auto &last_check = *this->last_check_;
|
||||
if (last_check > time && last_check.timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) {
|
||||
if (*this->last_check_ > time && this->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 (last_check >= time) {
|
||||
} else if (*this->last_check_ >= time) {
|
||||
// already handled this one
|
||||
return;
|
||||
} else if (time > last_check && time.timestamp - last_check.timestamp > MAX_TIMESTAMP_DRIFT) {
|
||||
} else if (time > *this->last_check_ && time.timestamp - this->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;
|
||||
@@ -46,11 +45,11 @@ void CronTrigger::check_time_() {
|
||||
}
|
||||
|
||||
while (true) {
|
||||
last_check.increment_second();
|
||||
if (last_check >= time)
|
||||
this->last_check_->increment_second();
|
||||
if (*this->last_check_ >= time)
|
||||
break;
|
||||
|
||||
if (this->matches(last_check))
|
||||
if (this->matches(*this->last_check_))
|
||||
this->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,13 +282,12 @@ optional<GateStatus> Tormatic::read_gate_status_() {
|
||||
}
|
||||
}
|
||||
|
||||
auto hdr = this->pending_hdr_.value();
|
||||
|
||||
// Wait for all payload bytes to arrive before processing.
|
||||
if (this->available() < hdr.payload_size()) {
|
||||
if (this->available() < this->pending_hdr_->payload_size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto hdr = *this->pending_hdr_;
|
||||
this->pending_hdr_.reset();
|
||||
|
||||
switch (hdr.type) {
|
||||
|
||||
@@ -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 || static_cast<size_t>(temp_index) >= sizeof(RAS_2819T_TEMP_CODES)) {
|
||||
if (temp_index < 0 || temp_index >= static_cast<int>(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
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@ from esphome.const import (
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_WATER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, 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 LambdaExpression, MockObjClass
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -43,7 +43,6 @@ 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
|
||||
@@ -229,40 +228,17 @@ 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])
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -47,25 +47,24 @@ 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:
|
||||
using ApplyFn = void (*)(ValveCall &, const Ts &...);
|
||||
ControlAction(Valve *valve, ApplyFn apply) : valve_(valve), apply_(apply) {}
|
||||
explicit ControlAction(Valve *valve) : valve_(valve) {}
|
||||
|
||||
TEMPLATABLE_VALUE(bool, stop)
|
||||
TEMPLATABLE_VALUE(float, position)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->valve_->make_call();
|
||||
this->apply_(call, x...);
|
||||
if (this->stop_.has_value())
|
||||
call.set_stop(this->stop_.value(x...));
|
||||
if (this->position_.has_value())
|
||||
call.set_position(this->position_.value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
Valve *valve_;
|
||||
ApplyFn apply_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class ValveIsOpenCondition : public Condition<Ts...> {
|
||||
|
||||
@@ -39,18 +39,9 @@ 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 = 0,
|
||||
.trigger_panic = false,
|
||||
.idle_core_mask = (1U << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1U,
|
||||
.trigger_panic = true,
|
||||
};
|
||||
#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
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ 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
|
||||
@@ -113,14 +112,6 @@ 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)
|
||||
@@ -415,10 +406,6 @@ 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,
|
||||
@@ -582,9 +569,6 @@ 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)
|
||||
|
||||
|
||||
@@ -309,18 +309,6 @@ 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)
|
||||
@@ -1547,9 +1535,6 @@ 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_();
|
||||
|
||||
@@ -345,17 +345,6 @@ 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
|
||||
@@ -466,9 +455,6 @@ 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);
|
||||
|
||||
@@ -686,9 +672,6 @@ 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_();
|
||||
@@ -827,9 +810,6 @@ 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};
|
||||
|
||||
@@ -621,25 +621,10 @@ 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);
|
||||
|
||||
|
||||
@@ -25,10 +25,7 @@ from esphome.const import (
|
||||
CONF_SUBSTITUTIONS,
|
||||
)
|
||||
from esphome.core import CORE, DocumentRange, EsphomeError
|
||||
|
||||
# `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.core.config as core_config
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import indent
|
||||
from esphome.loader import ComponentManifest, get_component, get_platform
|
||||
@@ -971,8 +968,6 @@ 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:
|
||||
@@ -1078,8 +1073,6 @@ 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:
|
||||
|
||||
@@ -178,7 +178,7 @@ class ProjectUpdateTrigger : public Trigger<std::string>, public Component {
|
||||
};
|
||||
#endif
|
||||
|
||||
template<typename... Ts> class DelayAction : public Action<Ts...> {
|
||||
template<typename... Ts> class DelayAction : public Action<Ts...>, public Component {
|
||||
public:
|
||||
explicit DelayAction() = default;
|
||||
|
||||
@@ -198,8 +198,8 @@ template<typename... Ts> class DelayAction : public Action<Ts...> {
|
||||
// to avoid overhead from capturing arguments by value
|
||||
if constexpr (sizeof...(Ts) == 0) {
|
||||
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(),
|
||||
this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr,
|
||||
static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), 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...> {
|
||||
// `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_(
|
||||
/* 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void play(const Ts &...x) override { /* ignore - see play_complex */
|
||||
}
|
||||
|
||||
void stop() override { App.scheduler.cancel_timeout(this); }
|
||||
void stop() override { this->cancel_timeout(InternalSchedulerID::DELAY_ACTION); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class LambdaAction : public Action<Ts...> {
|
||||
|
||||
@@ -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, g, b});
|
||||
const uint16_t max_rgb = std::max(r, std::max(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);
|
||||
}
|
||||
|
||||
@@ -655,15 +655,7 @@ class WarnIfComponentBlockingGuard {
|
||||
// Inlined: the fast path is just millis() + subtract + compare
|
||||
inline uint32_t HOT finish() {
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
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;
|
||||
}
|
||||
this->component_->runtime_stats_.record_time(micros() - this->started_us_);
|
||||
#endif
|
||||
uint32_t curr_time = MillisInternal::get();
|
||||
#ifndef USE_BENCHMARK
|
||||
|
||||
@@ -297,7 +297,6 @@
|
||||
#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
|
||||
|
||||
@@ -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 = BitPolicy::mask_t;
|
||||
using bitmask_t = typename BitPolicy::mask_t;
|
||||
|
||||
constexpr FiniteSetMask() = default;
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// 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 delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
|
||||
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
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <esp_attr.h>
|
||||
#include <esp_cpu.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
@@ -17,11 +15,6 @@
|
||||
|
||||
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; }
|
||||
|
||||
@@ -37,11 +30,6 @@ __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
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include <c_types.h>
|
||||
#include <cstdint>
|
||||
#include <pgmspace.h>
|
||||
|
||||
#include "esphome/core/time_64.h"
|
||||
|
||||
@@ -20,16 +19,8 @@ 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
|
||||
@@ -43,24 +34,10 @@ void delay(uint32_t ms);
|
||||
uint32_t millis();
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
|
||||
|
||||
// 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();
|
||||
// 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);
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -19,10 +19,6 @@ 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
|
||||
|
||||
@@ -88,10 +88,6 @@ __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
|
||||
|
||||
@@ -35,10 +35,6 @@ __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
|
||||
|
||||
@@ -19,10 +19,6 @@ 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
|
||||
|
||||
@@ -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({red, green, blue});
|
||||
float min_color_value = std::min({red, green, blue});
|
||||
float max_color_value = std::max(std::max(red, green), blue);
|
||||
float min_color_value = std::min(std::min(red, green), blue);
|
||||
float delta = max_color_value - min_color_value;
|
||||
|
||||
if (delta == 0) {
|
||||
|
||||
@@ -69,10 +69,6 @@ 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.
|
||||
|
||||
@@ -35,9 +35,7 @@ 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 {
|
||||
// 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];
|
||||
char buffer[20]; // Enough for "id:4294967295" or "hash:0xFFFFFFFF" or "(null)"
|
||||
|
||||
// Format a scheduler item name for logging
|
||||
// Returns pointer to formatted string (either static_name or internal buffer)
|
||||
@@ -55,15 +53,9 @@ 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 if (name_type == NameType::NUMERIC_ID_INTERNAL) {
|
||||
} else { // 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -301,27 +293,6 @@ 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
|
||||
|
||||
@@ -146,43 +146,22 @@ class Scheduler {
|
||||
}
|
||||
|
||||
// Name storage type discriminator for SchedulerItem
|
||||
// Used to distinguish between static strings, hashed strings, numeric IDs, internal numeric IDs,
|
||||
// and self-keyed pointers (caller-supplied `void *`, typically `this`).
|
||||
// Used to distinguish between static strings, hashed strings, numeric IDs, and internal numeric IDs
|
||||
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)
|
||||
SELF_POINTER = 4 // void* caller-supplied key (typically `this`); pointer equality
|
||||
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-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) and SELF_POINTER (caller's `this`)
|
||||
uint32_t hash_or_id; // For HASHED_STRING, NUMERIC_ID, and NUMERIC_ID_INTERNAL
|
||||
const char *static_name; // For STATIC_STRING (string literals, no allocation)
|
||||
uint32_t hash_or_id; // For HASHED_STRING or NUMERIC_ID
|
||||
} name_;
|
||||
uint32_t interval;
|
||||
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
|
||||
@@ -203,19 +182,19 @@ class Scheduler {
|
||||
// std::atomic<uint8_t> inlines correctly on all platforms.
|
||||
std::atomic<uint8_t> remove{0};
|
||||
|
||||
// Bit-packed fields (5 bits used, 3 bits padding in 1 byte)
|
||||
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
|
||||
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
|
||||
NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum)
|
||||
NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum)
|
||||
bool is_retry : 1; // True if this is a retry timeout
|
||||
// 3 bits padding
|
||||
// 4 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)
|
||||
// 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_ : 3; // Discriminator for name_ union (0–4, see NameType enum)
|
||||
NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum)
|
||||
bool is_retry : 1; // True if this is a retry timeout
|
||||
// 2 bits padding
|
||||
// 3 bits padding
|
||||
#endif
|
||||
|
||||
// Constructor
|
||||
@@ -249,26 +228,19 @@ class Scheduler {
|
||||
SchedulerItem(SchedulerItem &&) = delete;
|
||||
SchedulerItem &operator=(SchedulerItem &&) = delete;
|
||||
|
||||
// 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 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 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 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 name type
|
||||
NameType get_name_type() const { return name_type_; }
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
void set_name(NameType type, const char *static_name, uint32_t hash_or_id) {
|
||||
if (type == NameType::STATIC_STRING || type == NameType::SELF_POINTER) {
|
||||
if (type == NameType::STATIC_STRING) {
|
||||
name_.static_name = static_name;
|
||||
} else {
|
||||
name_.hash_or_id = hash_or_id;
|
||||
@@ -395,14 +367,10 @@ class Scheduler {
|
||||
// Name type must match
|
||||
if (item->get_name_type() != name_type)
|
||||
return false;
|
||||
// STATIC_STRING: compare string content. SELF_POINTER: raw pointer equality (no strcmp).
|
||||
// Other types: compare hash/ID value.
|
||||
// For static strings, compare the string content; for hash/ID, compare the 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import contextlib
|
||||
from datetime import UTC, datetime
|
||||
import logging
|
||||
@@ -11,10 +9,9 @@ from pathlib import Path
|
||||
import requests
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FILE, CONF_TYPE, CONF_URL, __version__
|
||||
from esphome.const import __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"]
|
||||
@@ -88,9 +85,7 @@ def _write_etag(local_file_path: Path, etag: str | None) -> None:
|
||||
)
|
||||
|
||||
|
||||
def has_remote_file_changed(
|
||||
url: str, local_file_path: Path, timeout: int = NETWORK_TIMEOUT
|
||||
) -> bool:
|
||||
def has_remote_file_changed(url: str, local_file_path: Path) -> bool:
|
||||
if local_file_path.exists():
|
||||
_LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path)
|
||||
try:
|
||||
@@ -106,7 +101,7 @@ def has_remote_file_changed(
|
||||
if etag := _read_etag(local_file_path):
|
||||
headers[IF_NONE_MATCH] = etag
|
||||
response = requests.head(
|
||||
url, headers=headers, timeout=timeout, allow_redirects=True
|
||||
url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -158,7 +153,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, timeout):
|
||||
if not has_remote_file_changed(url, path):
|
||||
_LOGGER.debug("Remote file has not changed %s", url)
|
||||
return path.read_bytes()
|
||||
|
||||
@@ -175,11 +170,6 @@ 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(
|
||||
@@ -190,91 +180,7 @@ 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
|
||||
|
||||
@@ -9,23 +9,14 @@ import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import 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__)
|
||||
|
||||
|
||||
@@ -103,7 +94,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
|
||||
@@ -222,13 +213,6 @@ 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:
|
||||
@@ -264,6 +248,9 @@ 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:
|
||||
|
||||
@@ -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==22.1.0.1
|
||||
clang-tidy==18.1.8 # When updating clang-tidy, also update Dockerfile
|
||||
yamllint==1.38.0 # also change in .pre-commit-config.yaml when updating
|
||||
|
||||
@@ -57,59 +57,6 @@ 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.
|
||||
|
||||
@@ -369,7 +316,31 @@ def compile_and_get_binary(
|
||||
|
||||
# Add remaining components and dependencies to the configuration after
|
||||
# validation, so their source files are included in the build.
|
||||
populate_dependency_config(config, components_with_dependencies)
|
||||
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] = {}
|
||||
|
||||
# Register platforms from the extra config (benchmark.yaml) so
|
||||
# USE_SENSOR, USE_LIGHT, etc. defines are emitted without needing
|
||||
|
||||
@@ -295,7 +295,7 @@ def main():
|
||||
|
||||
failed_files = []
|
||||
try:
|
||||
executable = get_binary("clang-tidy", 22)
|
||||
executable = get_binary("clang-tidy", 18)
|
||||
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-22", tmpdir], close_fds=False
|
||||
["clang-apply-replacements-18", tmpdir], close_fds=False
|
||||
)
|
||||
except FileNotFoundError:
|
||||
subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False)
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"Error please install clang-apply-replacements-22 or clang-apply-replacements.\n",
|
||||
"Error please install clang-apply-replacements-18 or clang-apply-replacements.\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
except:
|
||||
|
||||
@@ -6,7 +6,8 @@ what files have changed. It outputs JSON with the following structure:
|
||||
|
||||
{
|
||||
"integration_tests": true/false,
|
||||
"integration_test_buckets": [{"name": "1/3", "tests": ["tests/integration/test_foo.py", ...]}, ...],
|
||||
"integration_tests_run_all": true/false,
|
||||
"integration_test_files": ["tests/integration/test_foo.py", ...],
|
||||
"clang_tidy": true/false,
|
||||
"clang_format": true/false,
|
||||
"python_linters": true/false,
|
||||
@@ -80,62 +81,6 @@ 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."""
|
||||
@@ -867,9 +812,7 @@ def main() -> None:
|
||||
integration_run_all, integration_test_files = determine_integration_tests(
|
||||
args.branch
|
||||
)
|
||||
run_integration, integration_test_buckets = _compute_integration_test_buckets(
|
||||
integration_run_all, integration_test_files
|
||||
)
|
||||
run_integration = integration_run_all or bool(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)
|
||||
@@ -1001,7 +944,8 @@ def main() -> None:
|
||||
|
||||
output: dict[str, Any] = {
|
||||
"integration_tests": run_integration,
|
||||
"integration_test_buckets": integration_test_buckets,
|
||||
"integration_tests_run_all": integration_run_all,
|
||||
"integration_test_files": integration_test_files,
|
||||
"clang_tidy": run_clang_tidy,
|
||||
"clang_tidy_mode": clang_tidy_mode,
|
||||
"clang_format": run_clang_format,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"target_module": "esphome.__main__",
|
||||
"margin_pct": 15,
|
||||
"cumulative_us": 91000
|
||||
"cumulative_us": 123000
|
||||
}
|
||||
|
||||
@@ -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,bugprone-random-generator-seed) -- fixed seed for reproducibility
|
||||
// NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp) -- intentional fixed seed for reproducibility.
|
||||
std::mt19937_64 rng(0xC0FFEE);
|
||||
for (int i = 0; i < 100; i++) {
|
||||
uint64_t mac = rng() & 0xFFFFFFFFFFFFULL;
|
||||
|
||||
@@ -29,59 +29,3 @@ 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"
|
||||
|
||||
@@ -57,34 +57,3 @@ 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;'
|
||||
|
||||
@@ -108,10 +108,6 @@ 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%
|
||||
|
||||
@@ -293,60 +293,6 @@ 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
|
||||
@@ -442,20 +388,6 @@ 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:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
wifi:
|
||||
min_auth_mode: WPA2
|
||||
post_connect_roaming: true
|
||||
phy_mode: 11G
|
||||
|
||||
packages:
|
||||
- !include common.yaml
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
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);"
|
||||
@@ -1,60 +0,0 @@
|
||||
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%
|
||||
@@ -1,37 +0,0 @@
|
||||
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
|
||||
@@ -1,112 +0,0 @@
|
||||
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
|
||||
@@ -1,69 +0,0 @@
|
||||
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
|
||||
@@ -1,75 +0,0 @@
|
||||
"""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
|
||||
@@ -1,72 +0,0 @@
|
||||
"""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
Reference in New Issue
Block a user