diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5eb96af59b..586263c651 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -286,7 +286,7 @@ jobs: fi echo "version=$version" >> "$GITHUB_OUTPUT" - name: Run prek - uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 + uses: j178/prek-action@1382ad72dea72f9d53832f2bcb65b3e87a947204 # v3.0.1 with: prek-version: ${{ steps.prek.outputs.version }} # This job only runs on pull requests, so nothing ever populates @@ -893,14 +893,20 @@ jobs: matrix: include: - id: clang-tidy - name: Run script/clang-tidy for ESP32 IDF 1/3 - options: --environment esp32-idf-tidy --split-num 3 --split-at 1 + name: Run script/clang-tidy for ESP32 IDF 1/5 + options: --environment esp32-idf-tidy --split-num 5 --split-at 1 - id: clang-tidy - name: Run script/clang-tidy for ESP32 IDF 2/3 - options: --environment esp32-idf-tidy --split-num 3 --split-at 2 + name: Run script/clang-tidy for ESP32 IDF 2/5 + options: --environment esp32-idf-tidy --split-num 5 --split-at 2 - id: clang-tidy - name: Run script/clang-tidy for ESP32 IDF 3/3 - options: --environment esp32-idf-tidy --split-num 3 --split-at 3 + name: Run script/clang-tidy for ESP32 IDF 3/5 + options: --environment esp32-idf-tidy --split-num 5 --split-at 3 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 IDF 4/5 + options: --environment esp32-idf-tidy --split-num 5 --split-at 4 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 IDF 5/5 + options: --environment esp32-idf-tidy --split-num 5 --split-at 5 steps: - name: Check out code from GitHub @@ -1416,31 +1422,27 @@ jobs: echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" fi - # Check if test files exist on the target branch for the requested - # components and platform. When a PR adds new test files for a platform, - # the target branch won't have them yet, so skip instead of failing. - # This check must be done here (not in determine-jobs.py) because - # determine-jobs runs on the PR branch and cannot see what the target - # branch has. + # The build below uses --base-only, so skip unless the target branch has + # test..yaml for every requested component (test-*.yaml + # variants alone compile nothing, and a partial set would compare a + # smaller target build against the PR build). Checked here, not in + # determine-jobs.py, because determine-jobs runs on the PR branch and + # cannot see the target branch. - name: Check for test files on target branch id: check-tests if: steps.check-script.outputs.skip != 'true' run: | components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" - found=false + missing="" for component in $(echo "$components" | jq -r '.[]'); do - # Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml) - for f in tests/components/${component}/test*.${platform}.yaml; do - if [ -f "$f" ]; then - found=true - break 2 - fi - done + if [ ! -f "tests/components/${component}/test.${platform}.yaml" ]; then + missing="${missing} ${component}" + fi done - if [ "$found" = false ]; then + if [ -n "$missing" ]; then echo "skip=true" >> $GITHUB_OUTPUT - echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis" + echo "::warning::No test.${platform}.yaml on target branch for${missing}, skipping memory impact analysis" else echo "skip=false" >> $GITHUB_OUTPUT fi diff --git a/AGENTS.md b/AGENTS.md index 6a60fc47b1..8194cd5040 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,6 +177,13 @@ file does, and it is the authority when they disagree. The most useful starting Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use `set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms: https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb + * **Scheduler ids:** name a timer only when it must be cancelled or replaced, and use a + `static constexpr uint32_t` id, never a string. Ids are per component instance and cannot clash with other + components, so number them from 0 and keep all of a component's ids together in one place. + ```cpp + static constexpr uint32_t READ_TIMEOUT_ID = 0; + this->set_timeout(READ_TIMEOUT_ID, 50, [this]() { this->read_(); }); + ``` * **Don't override a default with the same value:** if a base class method already returns what you want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a component that wants `DATA` should simply leave it alone. @@ -230,6 +237,10 @@ file does, and it is the authority when they disagree. The most useful starting ``` * **Component Metadata:** + - `DOMAIN`: Required in every component's `__init__.py`, set to the component's own name as a plain + string (e.g. `DOMAIN = "my_component"`). Other code refers to the component through it, e.g. + `from esphome.components.my_component import DOMAIN`. Keep it in alphabetical order with the other + metadata constants. CI (`lint_component_domain`) fails without it. - `DEPENDENCIES`: List of required components - `AUTO_LOAD`: Components to automatically load - `CONFLICTS_WITH`: Incompatible components @@ -243,6 +254,7 @@ file does, and it is the authority when they disagree. The most useful starting import esphome.config_validation as cv from esphome.const import CONF_KEY, CONF_ID + DOMAIN = "my_component" CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py my_component_ns = cg.esphome_ns.namespace("my_component") @@ -384,6 +396,25 @@ file does, and it is the authority when they disagree. The most useful starting ) ``` + When the callback's parameters differ from the automation's arguments, or the trigger should only + fire for some values, pass `params`, `forward` and `when`; the helper then generates a capture-less + lambda (stored inline, nothing allocated). Name the parent with `automation.parent_ref(var)` and build + `forward` from `MockObj` calls, not f-strings of C++: + ```python + parent = automation.parent_ref(var) + index = cg.RawExpression("index") + await automation.build_callback_automation( + var, + "add_on_state_callback", + [(cg.StringRef, "x"), (cg.size_t, "i")], + conf, + params=[(cg.size_t, "index")], + forward=[cg.StringRef(parent.option_at(index)), index], + ) + ``` + `build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and + `build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`. + **C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation): ```cpp class MyComponent : public Component { diff --git a/CODEOWNERS b/CODEOWNERS index b5138c0612..a851e62a3e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -188,6 +188,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/esp_ldo/* @clydebarrow esphome/components/espectre/* @francescopace +esphome/components/esphome/ota/* @esphome/core esphome/components/espnow/* @jesserockz esphome/components/espnow/packet_transport/* @EasilyBoredEngineer esphome/components/ethernet_info/* @gtjadsonsantos @@ -518,6 +519,7 @@ esphome/components/sound_level/* @kahrendt esphome/components/spa06_base/* @danielkent-net esphome/components/spa06_i2c/* @danielkent-net esphome/components/spa06_spi/* @danielkent-net +esphome/components/spd2010/* @grischard esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/docker/Dockerfile b/docker/Dockerfile index 0282011f50..9edbd6e4f9 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.22.0 +RUN uv pip install --no-cache-dir esphome-device-builder==1.23.0 RUN \ platformio settings set enable_telemetry No \ diff --git a/esphome/arduino8266/framework.py b/esphome/arduino8266/framework.py index c94ea1df9c..16fd5d6282 100644 --- a/esphome/arduino8266/framework.py +++ b/esphome/arduino8266/framework.py @@ -49,9 +49,9 @@ class FrameworkRelease(NamedTuple): # Arduino core version -> its build in esphome-libs/arduino-esp8266 FRAMEWORK_RELEASES: dict[Version, FrameworkRelease] = { Version(3, 1, 2): FrameworkRelease( - "3.1.2-esphome.2", - "9377d63f569c524823199b1897ed7fde2063e50e2676dd519a0d758ab73678f7", - 37192128, + "3.1.2-esphome.3", + "3af533a85163804e164500a5b254dc23b922080e25ecb7dc37b3dd8ac84651db", + 37185650, ), } diff --git a/esphome/automation.py b/esphome/automation.py index b69edd04a1..6b6af4ed72 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -1,4 +1,4 @@ -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import dataclass, field import logging import string @@ -23,7 +23,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, EsphomeError, HexInt, Lambda from esphome.cpp_generator import ( - FlashStringLiteral, + Expression, LambdaExpression, MockObj, MockObjClass, @@ -245,9 +245,7 @@ ApplyCondition = cg.esphome_ns.class_("ApplyCondition", Condition) def flash_string(config: ConfigType, value: str) -> str: """Default renderer for ``std::string`` constants; copies the literal out of flash on ESP8266.""" - if CORE.is_esp8266: - return f"progmem_string({FlashStringLiteral(value)})" - return str(cg.safe_exp(value)) + return str(cg.progmem_string(value)) def literal_with_length(config: ConfigType, value: str) -> str: @@ -259,13 +257,24 @@ def literal_with_length(config: ConfigType, value: str) -> str: return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}" +def string_ref_literal(config: ConfigType, value: str) -> str: + """Renderer for a ``StringRef`` comparison: a flash literal on ESP8266, else ``StringRef(literal, length)``.""" + if CORE.is_esp8266: + return str(cg.FlashStringLiteral(value)) + return f"StringRef({literal_with_length(config, value)})" + + +def _names_parent(text: str) -> bool: + return any(f == "parent" for _, f, _, _ in string.Formatter().parse(text)) + + @dataclass(frozen=True) class ApplyCall: """One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``. Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a - path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use - ``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an + path into nested sections. ``target`` and a plain ``str`` ``type_`` (raw C++ type text) may + name the parent object as ``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an id bypasses it. The statement is skipped when none of its keys is set, always emitted when it has no keys, and a partial set is a config error. """ @@ -277,13 +286,13 @@ class ApplyCall: fields = [ f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None ] - if any(fields): + if any(f not in ("", "parent") for f in fields): raise ValueError( - f"apply target {self.target!r}: only bare {{}} placeholders" + f"apply target {self.target!r}: only {{}} and {{parent}} placeholders" ) - if len(fields) != len(self.args): + if (count := fields.count("")) != len(self.args): raise ValueError( - f"apply target {self.target!r} has {len(fields)} " + f"apply target {self.target!r} has {count} " f"placeholder(s) for {len(self.args)} config key(s)" ) if any(len(arg) not in (2, 3) for arg in self.args): @@ -291,6 +300,12 @@ class ApplyCall: f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])" ) + @property + def names_parent(self) -> bool: + return _names_parent(self.target) or any( + isinstance(arg[1], str) and _names_parent(arg[1]) for arg in self.args + ) + @property def members(self) -> list[tuple[Any, Any, Any]]: """Each arg as ``(conf_key, type_, const_fn or None)``.""" @@ -365,9 +380,16 @@ def _check_key_in_schema( schema = schema.schema[markers[part]] +def parent_ref(var: MockObj) -> MockObj: + """``var`` named from global scope, so a trigger argument cannot shadow it. + + Also how a generated callback names its Automation. + """ + return MockObj(f"::{var}", "->") + + async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str: - # Global-scope qualified so a trigger arg named like the id cannot shadow it. - return f"::{await cg.get_variable(config[id_key])}" + return str(parent_ref(await cg.get_variable(config[id_key]))) def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType: @@ -399,7 +421,7 @@ async def _render_values( members: list[tuple[Any, Any, Any]], values: list[Any], config: ConfigType, - parent: str, + parent: str | None, lambda_args: TemplateArgsType, compare: bool = False, ) -> list[str]: @@ -477,12 +499,12 @@ def register_apply_action( statements: list[str] = [] for target, members in statements_spec: values = _apply_values(config, members) - if members and all(value is None for value in values): + if not _apply_call_active(members, values): continue exprs = await _render_values( name, target, members, values, config, parent, lambda_args ) - statements.append(f"{receiver}{target.format(*exprs)};") + statements.append(f"{receiver}{target.format(*exprs, parent=parent)};") if call: statements = [ f"auto apply_call = {parent}->{call}();", @@ -496,6 +518,27 @@ def register_apply_action( register_action(name, ApplyAction, schema, synchronous=True)(builder) +def _apply_call_active(members: list[tuple[Any, Any, Any]], values: list[Any]) -> bool: + """An ``ApplyCall`` is emitted unless it has keys and none of them is set.""" + return not members or any(value is not None for value in values) + + +async def _render_check( + name: str, + target: str, + members: list[tuple[Any, Any, Any]], + values: list[Any], + config: ConfigType, + parent: str | None, + lambda_args: TemplateArgsType, +) -> str: + """Render a boolean check with ``values`` compared against config.""" + exprs = await _render_values( + name, target, members, values, config, parent, lambda_args, compare=True + ) + return target.format(*exprs, parent=parent) + + def register_apply_condition( name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID ) -> None: @@ -521,22 +564,16 @@ def register_apply_condition( ) -> MockObj: parent = await _apply_parent(config, id_key) lambda_args = _apply_lambda_args(args) - exprs = await _render_values( - name, - call.target, - members, - _apply_values(config, members), - config, - parent, - lambda_args, - compare=True, + values = _apply_values(config, members) + check = await _render_check( + name, call.target, members, values, config, parent, lambda_args ) return _apply_function( condition_id, cg.bool_, template_arg, lambda_args, - [f"return {parent}->{call.target.format(*exprs)};"], + [f"return {parent}->{check};"], ) register_condition(name, ApplyCondition, schema)(builder) @@ -672,52 +709,20 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition) @register_condition("and", AndCondition, validate_condition_list) -async def and_condition_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable( - condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions - ) - - -@register_condition("or", OrCondition, validate_condition_list) -async def or_condition_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable( - condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions - ) - - @register_condition("all", AndCondition, validate_condition_list) -async def all_condition_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable( - condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions - ) - - +@register_condition("or", OrCondition, validate_condition_list) @register_condition("any", OrCondition, validate_condition_list) -async def any_condition_to_code( +@register_condition("xor", XorCondition, validate_condition_list) +async def condition_group_to_code( config: ConfigType, condition_id: ID, template_arg: cg.TemplateArguments, args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) + # A group of one is that condition for and, or and xor; skip the wrapper. + if len(conditions) == 1: + return conditions[0] return cg.new_Pvariable( condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions ) @@ -734,19 +739,6 @@ async def not_condition_to_code( return cg.new_Pvariable(condition_id, template_arg, condition) -@register_condition("xor", XorCondition, validate_condition_list) -async def xor_condition_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable( - condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions - ) - - @register_condition("lambda", LambdaCondition, cv.returning_lambda) async def lambda_condition_to_code( config: ConfigType, @@ -1056,23 +1048,78 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool: return False -async def build_automation( - trigger: MockObj, args: TemplateArgsType, config: ConfigType +async def _new_automation( + args: TemplateArgsType, config: ConfigType, *ctor_args: MockObj ) -> MockObj: - arg_types = [arg[0] for arg in args] - templ = cg.TemplateArguments(*arg_types) - obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger) + """Create the Automation for ``config`` with its actions.""" + templ = cg.TemplateArguments(*(arg[0] for arg in args)) + obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, *ctor_args) actions = await build_action_list(config[CONF_THEN], templ, args) cg.add(obj.add_actions(actions)) return obj +async def build_automation( + trigger: MockObj, args: TemplateArgsType, config: ConfigType +) -> MockObj: + return await _new_automation(args, config, trigger) + + +async def build_trigger_callback( + args: TemplateArgsType, + config: ConfigType, + params: TemplateArgsType, + forward: Sequence[str | Expression] | None = None, + when: str | ApplyCall | None = None, + parent: MockObj | None = None, +) -> LambdaExpression: + """Build the Automation for ``config`` and return a stateless callback that triggers it. + + ``params`` are the parent callback's parameters, ``forward`` the expressions passed to + ``trigger()`` (default: the parameter names; write the parent as ``parent_ref(var)``), + ``when`` a filter the callback returns early on, skipped like any ``ApplyCall`` when none + of its keys is set. ``when`` may name ``parent`` as ``{parent}``, e.g. + ``"{parent}->is_fully_open()"``. + """ + members: list[tuple[Any, Any, Any]] = [] + if when is not None: + call = when if isinstance(when, ApplyCall) else ApplyCall(when) + members = call.members + if parent is None and call.names_parent: + raise ValueError(f"trigger filter {call.target!r} names {{parent}}") + obj = await _new_automation(args, config) + lambda_args = _apply_lambda_args(params) + statements: list[str] = [] + if when is not None: + values = _apply_values(config, members) + if _apply_call_active(members, values): + check = await _render_check( + "trigger filter", + call.target, + members, + values, + config, + None if parent is None else str(parent_ref(parent)), + lambda_args, + ) + statements.append(f"if (!({check}))\n return;") + if forward is None: + forward = [name for _, name in params] + statements.append(f"{parent_ref(obj)}->trigger({', '.join(map(str, forward))});") + return LambdaExpression( + ["\n".join(statements)], lambda_args, capture="", return_type=cg.void + ) + + async def build_callback_automation( parent: MockObj, callback_method: str, args: TemplateArgsType, config: ConfigType, forwarder: MockObj | MockObjClass | None = None, + params: TemplateArgsType | None = None, + forward: Sequence[str | Expression] | None = None, + when: str | ApplyCall | None = None, ) -> None: """Build an Automation and register it as a callback on the parent. @@ -1084,6 +1131,9 @@ async def build_callback_automation( pointer-sized (single Automation* field) to fit inline in Callback::ctx_ and avoid heap allocation. + With ``params``, ``forward`` or ``when`` the callback is instead the stateless + lambda of ``build_trigger_callback``; ``forwarder`` cannot be combined with them. + :param parent: The component object (e.g., button, sensor). :param callback_method: Name of the callback method (e.g., "add_on_press_callback"). :param args: Automation template args as list of (type, name) tuples. @@ -1092,22 +1142,56 @@ async def build_callback_automation( TriggerForwarder. Pass any struct type whose aggregate init takes a single Automation pointer (e.g., TriggerOnTrueForwarder). """ - arg_types = [arg[0] for arg in args] - templ = cg.TemplateArguments(*arg_types) - obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ) - actions = await build_action_list(config[CONF_THEN], templ, args) - cg.add(obj.add_actions(actions)) + if params is not None or forward is not None or when is not None: + if forwarder is not None: + raise ValueError( + "forwarder cannot be combined with params, forward or when" + ) + callback = await build_trigger_callback( + args, config, args if params is None else params, forward, when, parent + ) + cg.add(getattr(parent, callback_method)(callback)) + return + obj = await _new_automation(args, config) # Use template forwarder structs for deduplication. The compiler generates # one operator() per forwarder type; different automation pointers are just # data in the struct. if forwarder is None: - forwarder = TriggerForwarder.template(templ) + forwarder = TriggerForwarder.template(*(arg[0] for arg in args)) # RawExpression for aggregate init — both forwarder and obj are codegen # MockObjs (not user input), and there's no Expression type for positional # aggregate initialization (StructInitializer uses named fields). cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}"))) +async def build_parent_callback_automation( + parent: MockObj, callback_method: str, arg: tuple[Any, str], config: ConfigType +) -> None: + """Register an Automation that receives ``parent`` from a callback that carries nothing. + + ``arg`` is the automation's ``(type, name)``, e.g. ``(Fan.operator("ptr"), "x")``. + """ + await build_callback_automation( + parent, callback_method, [arg], config, params=[], forward=[parent_ref(parent)] + ) + + +async def build_trigger_automations( + parent: MockObj | None, + config: ConfigType, + entries: tuple[tuple[str, TemplateArgsType], ...], +) -> None: + """Instantiate each entry's Trigger class, with ``parent`` when given, and build its automations. + + ``entries`` are ``(conf_key, args)`` pairs; the class comes from the entry's ``CONF_TRIGGER_ID``. + """ + ctor_args = () if parent is None else (parent,) + for conf_key, args in entries: + for conf in config.get(conf_key, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], *ctor_args) + await build_automation(trigger, args, conf) + + @dataclass(frozen=True, slots=True) class CallbackAutomation: """A single callback automation entry for build_callback_automations.""" @@ -1116,6 +1200,9 @@ class CallbackAutomation: callback_method: str args: TemplateArgsType = field(default_factory=list) forwarder: MockObj | MockObjClass | None = None + params: TemplateArgsType | None = None + forward: Sequence[str | Expression] | None = None + when: str | ApplyCall | None = None async def build_callback_automations( @@ -1137,4 +1224,7 @@ async def build_callback_automations( entry.args, conf, forwarder=entry.forwarder, + params=entry.params, + forward=entry.forward, + when=entry.when, ) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index e7a2dc1536..a826a2b628 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -237,6 +237,35 @@ def idf_macro_matches(idf_path: Path) -> bool: return live == _EXPECTED_MACRO +def _cmake_args_for_write(minimal: bool) -> dict[str, str]: + """The cg.add_cmake_arg() values for one project write. + + The discovery (minimal) write lets every built-in component register so + the list it finds, cached per target, serves any exclusion set; the full + write drops the excluded names from that list and passes all of them to + IDF. Names outside the framework's components directory (managed + components) stay excluded on the discovery write too, since the list + only holds built-in ones. A checkout supplied through IDF_PATH caches + nothing, so its discovery keeps every exclusion. + """ + from esphome.espidf.toolchain import _esphome_manages_idf, _get_idf_path + + args = dict(CORE.cmake_args) + if ( + not minimal + or not _esphome_manages_idf() + or not (excluded := args.get("EXCLUDE_COMPONENTS")) + ): + return args + root = _get_idf_path() / "components" + outside = [name for name in excluded.split(";") if not (root / name).is_dir()] + if outside: + args["EXCLUDE_COMPONENTS"] = ";".join(outside) + else: + del args["EXCLUDE_COMPONENTS"] + return args + + def get_project_cmakelists( minimal: bool = False, builtin_components: list[str] | None = None ) -> str: @@ -300,11 +329,10 @@ def get_project_cmakelists( # CMake variables registered via cg.add_cmake_arg(). Emitted before # include(project.cmake) so values like EXCLUDE_COMPONENTS are already - # set when project.cmake seeds the component list, and on minimal - # (discovery) writes too so excluded components never register. + # set when project.cmake seeds the component list. cmake_args = "\n".join( f"set({name} {_cmake_quote(value)})" - for name, value in sorted(CORE.cmake_args.items()) + for name, value in sorted(_cmake_args_for_write(minimal).items()) ) # Per-project list exposed as a CMake variable so converted PIO libs @@ -327,10 +355,10 @@ def get_project_cmakelists( # component's REQUIRES including real IDF components). Referenced by # src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped # on minimal writes because project_description.json may be stale. - # Excluded components are dropped here as well: a stale - # project_description.json from a build without exclusions may still - # list them, and requiring an excluded component pulls it back into - # the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS). + # Excluded components are dropped here as well: the discovery list + # (cached per target) still holds them, and requiring an excluded + # component pulls it back into the build (IDF requirement expansion + # overrides EXCLUDE_COMPONENTS). # Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the # two can never disagree within one generated file. builtin_components_property = ( @@ -397,15 +425,24 @@ project({CORE.name}) # Emit per-memory-type JSON size data for ESPHome to read post-build. # json2 stays small; raw dumps every symbol (~2s on a large map) and # this command runs inside the link edge, blocking everything downstream. -# The map is a BYPRODUCT so ninja knows the link writes it; IDF's size -# target depends on the map and can then be built in the same run as all. -# IDF's cmakev2 declares the map itself, so drop this line on that switch. +# IDF's size target depends on the map, so ninja has to know the link +# writes it for size to build in the same run as all. IDF declares the +# map itself since espressif/esp-idf#19201 (and in cmakev2), and ninja +# rejects two rules for one output, so declare it here only when IDF has +# not. add_custom_command(OUTPUT) marks its outputs GENERATED. +get_source_file_property(esphome_map_declared + ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map GENERATED) +if(esphome_map_declared) + set(esphome_map_byproducts) +else() + set(esphome_map_byproducts BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map) +endif() add_custom_command( TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2 -o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json ${{CMAKE_PROJECT_NAME}}.map - BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map + ${{esphome_map_byproducts}} WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}} VERBATIM ) @@ -554,10 +591,9 @@ def write_project( ) # Snapshot the exclusion set so has_outdated_files() can trigger a - # discovery reconfigure when it changes. Excluded components never - # register in project_description.json, so re-including one (e.g. a - # config gains mqtt) requires a fresh discovery pass before the - # ESPHOME_PROJECT_BUILTIN_COMPONENTS property can list it. + # reconfigure when it changes. The ESPHOME_PROJECT_BUILTIN_COMPONENTS + # property drops excluded names, so re-including one (e.g. a config + # gains mqtt) needs a reconfigure to rebuild it from the discovery list. write_file_if_changed( CORE.relative_build_path("exclude_components.esphomeinternal"), ";".join(get_excluded_builtin_components()), diff --git a/esphome/codegen.py b/esphome/codegen.py index e7c08048b9..4db01a2174 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -34,11 +34,13 @@ from esphome.cpp_generator import ( # noqa: F401 extern_progmem_array, get_variable, get_variable_with_full_id, + is_static_pointer, is_template, new_Pvariable, new_variable, process_lambda, progmem_array, + progmem_string, safe_exp, set_cpp_standard, shared_progmem_array, diff --git a/esphome/components/a01nyub/__init__.py b/esphome/components/a01nyub/__init__.py index 4c84847fb6..45206b66bc 100644 --- a/esphome/components/a01nyub/__init__.py +++ b/esphome/components/a01nyub/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@MrSuicideParrot"] +DOMAIN = "a01nyub" diff --git a/esphome/components/a02yyuw/__init__.py b/esphome/components/a02yyuw/__init__.py index 6724dbb970..f6a28dfa18 100644 --- a/esphome/components/a02yyuw/__init__.py +++ b/esphome/components/a02yyuw/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@TH-Braemer"] +DOMAIN = "a02yyuw" diff --git a/esphome/components/a4988/__init__.py b/esphome/components/a4988/__init__.py index e69de29bb2..55797725d6 100644 --- a/esphome/components/a4988/__init__.py +++ b/esphome/components/a4988/__init__.py @@ -0,0 +1 @@ +DOMAIN = "a4988" diff --git a/esphome/components/absolute_humidity/__init__.py b/esphome/components/absolute_humidity/__init__.py index 8f113b48f6..5cfae55a40 100644 --- a/esphome/components/absolute_humidity/__init__.py +++ b/esphome/components/absolute_humidity/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@DAVe3283"] +DOMAIN = "absolute_humidity" diff --git a/esphome/components/ac_dimmer/__init__.py b/esphome/components/ac_dimmer/__init__.py index e69de29bb2..7025ecff72 100644 --- a/esphome/components/ac_dimmer/__init__.py +++ b/esphome/components/ac_dimmer/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ac_dimmer" diff --git a/esphome/components/adalight/__init__.py b/esphome/components/adalight/__init__.py index afdfefaba6..caf66167b1 100644 --- a/esphome/components/adalight/__init__.py +++ b/esphome/components/adalight/__init__.py @@ -9,6 +9,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType DEPENDENCIES = ["uart"] +DOMAIN = "adalight" adalight_ns = cg.esphome_ns.namespace("adalight") AdalightLightEffect = adalight_ns.class_( diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index c397e746b0..6966a86f6b 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -22,6 +22,7 @@ from esphome.core import CORE from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "adc" adc_ns = cg.esphome_ns.namespace("adc") diff --git a/esphome/components/adc128s102/__init__.py b/esphome/components/adc128s102/__init__.py index 684147752d..125b4113c5 100644 --- a/esphome/components/adc128s102/__init__.py +++ b/esphome/components/adc128s102/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType DEPENDENCIES = ["spi"] +DOMAIN = "adc128s102" MULTI_CONF = True CODEOWNERS = ["@DeerMaximum"] diff --git a/esphome/components/addressable_light/__init__.py b/esphome/components/addressable_light/__init__.py index e69de29bb2..28a33c66d8 100644 --- a/esphome/components/addressable_light/__init__.py +++ b/esphome/components/addressable_light/__init__.py @@ -0,0 +1 @@ +DOMAIN = "addressable_light" diff --git a/esphome/components/ade7880/__init__.py b/esphome/components/ade7880/__init__.py index e69de29bb2..f3dd325f5c 100644 --- a/esphome/components/ade7880/__init__.py +++ b/esphome/components/ade7880/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ade7880" diff --git a/esphome/components/ade7953/__init__.py b/esphome/components/ade7953/__init__.py index d3078a0b67..512ac1a3f3 100644 --- a/esphome/components/ade7953/__init__.py +++ b/esphome/components/ade7953/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@angelnu"] +DOMAIN = "ade7953" diff --git a/esphome/components/ade7953_base/__init__.py b/esphome/components/ade7953_base/__init__.py index 71250ac94e..808de99984 100644 --- a/esphome/components/ade7953_base/__init__.py +++ b/esphome/components/ade7953_base/__init__.py @@ -27,6 +27,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@angelnu"] +DOMAIN = "ade7953_base" CONF_CURRENT_A = "current_a" CONF_CURRENT_B = "current_b" diff --git a/esphome/components/ade7953_i2c/__init__.py b/esphome/components/ade7953_i2c/__init__.py index d3078a0b67..691ceb0274 100644 --- a/esphome/components/ade7953_i2c/__init__.py +++ b/esphome/components/ade7953_i2c/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@angelnu"] +DOMAIN = "ade7953_i2c" diff --git a/esphome/components/ade7953_spi/__init__.py b/esphome/components/ade7953_spi/__init__.py index d3078a0b67..71ad260db2 100644 --- a/esphome/components/ade7953_spi/__init__.py +++ b/esphome/components/ade7953_spi/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@angelnu"] +DOMAIN = "ade7953_spi" diff --git a/esphome/components/ads1115/__init__.py b/esphome/components/ads1115/__init__.py index b42ee918c5..1b8405322f 100644 --- a/esphome/components/ads1115/__init__.py +++ b/esphome/components/ads1115/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType DEPENDENCIES = ["i2c"] +DOMAIN = "ads1115" MULTI_CONF = True ads1115_ns = cg.esphome_ns.namespace("ads1115") diff --git a/esphome/components/ads1118/__init__.py b/esphome/components/ads1118/__init__.py index 956b9a0c1f..98129ecbea 100644 --- a/esphome/components/ads1118/__init__.py +++ b/esphome/components/ads1118/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@solomondg1"] DEPENDENCIES = ["spi"] +DOMAIN = "ads1118" MULTI_CONF = True CONF_ADS1118_ID = "ads1118_id" diff --git a/esphome/components/ags10/__init__.py b/esphome/components/ags10/__init__.py index 37f34d06df..8d2bbd6fb4 100644 --- a/esphome/components/ags10/__init__.py +++ b/esphome/components/ags10/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@mak-42"] +DOMAIN = "ags10" diff --git a/esphome/components/aht10/__init__.py b/esphome/components/aht10/__init__.py index e69de29bb2..888d442fdf 100644 --- a/esphome/components/aht10/__init__.py +++ b/esphome/components/aht10/__init__.py @@ -0,0 +1 @@ +DOMAIN = "aht10" diff --git a/esphome/components/aic3204/__init__.py b/esphome/components/aic3204/__init__.py index e69de29bb2..c7f7f00960 100644 --- a/esphome/components/aic3204/__init__.py +++ b/esphome/components/aic3204/__init__.py @@ -0,0 +1 @@ +DOMAIN = "aic3204" diff --git a/esphome/components/airthings_ble/__init__.py b/esphome/components/airthings_ble/__init__.py index 44534b80e9..48cebad5fb 100644 --- a/esphome/components/airthings_ble/__init__.py +++ b/esphome/components/airthings_ble/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["ble_device_base"] CODEOWNERS = ["@jeromelaban"] +DOMAIN = "airthings_ble" airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble") AirthingsListener = airthings_ble_ns.class_( diff --git a/esphome/components/airthings_wave_base/__init__.py b/esphome/components/airthings_wave_base/__init__.py index 2cec58934d..035142193a 100644 --- a/esphome/components/airthings_wave_base/__init__.py +++ b/esphome/components/airthings_wave_base/__init__.py @@ -24,6 +24,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@ncareau", "@jeromelaban"] +DOMAIN = "airthings_wave_base" DEPENDENCIES = ["ble_client"] diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.cpp b/esphome/components/airthings_wave_base/airthings_wave_base.cpp index d67a2e4a0f..f4dfaaee32 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.cpp +++ b/esphome/components/airthings_wave_base/airthings_wave_base.cpp @@ -10,6 +10,9 @@ namespace esphome::airthings_wave_base { ESPHOME_LOG_TAG(TAG, "airthings_wave_base"); +static constexpr uint32_t BATTERY_READ_TIMEOUT_ID = 0; +static constexpr uint32_t RESPONSE_TIMEOUT_ID = 1; + void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { @@ -181,7 +184,7 @@ void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) { // read the battery again at the configured update interval if (this->battery_update_interval_ != this->update_interval_) { this->read_battery_next_update_ = false; - this->set_timeout("battery", this->battery_update_interval_, + this->set_timeout(BATTERY_READ_TIMEOUT_ID, this->battery_update_interval_, [this]() { this->read_battery_next_update_ = true; }); } } @@ -204,7 +207,7 @@ void AirthingsWaveBase::response_received_() { } void AirthingsWaveBase::set_response_timeout_() { - this->set_timeout("response_timeout", 30 * 1000, [this]() { + this->set_timeout(RESPONSE_TIMEOUT_ID, 30 * 1000, [this]() { this->responses_pending_ = 1; this->response_received_(); }); diff --git a/esphome/components/airthings_wave_mini/__init__.py b/esphome/components/airthings_wave_mini/__init__.py index 022f35b4cf..64513a448d 100644 --- a/esphome/components/airthings_wave_mini/__init__.py +++ b/esphome/components/airthings_wave_mini/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ncareau"] +DOMAIN = "airthings_wave_mini" diff --git a/esphome/components/airthings_wave_plus/__init__.py b/esphome/components/airthings_wave_plus/__init__.py index e26bfd471b..904aa922ba 100644 --- a/esphome/components/airthings_wave_plus/__init__.py +++ b/esphome/components/airthings_wave_plus/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@jeromelaban", "@precurse"] +DOMAIN = "airthings_wave_plus" diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 78206ff3f8..ac12835433 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -21,6 +21,7 @@ from esphome.core.entity_helpers import ( from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@grahambrown11", "@hwstar"] +DOMAIN = "alarm_control_panel" IS_PLATFORM_COMPONENT = True CONF_ON_TRIGGERED = "on_triggered" diff --git a/esphome/components/alpha3/__init__.py b/esphome/components/alpha3/__init__.py index 7cd320c80f..551edbeb7f 100644 --- a/esphome/components/alpha3/__init__.py +++ b/esphome/components/alpha3/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@jan-hofmeier"] +DOMAIN = "alpha3" diff --git a/esphome/components/am2315c/__init__.py b/esphome/components/am2315c/__init__.py index 2398e4fa23..a0ffa96f5e 100644 --- a/esphome/components/am2315c/__init__.py +++ b/esphome/components/am2315c/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@swoboda1337"] +DOMAIN = "am2315c" diff --git a/esphome/components/am2320/__init__.py b/esphome/components/am2320/__init__.py index e69de29bb2..34e57d220c 100644 --- a/esphome/components/am2320/__init__.py +++ b/esphome/components/am2320/__init__.py @@ -0,0 +1 @@ +DOMAIN = "am2320" diff --git a/esphome/components/am43/__init__.py b/esphome/components/am43/__init__.py index f21a15ce0a..fa3caae506 100644 --- a/esphome/components/am43/__init__.py +++ b/esphome/components/am43/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@buxtronix"] +DOMAIN = "am43" diff --git a/esphome/components/analog_threshold/__init__.py b/esphome/components/analog_threshold/__init__.py index 9ae2df986d..c01f3b40e5 100644 --- a/esphome/components/analog_threshold/__init__.py +++ b/esphome/components/analog_threshold/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ianchi"] +DOMAIN = "analog_threshold" diff --git a/esphome/components/anova/__init__.py b/esphome/components/anova/__init__.py index e69de29bb2..4fdd8de72c 100644 --- a/esphome/components/anova/__init__.py +++ b/esphome/components/anova/__init__.py @@ -0,0 +1 @@ +DOMAIN = "anova" diff --git a/esphome/components/apds9306/__init__.py b/esphome/components/apds9306/__init__.py index 3dc8fcf5ff..b22aecf6d2 100644 --- a/esphome/components/apds9306/__init__.py +++ b/esphome/components/apds9306/__init__.py @@ -2,3 +2,4 @@ # https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf CODEOWNERS = ["@aodrenah"] +DOMAIN = "apds9306" diff --git a/esphome/components/apds9960/__init__.py b/esphome/components/apds9960/__init__.py index 7ac1e5eb32..8c19eec5ca 100644 --- a/esphome/components/apds9960/__init__.py +++ b/esphome/components/apds9960/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType DEPENDENCIES = ["i2c"] +DOMAIN = "apds9960" MULTI_CONF = True CONF_APDS9960_ID = "apds9960_id" diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 5e68e20031..f538c0b5e5 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -8,6 +8,7 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_DESCRIPTION, CONF_HOST from esphome.components.logger import request_log_listener +from esphome.components.network import DOMAIN as NETWORK_DOMAIN # ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external # components and downstream consumers that import them from api @@ -514,7 +515,7 @@ def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType: or host.version != 6 ): return config - network_conf = fv.full_config.get().get("network") or {} + network_conf = fv.full_config.get().get(NETWORK_DOMAIN) or {} if not network_conf.get(CONF_ENABLE_IPV6): raise cv.Invalid( "outgoing_connection host is an IPv6 address but IPv6 is not " diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 0d2ac36937..d4ae82fd9f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -81,6 +81,8 @@ service APIConnection { rpc subscribe_serial_proxy_identity(SubscribeSerialProxyIdentityRequest) returns (void) {} rpc serial_proxy_request(SerialProxyRequest) returns (void) {} rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {} + + rpc sendspin_pairing_token(SendspinPairingTokenRequest) returns (SendspinPairingTokenResponse) {} } @@ -398,6 +400,11 @@ message WizardCapabilities { bool configured = 1; } +message SendspinCapabilities { + // Bitmask of the Sendspin features this device supports + uint32 feature_flags = 1; +} + message DeviceCapabilitiesResponse { option (id) = 150; option (source) = SOURCE_SERVER; @@ -408,6 +415,7 @@ message DeviceCapabilitiesResponse { repeated SerialProxyInfo serial_proxies = 4 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"]; WizardCapabilities wizard = 5 [(field_ifdef) = "USE_API_WIZARD"]; + SendspinCapabilities sendspin = 6 [(field_ifdef) = "USE_SENDSPIN"]; } // ==================== DEVICE WIZARD ==================== @@ -752,7 +760,7 @@ message ListEntitiesLightResponse { bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; - repeated string effects = 11 [(container_pointer_no_template) = "FixedVector"]; + repeated string effects = 11 [(container_pointer_no_template) = "light::LightEffectNames"]; bool disabled_by_default = 13; string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 15; @@ -3069,3 +3077,34 @@ message BluetoothSetConnectionParamsResponse { uint64 address = 1; int32 error = 2; } + +// ==================== SENDSPIN ==================== +enum SendspinPairingTokenStatus { + // The Sendspin client has not started yet, so the token does not exist yet. Ask again later. + SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY = 0; + SENDSPIN_PAIRING_TOKEN_STATUS_OK = 1; + // The device has no API encryption key, so it will not send the token. Ask again once a key is set. + SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED = 2; + // Sendspin is turned off and has not started since boot, so the token does not exist yet. + SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED = 3; + // The Sendspin client failed to start, and will not start until the device reboots. + SENDSPIN_PAIRING_TOKEN_STATUS_FAILED = 4; +} + +// Requests the Sendspin pairing token, which a Sendspin server uses to pair with this device +message SendspinPairingTokenRequest { + option (id) = 159; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SENDSPIN"; + // Empty +} + +message SendspinPairingTokenResponse { + option (id) = 160; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SENDSPIN"; + + SendspinPairingTokenStatus status = 1; + // The pairing token (Sendspin pairing.md "Pairing Token"). Set only when status is OK. + string token = 2 [(max_data_length) = 127]; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 42ef372667..fd51aae1be 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -48,6 +48,9 @@ #ifdef USE_ZWAVE_PROXY #include "esphome/components/zwave_proxy/zwave_proxy.h" #endif +#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF) +#include "esphome/components/sendspin/sendspin_hub.h" +#endif #ifdef USE_WATER_HEATER #include "esphome/components/water_heater/water_heater.h" #endif @@ -641,16 +644,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } - FixedVector effects_list; - if (light->supports_effects()) { - auto &light_effects = light->get_effects(); - effects_list.init(light_effects.size() + 1); - effects_list.push_back("None"); - for (auto *effect : light_effects) { - // c_str() is safe as effect names are null-terminated strings from codegen - effects_list.push_back(effect->get_name().c_str()); - } - } + light::LightEffectNames effects_list(light->get_effects()); msg.effects = &effects_list; return fill_and_encode_entity_info(light, msg, conn, remaining_size); } @@ -1402,6 +1396,40 @@ void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { } #endif +#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF) +bool APIConnection::send_sendspin_pairing_token_response_() { + SendspinPairingTokenResponse resp; +#ifdef USE_API_NOISE + // has_psk() stands in for "this session proved the key": with a key active, plaintext and zero-key sessions + // never reach hello, and activating a key closes every session opened before it. + if (!this->parent_->get_noise_ctx().has_psk()) { + resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED; + return this->send_message(resp); + } + auto token = sendspin_::global_sendspin_hub->get_pairing_token(); + if (token.has_value()) { + resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_OK; + resp.token = StringRef(*token); + } else if (sendspin_::global_sendspin_hub->status_has_error()) { + resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_FAILED; + } else if (sendspin_::global_sendspin_hub->is_disabled()) { + resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED; + } else { + resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY; + } +#else + resp.status = enums::SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED; +#endif + return this->send_message(resp); +} + +void APIConnection::on_sendspin_pairing_token_request() { + if (!this->send_sendspin_pairing_token_response_()) { + this->on_fatal_error(); + } +} +#endif + #ifdef USE_ALARM_CONTROL_PANEL bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE, @@ -1872,7 +1900,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 18; + resp.api_version_minor = 19; // Send only the version string - the client only logs this for debugging and doesn't use it otherwise resp.server_info = ESPHOME_VERSION_REF; resp.name = StringRef(App.get_name()); @@ -2088,6 +2116,9 @@ bool APIConnection::send_device_capabilities_response_() { #endif #ifdef USE_API_WIZARD resp.wizard.configured = true; +#endif +#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF) + resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags(); #endif return this->send_message(resp); } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b7f0bda0ed..9f784185b8 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -223,6 +223,10 @@ class APIConnection final : public APIServerConnectionBase { void on_z_wave_proxy_request(const ZWaveProxyRequest &msg); #endif +#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF) + void on_sendspin_pairing_token_request(); +#endif + #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg); @@ -432,6 +436,9 @@ class APIConnection final : public APIServerConnectionBase { #ifdef USE_VOICE_ASSISTANT bool send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg); #endif +#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF) + bool send_sendspin_pairing_token_response_(); +#endif #ifdef USE_CAMERA void try_send_camera_image_(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 646a8f4211..8847249b17 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -306,6 +306,19 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) { return size; } #endif +#ifdef USE_SENDSPIN +uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) { + const auto &msg = *static_cast(self); + pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags); + return pos; +} +uint32_t SendspinCapabilities::calc_size_msg(const void *self) { + const auto &msg = *static_cast(self); + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, msg.feature_flags); + return size; +} +#endif uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) { const auto &msg = *static_cast(self); #ifdef USE_BLUETOOTH_PROXY @@ -324,6 +337,9 @@ uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__res #endif #ifdef USE_API_WIZARD pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.wizard); +#endif +#ifdef USE_SENDSPIN + pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.sendspin); #endif return pos; } @@ -346,6 +362,9 @@ uint32_t DeviceCapabilitiesResponse::calc_size_msg(const void *self) { #endif #ifdef USE_API_WIZARD size += ProtoSize::calc_message(1, msg.wizard.calculate_size()); +#endif +#ifdef USE_SENDSPIN + size += ProtoSize::calc_message(1, msg.sendspin.calculate_size()); #endif return size; } @@ -669,8 +688,8 @@ uint8_t *ListEntitiesLightResponse::encode_msg(const void *self, uint8_t *__rest if (uint32_t raw = float_to_raw(msg.max_mireds); raw != 0) [[likely]] { pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 85, raw); } - for (const char *it : *msg.effects) { - pos = ProtoEncode::encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, 11, it, strlen(it)); + for (const auto &it : *msg.effects) { + pos = ProtoEncode::encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, 11, it); } pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.disabled_by_default); #ifdef USE_ENTITY_ICON @@ -694,8 +713,8 @@ uint32_t ListEntitiesLightResponse::calc_size_msg(const void *self) { size += ProtoSize::calc_float(1, msg.min_mireds); size += ProtoSize::calc_float(1, msg.max_mireds); if (!msg.effects->empty()) { - for (const char *it : *msg.effects) { - size += ProtoSize::calc_length_force(1, strlen(it)); + for (const auto &it : *msg.effects) { + size += ProtoSize::calc_length_force(1, it.size()); } } size += ProtoSize::calc_bool(1, msg.disabled_by_default); @@ -4229,6 +4248,22 @@ uint32_t BluetoothSetConnectionParamsResponse::calc_size_msg(const void *self) { return size; } #endif +#ifdef USE_SENDSPIN +uint8_t *SendspinPairingTokenResponse::encode_msg(const void *self, + uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) { + const auto &msg = *static_cast(self); + pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast(msg.status)); + pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.token); + return pos; +} +uint32_t SendspinPairingTokenResponse::calc_size_msg(const void *self) { + const auto &msg = *static_cast(self); + uint32_t size = 0; + size += msg.status ? 2 : 0; + size += !msg.token.empty() ? 2 + msg.token.size() : 0; + return size; +} +#endif #ifndef HAS_PROTO_MESSAGE_DUMP static_assert(!std::is_polymorphic_v, "decodable messages carry no vtable"); static_assert(!std::is_polymorphic_v, "decodable messages carry no vtable"); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 9ad114b2ca..17436d33aa 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -383,6 +383,15 @@ enum SerialProxyIdentityFlag : uint32_t { SERIAL_PROXY_IDENTITY_FLAG_CONNECTED = 1, SERIAL_PROXY_IDENTITY_FLAG_ERROR = 2, }; +#ifdef USE_SENDSPIN +enum SendspinPairingTokenStatus : uint32_t { + SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY = 0, + SENDSPIN_PAIRING_TOKEN_STATUS_OK = 1, + SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED = 2, + SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED = 3, + SENDSPIN_PAIRING_TOKEN_STATUS_FAILED = 4, +}; +#endif } // namespace enums @@ -737,10 +746,27 @@ class WizardCapabilities final : public ProtoMessage { protected: }; #endif +#ifdef USE_SENDSPIN +class SendspinCapabilities final : public ProtoMessage { + public: + uint32_t feature_flags{0}; + static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM); + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG); + } + static uint32_t calc_size_msg(const void *self); + uint32_t calculate_size() const { return calc_size_msg(this); } +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif class DeviceCapabilitiesResponse final : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 150; - static constexpr uint8_t ESTIMATED_SIZE = 119; + static constexpr uint8_t ESTIMATED_SIZE = 136; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("device_capabilities_response"); } #endif @@ -758,6 +784,9 @@ class DeviceCapabilitiesResponse final : public ProtoMessage { #endif #ifdef USE_API_WIZARD WizardCapabilities wizard{}; +#endif +#ifdef USE_SENDSPIN + SendspinCapabilities sendspin{}; #endif static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM); uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { @@ -1030,7 +1059,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; - const FixedVector *effects{}; + const light::LightEffectNames *effects{}; static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM); uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG); @@ -4138,5 +4167,28 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage { protected: }; #endif +#ifdef USE_SENDSPIN +class SendspinPairingTokenResponse final : public ProtoMessage { + public: + static constexpr uint16_t MESSAGE_TYPE = 160; + static constexpr uint8_t ESTIMATED_SIZE = 11; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("sendspin_pairing_token_response"); } +#endif + enums::SendspinPairingTokenStatus status{}; + StringRef token{nullptr, 0}; // null until set, encode only + static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM); + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG); + } + static uint32_t calc_size_msg(const void *self); + uint32_t calculate_size() const { return calc_size_msg(this); } +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 196a4249cc..3c6f5302b5 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -928,6 +928,25 @@ template<> const char *proto_enum_to_string(enum return ESPHOME_PSTR("UNKNOWN"); } } +#ifdef USE_SENDSPIN +template<> +const char *proto_enum_to_string(enums::SendspinPairingTokenStatus value) { + switch (value) { + case enums::SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY: + return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_NOT_READY"); + case enums::SENDSPIN_PAIRING_TOKEN_STATUS_OK: + return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_OK"); + case enums::SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED: + return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_ENCRYPTION_REQUIRED"); + case enums::SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED: + return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_DISABLED"); + case enums::SENDSPIN_PAIRING_TOKEN_STATUS_FAILED: + return ESPHOME_PSTR("SENDSPIN_PAIRING_TOKEN_STATUS_FAILED"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} +#endif const char *HelloRequest::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("HelloRequest")); @@ -1096,6 +1115,13 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SENDSPIN +const char *SendspinCapabilities::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities")); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); + return out.c_str(); +} +#endif const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceCapabilitiesResponse")); #ifdef USE_BLUETOOTH_PROXY @@ -1124,6 +1150,11 @@ const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const { out.append(2, ' ').append_p(ESPHOME_PSTR("wizard")).append(": "); this->wizard.dump_to(out); out.append("\n"); +#endif +#ifdef USE_SENDSPIN + out.append(2, ' ').append_p(ESPHOME_PSTR("sendspin")).append(": "); + this->sendspin.dump_to(out); + out.append("\n"); #endif return out.c_str(); } @@ -2944,6 +2975,14 @@ const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const return out.c_str(); } #endif +#ifdef USE_SENDSPIN +const char *SendspinPairingTokenResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinPairingTokenResponse")); + dump_field(out, ESPHOME_PSTR("status"), static_cast(this->status)); + dump_field(out, ESPHOME_PSTR("token"), this->token); + return out.c_str(); +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_includes.h b/esphome/components/api/api_pb2_includes.h index 833e6529e2..e076f98fe3 100644 --- a/esphome/components/api/api_pb2_includes.h +++ b/esphome/components/api/api_pb2_includes.h @@ -15,6 +15,7 @@ #endif #ifdef USE_LIGHT +#include "esphome/components/light/light_effect_names.h" #include "esphome/components/light/light_traits.h" #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index fee775daa0..683bb84c30 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -751,6 +751,15 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui this->on_wizard_input_set_request(msg); break; } +#endif +#ifdef USE_SENDSPIN + case 159 /* SendspinPairingTokenRequest is empty */: { +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_sendspin_pairing_token_request")); +#endif + this->on_sendspin_pairing_token_request(); + break; + } #endif default: break; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 13fc03e03e..7f33e5668c 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -252,6 +252,10 @@ class APIServerConnectionBase { #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; #endif + +#ifdef USE_SENDSPIN + void on_sendspin_pairing_token_request(){}; +#endif }; } // namespace esphome::api diff --git a/esphome/components/api/wizard.py b/esphome/components/api/wizard.py index befed39972..3ab80a063e 100644 --- a/esphome/components/api/wizard.py +++ b/esphome/components/api/wizard.py @@ -11,6 +11,7 @@ from typing import Any from esphome import automation import esphome.codegen as cg from esphome.components.const import CONF_DESCRIPTION +from esphome.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, @@ -265,7 +266,7 @@ def _validate_wizard_input(conf: ConfigType) -> ConfigType: fv.full_config.get(), conf[CONF_ENTITY] ) if ( - declaration.get(CONF_PLATFORM) != "homeassistant" + declaration.get(CONF_PLATFORM) != HOMEASSISTANT_DOMAIN or domain not in WIZARD_INPUT_DOMAINS ): raise cv.Invalid( diff --git a/esphome/components/aqi/__init__.py b/esphome/components/aqi/__init__.py index 17d434294a..02c0bd2228 100644 --- a/esphome/components/aqi/__init__.py +++ b/esphome/components/aqi/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"] +DOMAIN = "aqi" aqi_ns = cg.esphome_ns.namespace("aqi") AQICalculatorType = aqi_ns.enum("AQICalculatorType") diff --git a/esphome/components/aqi/aqi_sensor.cpp b/esphome/components/aqi/aqi_sensor.cpp index d7fb120741..c242ee3913 100644 --- a/esphome/components/aqi/aqi_sensor.cpp +++ b/esphome/components/aqi/aqi_sensor.cpp @@ -5,18 +5,20 @@ namespace esphome::aqi { ESPHOME_LOG_TAG(TAG, "aqi"); +static constexpr uint32_t CALCULATE_DEFER_ID = 0; + void AQISensor::setup() { if (this->pm_2_5_sensor_ != nullptr) { this->pm_2_5_sensor_->add_on_state_callback([this](float value) { this->pm_2_5_value_ = value; // Defer calculation to avoid double-publishing if both sensors update in the same loop - this->defer("update", [this]() { this->calculate_aqi_(); }); + this->defer(CALCULATE_DEFER_ID, [this]() { this->calculate_aqi_(); }); }); } if (this->pm_10_0_sensor_ != nullptr) { this->pm_10_0_sensor_->add_on_state_callback([this](float value) { this->pm_10_0_value_ = value; - this->defer("update", [this]() { this->calculate_aqi_(); }); + this->defer(CALCULATE_DEFER_ID, [this]() { this->calculate_aqi_(); }); }); } } diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py index bd02d22d1b..b7b5b09c62 100644 --- a/esphome/components/as3935/__init__.py +++ b/esphome/components/as3935/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( from esphome.cpp_generator import MockObj from esphome.types import ConfigType +DOMAIN = "as3935" MULTI_CONF = True CONF_AS3935_ID = "as3935_id" diff --git a/esphome/components/as3935_i2c/__init__.py b/esphome/components/as3935_i2c/__init__.py index 83924de760..e1d2d89ec5 100644 --- a/esphome/components/as3935_i2c/__init__.py +++ b/esphome/components/as3935_i2c/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["as3935"] DEPENDENCIES = ["i2c"] +DOMAIN = "as3935_i2c" as3935_i2c_ns = cg.esphome_ns.namespace("as3935_i2c") I2CAS3935 = as3935_i2c_ns.class_("I2CAS3935Component", as3935.AS3935, i2c.I2CDevice) diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py index 332a51c7a9..9313705ab6 100644 --- a/esphome/components/as3935_spi/__init__.py +++ b/esphome/components/as3935_spi/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["as3935"] DEPENDENCIES = ["spi"] +DOMAIN = "as3935_spi" as3935_spi_ns = cg.esphome_ns.namespace("as3935_spi") SPIAS3935 = as3935_spi_ns.class_("SPIAS3935Component", as3935.AS3935, spi.SPIDevice) diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index 780712c3bd..3cc5ed5cc1 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -18,6 +18,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@ammmze"] DEPENDENCIES = ["i2c"] +DOMAIN = "as5600" MULTI_CONF = True as5600_ns = cg.esphome_ns.namespace("as5600") diff --git a/esphome/components/as7341/__init__.py b/esphome/components/as7341/__init__.py index e69de29bb2..bdbe78a4ec 100644 --- a/esphome/components/as7341/__init__.py +++ b/esphome/components/as7341/__init__.py @@ -0,0 +1 @@ +DOMAIN = "as7341" diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 31007022d5..b67202db59 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] +DOMAIN = "async_tcp" def AUTO_LOAD() -> list[str]: diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index a29e166745..50c9080c2d 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -8,6 +8,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@X-Ryl669"] DEPENDENCIES = ["i2c"] +DOMAIN = "at581x" MULTI_CONF = True diff --git a/esphome/components/atc_mithermometer/__init__.py b/esphome/components/atc_mithermometer/__init__.py index e69de29bb2..3d517583e8 100644 --- a/esphome/components/atc_mithermometer/__init__.py +++ b/esphome/components/atc_mithermometer/__init__.py @@ -0,0 +1 @@ +DOMAIN = "atc_mithermometer" diff --git a/esphome/components/atm90e26/__init__.py b/esphome/components/atm90e26/__init__.py index ac441a9c2d..b1047e9290 100644 --- a/esphome/components/atm90e26/__init__.py +++ b/esphome/components/atm90e26/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@danieltwagner"] +DOMAIN = "atm90e26" diff --git a/esphome/components/atm90e32/__init__.py b/esphome/components/atm90e32/__init__.py index 766807872b..d25f5c1ce9 100644 --- a/esphome/components/atm90e32/__init__.py +++ b/esphome/components/atm90e32/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg CODEOWNERS = ["@circuitsetup", "@descipher"] +DOMAIN = "atm90e32" atm90e32_ns = cg.esphome_ns.namespace("atm90e32") ATM90E32Component = atm90e32_ns.class_("ATM90E32Component", cg.Component) diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index 7d4b51db04..ba6544b50b 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -6,6 +6,7 @@ from esphome.core import CoroPriority, coroutine_with_priority from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] +DOMAIN = "audio_adc" IS_PLATFORM_COMPONENT = True audio_adc_ns = cg.esphome_ns.namespace("audio_adc") diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index fbaa72014f..346f1be798 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -7,6 +7,7 @@ from esphome.core import CoroPriority, coroutine_with_priority from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] +DOMAIN = "audio_dac" IS_PLATFORM_COMPONENT = True audio_dac_ns = cg.esphome_ns.namespace("audio_dac") diff --git a/esphome/components/audio_http/__init__.py b/esphome/components/audio_http/__init__.py index e69de29bb2..0126fa0e8b 100644 --- a/esphome/components/audio_http/__init__.py +++ b/esphome/components/audio_http/__init__.py @@ -0,0 +1 @@ +DOMAIN = "audio_http" diff --git a/esphome/components/axs15231/__init__.py b/esphome/components/axs15231/__init__.py index 3246dbed24..bed826cf8b 100644 --- a/esphome/components/axs15231/__init__.py +++ b/esphome/components/axs15231/__init__.py @@ -2,5 +2,6 @@ import esphome.codegen as cg CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["i2c"] +DOMAIN = "axs15231" axs15231_ns = cg.esphome_ns.namespace("axs15231") diff --git a/esphome/components/b_parasite/__init__.py b/esphome/components/b_parasite/__init__.py index e69de29bb2..147319ba95 100644 --- a/esphome/components/b_parasite/__init__.py +++ b/esphome/components/b_parasite/__init__.py @@ -0,0 +1 @@ +DOMAIN = "b_parasite" diff --git a/esphome/components/ballu/__init__.py b/esphome/components/ballu/__init__.py index e69de29bb2..d0d8d1b3b2 100644 --- a/esphome/components/ballu/__init__.py +++ b/esphome/components/ballu/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ballu" diff --git a/esphome/components/bang_bang/__init__.py b/esphome/components/bang_bang/__init__.py index 71a87b6ae5..6536dc2c4f 100644 --- a/esphome/components/bang_bang/__init__.py +++ b/esphome/components/bang_bang/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@OttoWinter"] +DOMAIN = "bang_bang" diff --git a/esphome/components/bedjet/__init__.py b/esphome/components/bedjet/__init__.py index 1b967e665a..61dc73c73d 100644 --- a/esphome/components/bedjet/__init__.py +++ b/esphome/components/bedjet/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@jhansche"] DEPENDENCIES = ["ble_client"] +DOMAIN = "bedjet" MULTI_CONF = True CONF_BEDJET_ID = "bedjet_id" diff --git a/esphome/components/beken_spi_led_strip/__init__.py b/esphome/components/beken_spi_led_strip/__init__.py index e69de29bb2..940eabbeb6 100644 --- a/esphome/components/beken_spi_led_strip/__init__.py +++ b/esphome/components/beken_spi_led_strip/__init__.py @@ -0,0 +1 @@ +DOMAIN = "beken_spi_led_strip" diff --git a/esphome/components/bh1750/__init__.py b/esphome/components/bh1750/__init__.py index e69de29bb2..67b3f5ba5a 100644 --- a/esphome/components/bh1750/__init__.py +++ b/esphome/components/bh1750/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bh1750" diff --git a/esphome/components/bh1900nux/__init__.py b/esphome/components/bh1900nux/__init__.py index e69de29bb2..b5b6c5eac9 100644 --- a/esphome/components/bh1900nux/__init__.py +++ b/esphome/components/bh1900nux/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bh1900nux" diff --git a/esphome/components/binary/__init__.py b/esphome/components/binary/__init__.py index 3f6a45d28a..6244d89b8d 100644 --- a/esphome/components/binary/__init__.py +++ b/esphome/components/binary/__init__.py @@ -1,3 +1,5 @@ import esphome.codegen as cg +DOMAIN = "binary" + binary_ns = cg.esphome_ns.namespace("binary") diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index f5cde2c8e6..60e20aa193 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -106,6 +106,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] +DOMAIN = "binary_sensor" IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/binary_sensor_map/__init__.py b/esphome/components/binary_sensor_map/__init__.py index e69de29bb2..3ac51b590e 100644 --- a/esphome/components/binary_sensor_map/__init__.py +++ b/esphome/components/binary_sensor_map/__init__.py @@ -0,0 +1 @@ +DOMAIN = "binary_sensor_map" diff --git a/esphome/components/bk72xx/__init__.py b/esphome/components/bk72xx/__init__.py index e64237c95a..1db8f5d54f 100644 --- a/esphome/components/bk72xx/__init__.py +++ b/esphome/components/bk72xx/__init__.py @@ -35,6 +35,7 @@ from .boards import BK72XX_BOARD_PINS, BK72XX_BOARDS CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["libretiny"] +DOMAIN = "bk72xx" IS_TARGET_PLATFORM = True COMPONENT_DATA = LibreTinyComponent( diff --git a/esphome/components/bk72xx_ble/__init__.py b/esphome/components/bk72xx_ble/__init__.py index 38cba56c62..44c66919bb 100644 --- a/esphome/components/bk72xx_ble/__init__.py +++ b/esphome/components/bk72xx_ble/__init__.py @@ -38,6 +38,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["bk72xx"] CODEOWNERS = ["@Bl00d-B0b"] +DOMAIN = "bk72xx_ble" _LOGGER = logging.getLogger(__name__) diff --git a/esphome/components/bk72xx_ble_tracker/__init__.py b/esphome/components/bk72xx_ble_tracker/__init__.py index 55013d4087..abef3c7752 100644 --- a/esphome/components/bk72xx_ble_tracker/__init__.py +++ b/esphome/components/bk72xx_ble_tracker/__init__.py @@ -44,6 +44,7 @@ CONF_BK72XX_BLE_ID = "bk72xx_ble_id" DEPENDENCIES = ["bk72xx"] AUTO_LOAD = ["ble_device_base", "bk72xx_ble"] CODEOWNERS = ["@Bl00d-B0b"] +DOMAIN = "bk72xx_ble_tracker" ble_device_base.register_hub_provider("bk72xx_ble_tracker") diff --git a/esphome/components/bl0906/__init__.py b/esphome/components/bl0906/__init__.py index b077792604..b840c0ee99 100644 --- a/esphome/components/bl0906/__init__.py +++ b/esphome/components/bl0906/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@athom-tech", "@tarontop", "@jesserockz"] +DOMAIN = "bl0906" diff --git a/esphome/components/bl0939/__init__.py b/esphome/components/bl0939/__init__.py index 9bd4598dd2..a2c2dd4c7b 100644 --- a/esphome/components/bl0939/__init__.py +++ b/esphome/components/bl0939/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ziceva"] +DOMAIN = "bl0939" diff --git a/esphome/components/bl0940/__init__.py b/esphome/components/bl0940/__init__.py index 066c2818b6..953824be51 100644 --- a/esphome/components/bl0940/__init__.py +++ b/esphome/components/bl0940/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg CODEOWNERS = ["@tobias-", "@dan-s-github"] +DOMAIN = "bl0940" CONF_BL0940_ID = "bl0940_id" bl0940_ns = cg.esphome_ns.namespace("bl0940") diff --git a/esphome/components/bl0942/__init__.py b/esphome/components/bl0942/__init__.py index 38b68d84b5..89141daae3 100644 --- a/esphome/components/bl0942/__init__.py +++ b/esphome/components/bl0942/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@dbuezas", "@dwmw2"] +DOMAIN = "bl0942" diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 810f4b66e3..b783ef1c15 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -22,6 +22,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["esp32_ble_client"] CODEOWNERS = ["@buxtronix", "@clydebarrow"] DEPENDENCIES = ["esp32_ble_tracker"] +DOMAIN = "ble_client" CONF_DESCRIPTOR_UUID = "descriptor_uuid" CONF_ON_NOTIFY = "on_notify" diff --git a/esphome/components/ble_device_base/__init__.py b/esphome/components/ble_device_base/__init__.py index 43ec736727..e8283457c6 100644 --- a/esphome/components/ble_device_base/__init__.py +++ b/esphome/components/ble_device_base/__init__.py @@ -41,6 +41,7 @@ from esphome.core import CORE, ID, KEY_CORE, TimePeriod from esphome.types import ConfigType CODEOWNERS = ["@Bl00d-B0b"] +DOMAIN = "ble_device_base" CONF_BLE_HUB_ID = "ble_hub_id" diff --git a/esphome/components/ble_nus/__init__.py b/esphome/components/ble_nus/__init__.py index 40983c4dbb..54f111e4af 100644 --- a/esphome/components/ble_nus/__init__.py +++ b/esphome/components/ble_nus/__init__.py @@ -21,6 +21,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["zephyr_ble_server", "uart"] CODEOWNERS = ["@tomaszduda23"] +DOMAIN = "ble_nus" ble_nus_ns = cg.esphome_ns.namespace("ble_nus") BLENUS = ble_nus_ns.class_("BLENUS", cg.Component, UARTComponent) diff --git a/esphome/components/ble_presence/__init__.py b/esphome/components/ble_presence/__init__.py index e69de29bb2..0be32a6e95 100644 --- a/esphome/components/ble_presence/__init__.py +++ b/esphome/components/ble_presence/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ble_presence" diff --git a/esphome/components/ble_rssi/__init__.py b/esphome/components/ble_rssi/__init__.py index e69de29bb2..2d31b9aec2 100644 --- a/esphome/components/ble_rssi/__init__.py +++ b/esphome/components/ble_rssi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ble_rssi" diff --git a/esphome/components/ble_scanner/__init__.py b/esphome/components/ble_scanner/__init__.py index e69de29bb2..e687d58e65 100644 --- a/esphome/components/ble_scanner/__init__.py +++ b/esphome/components/ble_scanner/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ble_scanner" diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index c87ad7f595..6332ea6dc1 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -58,6 +58,7 @@ _HUB_PLATFORMS = (PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RP2) DEPENDENCIES = ["api"] CODEOWNERS = ["@jesserockz", "@bdraco"] +DOMAIN = "bluetooth_proxy" _LOGGER = logging.getLogger(__name__) diff --git a/esphome/components/bm8563/__init__.py b/esphome/components/bm8563/__init__.py index 20254a8b00..c3d1fd8cf6 100644 --- a/esphome/components/bm8563/__init__.py +++ b/esphome/components/bm8563/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@abmantis"] +DOMAIN = "bm8563" diff --git a/esphome/components/bme280_base/__init__.py b/esphome/components/bme280_base/__init__.py index 4b079def64..8d54cbed2a 100644 --- a/esphome/components/bme280_base/__init__.py +++ b/esphome/components/bme280_base/__init__.py @@ -20,6 +20,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "bme280_base" bme280_ns = cg.esphome_ns.namespace("bme280_base") BME280Oversampling = bme280_ns.enum("BME280Oversampling") diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 6cc011d76b..96bb56f345 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -13,6 +13,8 @@ namespace esphome::bme280_base { ESPHOME_LOG_TAG(TAG, "bme280.sensor"); +static constexpr uint32_t DATA_TIMEOUT_ID = 0; + static const uint8_t BME280_REGISTER_DIG_T1 = 0x88; static const uint8_t BME280_REGISTER_DIG_T2 = 0x8A; static const uint8_t BME280_REGISTER_DIG_T3 = 0x8C; @@ -221,7 +223,7 @@ void BME280Component::update() { meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; meas_time += 2.3f * oversampling_to_time(this->humidity_oversampling_) + 0.575f; - this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { + this->set_timeout(DATA_TIMEOUT_ID, uint32_t(ceilf(meas_time)), [this]() { uint8_t data[8]; if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) { ESP_LOGW(TAG, "Error reading registers"); diff --git a/esphome/components/bme280_i2c/__init__.py b/esphome/components/bme280_i2c/__init__.py index e69de29bb2..d57004f5fb 100644 --- a/esphome/components/bme280_i2c/__init__.py +++ b/esphome/components/bme280_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bme280_i2c" diff --git a/esphome/components/bme280_spi/__init__.py b/esphome/components/bme280_spi/__init__.py index e69de29bb2..5ca2ba4595 100644 --- a/esphome/components/bme280_spi/__init__.py +++ b/esphome/components/bme280_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bme280_spi" diff --git a/esphome/components/bme680/__init__.py b/esphome/components/bme680/__init__.py index e69de29bb2..1ebff77202 100644 --- a/esphome/components/bme680/__init__.py +++ b/esphome/components/bme680/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bme680" diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 2ff7ca2b8c..6bacb71ff2 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -6,6 +6,8 @@ namespace esphome::bme680 { ESPHOME_LOG_TAG(TAG, "bme680.sensor"); +static constexpr uint32_t DATA_TIMEOUT_ID = 0; + static const uint8_t BME680_REGISTER_COEFF1 = 0x89; static const uint8_t BME680_REGISTER_COEFF2 = 0xE1; @@ -242,7 +244,7 @@ void BME680Component::update() { return; } - this->set_timeout("data", this->calc_meas_duration_(), [this]() { this->read_data_(); }); + this->set_timeout(DATA_TIMEOUT_ID, this->calc_meas_duration_(), [this]() { this->read_data_(); }); } uint8_t BME680Component::calc_heater_resistance_(uint16_t temperature) { diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 35df2a7ea3..190994b2f8 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -9,6 +9,7 @@ CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensor", "text_sensor"] CONFLICTS_WITH = ["bme68x_bsec2"] +DOMAIN = "bme680_bsec" MULTI_CONF = True CONF_BME680_BSEC_ID = "bme680_bsec_id" diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 219f72d3df..3f0082e1af 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -7,6 +7,8 @@ namespace esphome::bme680_bsec { #ifdef USE_BSEC ESPHOME_LOG_TAG(TAG, "bme680_bsec.sensor"); +static constexpr uint32_t READ_TIMEOUT_ID = 0; + static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; std::vector @@ -259,7 +261,7 @@ void BME680BSECComponent::run_() { this->snapshot_state_(); ESP_LOGV(TAG, "Queueing read in %ums", meas_dur); - this->set_timeout("read", meas_dur, [this]() { this->read_(); }); + this->set_timeout(READ_TIMEOUT_ID, meas_dur, [this]() { this->read_(); }); } else { ESP_LOGV(TAG, "Measurement not required"); this->read_(); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 2b113c179f..f5a674d6b9 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -15,6 +15,8 @@ namespace esphome::bme68x_bsec2 { ESPHOME_LOG_TAG(TAG, "bme68x_bsec2.sensor"); +static constexpr uint32_t READ_TIMEOUT_ID = 0; + static constexpr const char *const IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; static bool is_no_new_data_warning(int8_t status) { @@ -285,11 +287,11 @@ void BME68xBSEC2Component::run_() { uint32_t meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %" PRIu32 "us", meas_dur); this->trigger_time_ns_ = curr_time_ns; - this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); + this->set_timeout(READ_TIMEOUT_ID, meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); } else { ESP_LOGV(TAG, "Measurement not required, queueing immediate read"); this->trigger_time_ns_ = curr_time_ns; - this->set_timeout("read", 0, [this]() { this->read_(this->trigger_time_ns_); }); + this->set_timeout(READ_TIMEOUT_ID, 0, [this]() { this->read_(this->trigger_time_ns_); }); } } diff --git a/esphome/components/bme68x_bsec2_i2c/__init__.py b/esphome/components/bme68x_bsec2_i2c/__init__.py index 1da3bdbd6c..8c50f6e165 100644 --- a/esphome/components/bme68x_bsec2_i2c/__init__.py +++ b/esphome/components/bme68x_bsec2_i2c/__init__.py @@ -9,6 +9,7 @@ import esphome.config_validation as cv from esphome.types import ConfigType CODEOWNERS = ["@neffs", "@kbx81"] +DOMAIN = "bme68x_bsec2_i2c" AUTO_LOAD = ["bme68x_bsec2"] DEPENDENCIES = ["i2c"] diff --git a/esphome/components/bmi160/__init__.py b/esphome/components/bmi160/__init__.py index 49b6d0252a..75386e9857 100644 --- a/esphome/components/bmi160/__init__.py +++ b/esphome/components/bmi160/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@flaviut"] +DOMAIN = "bmi160" diff --git a/esphome/components/bmi270/__init__.py b/esphome/components/bmi270/__init__.py index 0e67e41a0e..499e88b5b1 100644 --- a/esphome/components/bmi270/__init__.py +++ b/esphome/components/bmi270/__init__.py @@ -3,6 +3,7 @@ from esphome.components import i2c from esphome.components.motion import MotionComponent CODEOWNERS = ["@clydebarrow"] +DOMAIN = "bmi270" CONF_BMI270_ID = "bmi270_id" # C++ namespace / class diff --git a/esphome/components/bmp085/__init__.py b/esphome/components/bmp085/__init__.py index e69de29bb2..51355f5bc0 100644 --- a/esphome/components/bmp085/__init__.py +++ b/esphome/components/bmp085/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp085" diff --git a/esphome/components/bmp085/bmp085.cpp b/esphome/components/bmp085/bmp085.cpp index 4badfd95f5..b989ed877b 100644 --- a/esphome/components/bmp085/bmp085.cpp +++ b/esphome/components/bmp085/bmp085.cpp @@ -5,6 +5,9 @@ namespace esphome::bmp085 { ESPHOME_LOG_TAG(TAG, "bmp085.sensor"); +static constexpr uint32_t TEMPERATURE_TIMEOUT_ID = 0; +static constexpr uint32_t PRESSURE_TIMEOUT_ID = 1; + static const uint8_t BMP085_ADDRESS = 0x77; static const uint8_t BMP085_REGISTER_AC1_H = 0xAA; static const uint8_t BMP085_REGISTER_CONTROL = 0xF4; @@ -16,7 +19,7 @@ void BMP085Component::update() { if (!this->set_mode_(BMP085_CONTROL_MODE_TEMPERATURE)) return; - this->set_timeout("temperature", 5, [this]() { this->read_temperature_(); }); + this->set_timeout(TEMPERATURE_TIMEOUT_ID, 5, [this]() { this->read_temperature_(); }); } void BMP085Component::setup() { uint8_t data[22]; @@ -84,7 +87,7 @@ void BMP085Component::read_temperature_() { return; } - this->set_timeout("pressure", 26, [this]() { this->read_pressure_(); }); + this->set_timeout(PRESSURE_TIMEOUT_ID, 26, [this]() { this->read_pressure_(); }); } void BMP085Component::read_pressure_() { uint8_t buffer[3]; diff --git a/esphome/components/bmp280/__init__.py b/esphome/components/bmp280/__init__.py index e69de29bb2..99130dd4e1 100644 --- a/esphome/components/bmp280/__init__.py +++ b/esphome/components/bmp280/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp280" diff --git a/esphome/components/bmp280_base/__init__.py b/esphome/components/bmp280_base/__init__.py index a956ed34f6..68d9145378 100644 --- a/esphome/components/bmp280_base/__init__.py +++ b/esphome/components/bmp280_base/__init__.py @@ -17,6 +17,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@ademuri"] +DOMAIN = "bmp280_base" bmp280_ns = cg.esphome_ns.namespace("bmp280_base") BMP280Oversampling = bmp280_ns.enum("BMP280Oversampling") diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 88ff098581..9cb023c688 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -8,6 +8,8 @@ namespace esphome::bmp280_base { ESPHOME_LOG_TAG(TAG, "bmp280.sensor"); +static constexpr uint32_t DATA_TIMEOUT_ID = 0; + static const uint8_t BMP280_REGISTER_STATUS = 0xF3; static const uint8_t BMP280_REGISTER_CONTROL = 0xF4; static const uint8_t BMP280_REGISTER_CONFIG = 0xF5; @@ -166,7 +168,7 @@ void BMP280Component::update() { meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_); meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; - this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { + this->set_timeout(DATA_TIMEOUT_ID, uint32_t(ceilf(meas_time)), [this]() { int32_t t_fine = 0; float temperature = this->read_temperature_(&t_fine); if (std::isnan(temperature)) { diff --git a/esphome/components/bmp280_i2c/__init__.py b/esphome/components/bmp280_i2c/__init__.py index e69de29bb2..38fc9582e5 100644 --- a/esphome/components/bmp280_i2c/__init__.py +++ b/esphome/components/bmp280_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp280_i2c" diff --git a/esphome/components/bmp280_spi/__init__.py b/esphome/components/bmp280_spi/__init__.py index e69de29bb2..2b67436333 100644 --- a/esphome/components/bmp280_spi/__init__.py +++ b/esphome/components/bmp280_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp280_spi" diff --git a/esphome/components/bmp3xx/__init__.py b/esphome/components/bmp3xx/__init__.py index e69de29bb2..076eafa8e7 100644 --- a/esphome/components/bmp3xx/__init__.py +++ b/esphome/components/bmp3xx/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp3xx" diff --git a/esphome/components/bmp3xx_base/__init__.py b/esphome/components/bmp3xx_base/__init__.py index 6d0618e136..de9b227aac 100644 --- a/esphome/components/bmp3xx_base/__init__.py +++ b/esphome/components/bmp3xx_base/__init__.py @@ -17,6 +17,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@martgras", "@latonita"] +DOMAIN = "bmp3xx_base" bmp3xx_ns = cg.esphome_ns.namespace("bmp3xx_base") Oversampling = bmp3xx_ns.enum("Oversampling") diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index ba85a20e3d..b10a7a7a95 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -15,6 +15,8 @@ namespace esphome::bmp3xx_base { ESPHOME_LOG_TAG(TAG, "bmp3xx.sensor"); +static constexpr uint32_t DATA_TIMEOUT_ID = 0; + static const LogString *chip_type_to_str(uint8_t chip_type) { switch (chip_type) { case BMP388_ID: @@ -170,7 +172,7 @@ void BMP3XXComponent::update() { const uint32_t meas_timeout = uint32_t(ceilf(meas_time)); ESP_LOGVV(TAG, "measurement time %" PRIu32, meas_timeout); - this->set_timeout("data", meas_timeout, [this]() { + this->set_timeout(DATA_TIMEOUT_ID, meas_timeout, [this]() { float temperature = 0.0f; float pressure = 0.0f; if (this->pressure_sensor_ != nullptr) { diff --git a/esphome/components/bmp3xx_i2c/__init__.py b/esphome/components/bmp3xx_i2c/__init__.py index e69de29bb2..87cf0e5261 100644 --- a/esphome/components/bmp3xx_i2c/__init__.py +++ b/esphome/components/bmp3xx_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp3xx_i2c" diff --git a/esphome/components/bmp3xx_spi/__init__.py b/esphome/components/bmp3xx_spi/__init__.py index e69de29bb2..6e04124ccb 100644 --- a/esphome/components/bmp3xx_spi/__init__.py +++ b/esphome/components/bmp3xx_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp3xx_spi" diff --git a/esphome/components/bmp581/__init__.py b/esphome/components/bmp581/__init__.py index e69de29bb2..ad0da1d8f7 100644 --- a/esphome/components/bmp581/__init__.py +++ b/esphome/components/bmp581/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp581" diff --git a/esphome/components/bmp581_base/__init__.py b/esphome/components/bmp581_base/__init__.py index 5d031a8208..18c18a2e9b 100644 --- a/esphome/components/bmp581_base/__init__.py +++ b/esphome/components/bmp581_base/__init__.py @@ -19,6 +19,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@kahrendt", "@danielkent-net"] +DOMAIN = "bmp581_base" bmp581_ns = cg.esphome_ns.namespace("bmp581_base") diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index 7d4761f516..29f67fadc5 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -19,6 +19,8 @@ namespace esphome::bmp581_base { ESPHOME_LOG_TAG(TAG, "bmp581"); +static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0; + // Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128 PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); @@ -272,7 +274,7 @@ void BMP581Component::update() { ESP_LOGVV(TAG, "Measurement should take %d ms", this->conversion_time_); - this->set_timeout("measurement", this->conversion_time_, [this]() { + this->set_timeout(MEASUREMENT_TIMEOUT_ID, this->conversion_time_, [this]() { float temperature = 0.0; float pressure = 0.0; diff --git a/esphome/components/bmp581_i2c/__init__.py b/esphome/components/bmp581_i2c/__init__.py index e69de29bb2..a5c70d3d42 100644 --- a/esphome/components/bmp581_i2c/__init__.py +++ b/esphome/components/bmp581_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp581_i2c" diff --git a/esphome/components/bmp581_spi/__init__.py b/esphome/components/bmp581_spi/__init__.py index e69de29bb2..d8f525adce 100644 --- a/esphome/components/bmp581_spi/__init__.py +++ b/esphome/components/bmp581_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "bmp581_spi" diff --git a/esphome/components/bp1658cj/__init__.py b/esphome/components/bp1658cj/__init__.py index b45272d2ea..76b3d427a4 100644 --- a/esphome/components/bp1658cj/__init__.py +++ b/esphome/components/bp1658cj/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@Cossid"] +DOMAIN = "bp1658cj" MULTI_CONF = True CONF_MAX_POWER_COLOR_CHANNELS = "max_power_color_channels" diff --git a/esphome/components/bp5758d/__init__.py b/esphome/components/bp5758d/__init__.py index fa4e8a231b..bd6b6ca110 100644 --- a/esphome/components/bp5758d/__init__.py +++ b/esphome/components/bp5758d/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@Cossid"] +DOMAIN = "bp5758d" MULTI_CONF = True AUTO_LOAD = ["output"] diff --git a/esphome/components/bthome_mithermometer/__init__.py b/esphome/components/bthome_mithermometer/__init__.py index ed0cbaa9e1..fe78162c65 100644 --- a/esphome/components/bthome_mithermometer/__init__.py +++ b/esphome/components/bthome_mithermometer/__init__.py @@ -8,6 +8,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@nagyrobi"] AUTO_LOAD = ["ble_device_base"] +DOMAIN = "bthome_mithermometer" bthome_mithermometer_ns = cg.esphome_ns.namespace("bthome_mithermometer") diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 342f451509..f27475bdca 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -28,6 +28,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@esphome/core"] +DOMAIN = "button" IS_PLATFORM_COMPONENT = True DEVICE_CLASSES = [ diff --git a/esphome/components/bytebuffer/__init__.py b/esphome/components/bytebuffer/__init__.py index 3c7c695118..df0c76338c 100644 --- a/esphome/components/bytebuffer/__init__.py +++ b/esphome/components/bytebuffer/__init__.py @@ -1,4 +1,5 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "bytebuffer" # Allows bytebuffer to be configured in yaml, to allow use of the C++ api. diff --git a/esphome/components/camera/__init__.py b/esphome/components/camera/__init__.py index a19f7707af..0cb916ac49 100644 --- a/esphome/components/camera/__init__.py +++ b/esphome/components/camera/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@DT-art1", "@bdraco"] +DOMAIN = "camera" diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py index 344248fcf3..f63c316126 100644 --- a/esphome/components/camera_encoder/__init__.py +++ b/esphome/components/camera_encoder/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE from esphome.types import ConfigType CODEOWNERS = ["@DT-art1"] +DOMAIN = "camera_encoder" AUTO_LOAD = ["camera"] diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index b70bac7265..51bda0c75d 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -10,6 +10,7 @@ from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@mvturnho", "@danielschramm"] +DOMAIN = "canbus" IS_PLATFORM_COMPONENT = True CONF_CAN_ID = "can_id" diff --git a/esphome/components/cap1188/__init__.py b/esphome/components/cap1188/__init__.py index eff0a05163..24f6e627d2 100644 --- a/esphome/components/cap1188/__init__.py +++ b/esphome/components/cap1188/__init__.py @@ -11,6 +11,7 @@ CONF_ALLOW_MULTIPLE_TOUCHES = "allow_multiple_touches" DEPENDENCIES = ["i2c"] AUTO_LOAD = ["binary_sensor", "output"] CODEOWNERS = ["@mreditor97"] +DOMAIN = "cap1188" cap1188_ns = cg.esphome_ns.namespace("cap1188") CONF_CAP1188_ID = "cap1188_id" diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index e490d89062..3bdea03bf5 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -3,6 +3,7 @@ import logging import esphome.codegen as cg from esphome.components import web_server_base, wifi from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +from esphome.components.wifi import DOMAIN as WIFI_DOMAIN from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -34,6 +35,7 @@ def AUTO_LOAD() -> list[str]: DEPENDENCIES = ["wifi"] CODEOWNERS = ["@esphome/core"] +DOMAIN = "captive_portal" captive_portal_ns = cg.esphome_ns.namespace("captive_portal") CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component) @@ -63,7 +65,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config: ConfigType) -> None: full_config = fv.full_config.get() - wifi_conf = full_config.get("wifi") + wifi_conf = full_config.get(WIFI_DOMAIN) if wifi_conf is None: # This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 65d5d6d9f1..8b9e91f833 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( CODEOWNERS = ["@lygris", "@gabest11"] DEPENDENCIES = ["spi"] +DOMAIN = "cc1101" MULTI_CONF = True ns = cg.esphome_ns.namespace("cc1101") diff --git a/esphome/components/ccs811/__init__.py b/esphome/components/ccs811/__init__.py index e69de29bb2..4a86948d39 100644 --- a/esphome/components/ccs811/__init__.py +++ b/esphome/components/ccs811/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ccs811" diff --git a/esphome/components/cd74hc4067/__init__.py b/esphome/components/cd74hc4067/__init__.py index 5f7778e186..655b8db819 100644 --- a/esphome/components/cd74hc4067/__init__.py +++ b/esphome/components/cd74hc4067/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["sensor", "voltage_sampler"] CODEOWNERS = ["@asoehlke"] +DOMAIN = "cd74hc4067" MULTI_CONF = True cd74hc4067_ns = cg.esphome_ns.namespace("cd74hc4067") diff --git a/esphome/components/cdc_acm_uart/__init__.py b/esphome/components/cdc_acm_uart/__init__.py index 516af84856..9d5c0a3e95 100644 --- a/esphome/components/cdc_acm_uart/__init__.py +++ b/esphome/components/cdc_acm_uart/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@kbx81"] +DOMAIN = "cdc_acm_uart" diff --git a/esphome/components/cdc_acm_uart/bridge/__init__.py b/esphome/components/cdc_acm_uart/bridge/__init__.py index cee048df5d..133ea92a48 100644 --- a/esphome/components/cdc_acm_uart/bridge/__init__.py +++ b/esphome/components/cdc_acm_uart/bridge/__init__.py @@ -36,19 +36,6 @@ CONFIG_SCHEMA = cv.All( ) -def _subtree_references_uart(node: object, uart_id: str) -> bool: - """Return True if any dict in the subtree has a uart_id entry naming this bus.""" - if isinstance(node, dict): - return any( - (key == CONF_UART_ID and str(value) == uart_id) - or _subtree_references_uart(value, uart_id) - for key, value in node.items() - ) - if isinstance(node, list): - return any(_subtree_references_uart(item, uart_id) for item in node) - return False - - def _reject_debug(uart_conf: ConfigType) -> ConfigType: # The worker tasks use the IDF driver directly, so the uart debugger never sees # bridge traffic and its dummy_receiver would drain RX bytes on the main loop. @@ -86,7 +73,7 @@ def _final_validate(config: ConfigType) -> ConfigType: for domain, domain_conf in full_config.items(): if domain == BRIDGE_DOMAIN: continue - if _subtree_references_uart(domain_conf, owned_id): + if uart.subtree_references_uart(domain_conf, owned_id): raise cv.Invalid( f"The {label} '{owned_id}' is also used by '{domain}'; a bridge " f"requires exclusive use of its {label}.", diff --git a/esphome/components/ch422g/__init__.py b/esphome/components/ch422g/__init__.py index 7f0c5bb95e..5110503bf8 100644 --- a/esphome/components/ch422g/__init__.py +++ b/esphome/components/ch422g/__init__.py @@ -18,6 +18,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@jesterret", "@clydebarrow"] DEPENDENCIES = ["i2c"] +DOMAIN = "ch422g" MULTI_CONF = True ch422g_ns = cg.esphome_ns.namespace("ch422g") diff --git a/esphome/components/ch423/__init__.py b/esphome/components/ch423/__init__.py index 9fbf3ea515..3997f9581f 100644 --- a/esphome/components/ch423/__init__.py +++ b/esphome/components/ch423/__init__.py @@ -19,6 +19,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@dwmw2"] DEPENDENCIES = ["i2c"] +DOMAIN = "ch423" MULTI_CONF = True ch423_ns = cg.esphome_ns.namespace("ch423") @@ -59,7 +60,7 @@ def pin_mode_check(pin_config: ConfigType, _: ConfigType) -> None: is_open_drain = pin_config[CONF_MODE][CONF_OPEN_DRAIN] # Track pin modes per CH423 instance in CORE.data - ch423_modes = CORE.data.setdefault(CONF_CH423, {}) + ch423_modes = CORE.data.setdefault(DOMAIN, {}) if ch423_id not in ch423_modes: ch423_modes[ch423_id] = {"gpio_output": None, "gpo_open_drain": None} diff --git a/esphome/components/chsc6x/__init__.py b/esphome/components/chsc6x/__init__.py index 80f3a0b33e..bbdc870773 100644 --- a/esphome/components/chsc6x/__init__.py +++ b/esphome/components/chsc6x/__init__.py @@ -1,2 +1,3 @@ CODEOWNERS = ["@kkosik20"] DEPENDENCIES = ["i2c"] +DOMAIN = "chsc6x" diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index e64092d30e..17aeb1467d 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -62,6 +62,7 @@ from esphome.types import ConfigType, SafeExpType IS_PLATFORM_COMPONENT = True CODEOWNERS = ["@esphome/core"] +DOMAIN = "climate" climate_ns = cg.esphome_ns.namespace("climate") Climate = climate_ns.class_("Climate", cg.EntityBase) diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index f5a31b31c4..f4b43e3a52 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -19,6 +19,7 @@ CONF_SUPPORTS_HEAT_COOL = "supports_heat_cool" DEPENDENCIES = ["remote_transmitter"] AUTO_LOAD = ["sensor", "remote_base"] CODEOWNERS = ["@glmnet"] +DOMAIN = "climate_ir" climate_ir_ns = cg.esphome_ns.namespace("climate_ir") ClimateIR = climate_ir_ns.class_( diff --git a/esphome/components/climate_ir_lg/__init__.py b/esphome/components/climate_ir_lg/__init__.py index e69de29bb2..b62e9d17fb 100644 --- a/esphome/components/climate_ir_lg/__init__.py +++ b/esphome/components/climate_ir_lg/__init__.py @@ -0,0 +1 @@ +DOMAIN = "climate_ir_lg" diff --git a/esphome/components/cm1106/__init__.py b/esphome/components/cm1106/__init__.py index fa3c3f1925..24203304ae 100644 --- a/esphome/components/cm1106/__init__.py +++ b/esphome/components/cm1106/__init__.py @@ -1 +1,3 @@ """CM1106 component for ESPHome.""" + +DOMAIN = "cm1106" diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py index 70240eff07..49978c29c2 100644 --- a/esphome/components/color/__init__.py +++ b/esphome/components/color/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType ColorStruct = cg.esphome_ns.struct("Color") +DOMAIN = "color" INSTANCE_TYPE = ColorStruct MULTI_CONF = True diff --git a/esphome/components/color_temperature/__init__.py b/esphome/components/color_temperature/__init__.py index e69de29bb2..2c38fe6fc6 100644 --- a/esphome/components/color_temperature/__init__.py +++ b/esphome/components/color_temperature/__init__.py @@ -0,0 +1 @@ +DOMAIN = "color_temperature" diff --git a/esphome/components/combination/__init__.py b/esphome/components/combination/__init__.py index e69de29bb2..7625bf2d02 100644 --- a/esphome/components/combination/__init__.py +++ b/esphome/components/combination/__init__.py @@ -0,0 +1 @@ +DOMAIN = "combination" diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index d10490a20c..0488fbb6ab 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -9,6 +9,8 @@ namespace esphome::combination { ESPHOME_LOG_TAG(TAG, "combination"); +static constexpr uint32_t NEW_VALUE_DEFER_ID = 0; + void CombinationComponent::log_config_(const LogString *combo_type) { LOG_SENSOR("", "Combination Sensor:", this); ESP_LOGCONFIG(TAG, " Combination Type: %s", LOG_STR_ARG(combo_type)); @@ -43,8 +45,9 @@ void CombinationNoParameterComponent::setup() { for (const auto &sensor : this->sensors_) { // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result // repeatedly in the same loop if multiple source senors update. - sensor->add_on_state_callback( - [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); + sensor->add_on_state_callback([this](float value) -> void { + this->defer(NEW_VALUE_DEFER_ID, [this, value]() { this->handle_new_value(value); }); + }); } } @@ -119,8 +122,9 @@ void LinearCombinationComponent::setup() { for (auto &source : this->sensor_sources_) { // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result // repeatedly in the same loop if multiple source senors update. - source.sensor->add_on_state_callback( - [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); + source.sensor->add_on_state_callback([this](float value) -> void { + this->defer(NEW_VALUE_DEFER_ID, [this, value]() { this->handle_new_value(value); }); + }); } } diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 6e98162baa..b54d797f09 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -1,6 +1,7 @@ """Constants used by esphome components.""" CODEOWNERS = ["@esphome/core"] +DOMAIN = "const" BYTE_ORDER_LITTLE = "little_endian" BYTE_ORDER_BIG = "big_endian" @@ -26,6 +27,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection" CONF_ENABLED = "enabled" CONF_GYROSCOPE_ODR = "gyroscope_odr" CONF_GYROSCOPE_RANGE = "gyroscope_range" +CONF_HOLD_STATE = "hold_state" CONF_HOST = "host" CONF_HUMIDITY_SOURCE = "humidity_source" CONF_IAQ = "iaq" diff --git a/esphome/components/coolix/__init__.py b/esphome/components/coolix/__init__.py index e69de29bb2..e916a94238 100644 --- a/esphome/components/coolix/__init__.py +++ b/esphome/components/coolix/__init__.py @@ -0,0 +1 @@ +DOMAIN = "coolix" diff --git a/esphome/components/copy/__init__.py b/esphome/components/copy/__init__.py index 7594894650..917991b2fc 100644 --- a/esphome/components/copy/__init__.py +++ b/esphome/components/copy/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg CODEOWNERS = ["@OttoWinter"] +DOMAIN = "copy" copy_ns = cg.esphome_ns.namespace("copy") diff --git a/esphome/components/counter/__init__.py b/esphome/components/counter/__init__.py index c58ce8a01e..a85a88d6e6 100644 --- a/esphome/components/counter/__init__.py +++ b/esphome/components/counter/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "counter" diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 0f5aa9b450..c570f3d4ca 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -62,6 +62,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ] +DOMAIN = "cover" _LOGGER = logging.getLogger(__name__) diff --git a/esphome/components/cs5460a/__init__.py b/esphome/components/cs5460a/__init__.py index e69de29bb2..2ea88409a9 100644 --- a/esphome/components/cs5460a/__init__.py +++ b/esphome/components/cs5460a/__init__.py @@ -0,0 +1 @@ +DOMAIN = "cs5460a" diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp index 6ef4588b17..7c4e484791 100644 --- a/esphome/components/cs5460a/cs5460a.cpp +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -5,6 +5,8 @@ namespace esphome::cs5460a { ESPHOME_LOG_TAG(TAG, "cs5460a"); +static constexpr uint32_t STATUS_CHECK_TIMEOUT_ID = 0; + void CS5460AComponent::write_register_(enum CS5460ARegister addr, uint32_t value) { this->write_byte(CMD_WRITE | (addr << 1)); this->write_byte(value >> 16); @@ -146,16 +148,16 @@ void CS5460AComponent::schedule_next_check_() { */ if (time_left > -15000) { time_left = 1000; - this->status_momentary_warning("warning", 1000); + this->status_momentary_warning(1000); } else { ESP_LOGCONFIG(TAG, "Device officially stuck, resetting"); - this->cancel_timeout("status-check"); + this->cancel_timeout(STATUS_CHECK_TIMEOUT_ID); this->hw_init_(); return; } } - this->set_timeout("status-check", time_left, [this]() { + this->set_timeout(STATUS_CHECK_TIMEOUT_ID, time_left, [this]() { if (!this->check_status_()) this->schedule_next_check_(); }); @@ -177,67 +179,67 @@ bool CS5460AComponent::check_status_() { if (status & (1 << 2)) { clear |= 1 << 2; ESP_LOGE(TAG, "Low supply detected"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 3)) { clear |= 1 << 3; ESP_LOGE(TAG, "Modulator oscillation on current channel"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 4)) { clear |= 1 << 4; ESP_LOGE(TAG, "Modulator oscillation on voltage channel"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 5)) { clear |= 1 << 5; ESP_LOGE(TAG, "Watch-dog timeout"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 11)) { clear |= 1 << 11; ESP_LOGE(TAG, "EOUT Energy Accumulation Register out of range"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 12)) { clear |= 1 << 12; ESP_LOGE(TAG, "Energy out of range"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 13)) { clear |= 1 << 13; ESP_LOGE(TAG, "RMS voltage out of range"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 14)) { clear |= 1 << 14; ESP_LOGE(TAG, "RMS current out of range"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 15)) { clear |= 1 << 15; ESP_LOGE(TAG, "Power calculation out of range"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 16)) { clear |= 1 << 16; ESP_LOGE(TAG, "Voltage out of range"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 17)) { clear |= 1 << 17; ESP_LOGE(TAG, "Current out of range"); - this->status_momentary_warning("warning", 500); + this->status_momentary_warning(500); } if (status & (1 << 19)) { diff --git a/esphome/components/cse7761/__init__.py b/esphome/components/cse7761/__init__.py index e69de29bb2..3db7e9a1d4 100644 --- a/esphome/components/cse7761/__init__.py +++ b/esphome/components/cse7761/__init__.py @@ -0,0 +1 @@ +DOMAIN = "cse7761" diff --git a/esphome/components/cse7766/__init__.py b/esphome/components/cse7766/__init__.py index e69de29bb2..df45f6395c 100644 --- a/esphome/components/cse7766/__init__.py +++ b/esphome/components/cse7766/__init__.py @@ -0,0 +1 @@ +DOMAIN = "cse7766" diff --git a/esphome/components/cst226/__init__.py b/esphome/components/cst226/__init__.py index 847e44dbda..5bccdc1cba 100644 --- a/esphome/components/cst226/__init__.py +++ b/esphome/components/cst226/__init__.py @@ -2,5 +2,6 @@ import esphome.codegen as cg CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["i2c"] +DOMAIN = "cst226" cst226_ns = cg.esphome_ns.namespace("cst226") diff --git a/esphome/components/cst328/__init__.py b/esphome/components/cst328/__init__.py index 374df64898..6b10398969 100644 --- a/esphome/components/cst328/__init__.py +++ b/esphome/components/cst328/__init__.py @@ -2,5 +2,6 @@ import esphome.codegen as cg CODEOWNERS = ["@latonita"] DEPENDENCIES = ["i2c"] +DOMAIN = "cst328" cst328_ns = cg.esphome_ns.namespace("cst328") diff --git a/esphome/components/cst816/__init__.py b/esphome/components/cst816/__init__.py index 674a80b7c1..8d6e15abc5 100644 --- a/esphome/components/cst816/__init__.py +++ b/esphome/components/cst816/__init__.py @@ -2,5 +2,6 @@ import esphome.codegen as cg CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["i2c"] +DOMAIN = "cst816" cst816_ns = cg.esphome_ns.namespace("cst816") diff --git a/esphome/components/cst9220/__init__.py b/esphome/components/cst9220/__init__.py index f97c8944ef..e3de3243e0 100644 --- a/esphome/components/cst9220/__init__.py +++ b/esphome/components/cst9220/__init__.py @@ -2,5 +2,6 @@ import esphome.codegen as cg CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["i2c"] +DOMAIN = "cst9220" cst9220_ns = cg.esphome_ns.namespace("cst9220") diff --git a/esphome/components/ct_clamp/__init__.py b/esphome/components/ct_clamp/__init__.py index e69de29bb2..f7ab2905b2 100644 --- a/esphome/components/ct_clamp/__init__.py +++ b/esphome/components/ct_clamp/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ct_clamp" diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index d9e4b2b8fa..6488c468c3 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -8,6 +8,8 @@ namespace esphome::ct_clamp { ESPHOME_LOG_TAG(TAG, "ct_clamp"); +static constexpr uint32_t READ_TIMEOUT_ID = 0; + void CTClampSensor::dump_config() { LOG_SENSOR("", "CT Clamp Sensor", this); ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f); @@ -21,7 +23,7 @@ void CTClampSensor::update() { this->high_freq_.start(); // Set timeout for ending sampling phase - this->set_timeout("read", this->sample_duration_, [this]() { + this->set_timeout(READ_TIMEOUT_ID, this->sample_duration_, [this]() { this->is_sampling_ = false; this->high_freq_.stop(); diff --git a/esphome/components/current_based/__init__.py b/esphome/components/current_based/__init__.py index e7f41154b7..0ef871eeed 100644 --- a/esphome/components/current_based/__init__.py +++ b/esphome/components/current_based/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@djwmarcx"] +DOMAIN = "current_based" diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 6311fd3cf3..0474397571 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -8,6 +8,8 @@ namespace esphome::current_based { ESPHOME_LOG_TAG(TAG, "current_based.cover"); +static constexpr uint32_t ROLLBACK_TIMEOUT_ID = 0; + using namespace esphome::cover; CoverTraits CurrentBasedCover::get_traits() { @@ -73,7 +75,7 @@ void CurrentBasedCover::loop() { ESP_LOGD(TAG, "'%s' - Obstacle detected during opening.", this->name_.c_str()); this->direction_idle_(); if (this->obstacle_rollback_ != 0) { - this->set_timeout("rollback", 300, [this]() { + this->set_timeout(ROLLBACK_TIMEOUT_ID, 300, [this]() { ESP_LOGD(TAG, "'%s' - Rollback.", this->name_.c_str()); this->target_position_ = clamp(this->position - this->obstacle_rollback_, 0.0F, 1.0F); this->start_direction_(COVER_OPERATION_CLOSING); @@ -94,7 +96,7 @@ void CurrentBasedCover::loop() { ESP_LOGD(TAG, "'%s' - Obstacle detected during closing.", this->name_.c_str()); this->direction_idle_(); if (this->obstacle_rollback_ != 0) { - this->set_timeout("rollback", 300, [this]() { + this->set_timeout(ROLLBACK_TIMEOUT_ID, 300, [this]() { ESP_LOGD(TAG, "'%s' - Rollback.", this->name_.c_str()); this->target_position_ = clamp(this->position + this->obstacle_rollback_, 0.0F, 1.0F); this->start_direction_(COVER_OPERATION_OPENING); diff --git a/esphome/components/custom/__init__.py b/esphome/components/custom/__init__.py index e69de29bb2..4eb4f32c12 100644 --- a/esphome/components/custom/__init__.py +++ b/esphome/components/custom/__init__.py @@ -0,0 +1 @@ +DOMAIN = "custom" diff --git a/esphome/components/custom_component/__init__.py b/esphome/components/custom_component/__init__.py index 982153414d..395f140d6c 100644 --- a/esphome/components/custom_component/__init__.py +++ b/esphome/components/custom_component/__init__.py @@ -1,5 +1,6 @@ import esphome.config_validation as cv +DOMAIN = "custom_component" MULTI_CONF = True CONFIG_SCHEMA = cv.invalid( diff --git a/esphome/components/cwww/__init__.py b/esphome/components/cwww/__init__.py index e69de29bb2..2da0e50fa0 100644 --- a/esphome/components/cwww/__init__.py +++ b/esphome/components/cwww/__init__.py @@ -0,0 +1 @@ +DOMAIN = "cwww" diff --git a/esphome/components/d01/__init__.py b/esphome/components/d01/__init__.py index e69de29bb2..45595aada3 100644 --- a/esphome/components/d01/__init__.py +++ b/esphome/components/d01/__init__.py @@ -0,0 +1 @@ +DOMAIN = "d01" diff --git a/esphome/components/dac7678/__init__.py b/esphome/components/dac7678/__init__.py index 668cc87cec..4ee9f1f541 100644 --- a/esphome/components/dac7678/__init__.py +++ b/esphome/components/dac7678/__init__.py @@ -8,6 +8,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["output"] CODEOWNERS = ["@NickB1"] DEPENDENCIES = ["i2c"] +DOMAIN = "dac7678" MULTI_CONF = True dac7678_ns = cg.esphome_ns.namespace("dac7678") diff --git a/esphome/components/daikin/__init__.py b/esphome/components/daikin/__init__.py index e69de29bb2..cc5617d4d5 100644 --- a/esphome/components/daikin/__init__.py +++ b/esphome/components/daikin/__init__.py @@ -0,0 +1 @@ +DOMAIN = "daikin" diff --git a/esphome/components/daikin_arc/__init__.py b/esphome/components/daikin_arc/__init__.py index f9164fb02b..39d99e9da0 100644 --- a/esphome/components/daikin_arc/__init__.py +++ b/esphome/components/daikin_arc/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@MagicBear"] +DOMAIN = "daikin_arc" diff --git a/esphome/components/daikin_brc/__init__.py b/esphome/components/daikin_brc/__init__.py index 69002c015f..c9352db073 100644 --- a/esphome/components/daikin_brc/__init__.py +++ b/esphome/components/daikin_brc/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@hagak"] +DOMAIN = "daikin_brc" diff --git a/esphome/components/dallas/__init__.py b/esphome/components/dallas/__init__.py index 6c2a9d830e..7d55ae9129 100644 --- a/esphome/components/dallas/__init__.py +++ b/esphome/components/dallas/__init__.py @@ -1,5 +1,6 @@ import esphome.config_validation as cv +DOMAIN = "dallas" MULTI_CONF = True CONFIG_SCHEMA = cv.invalid( diff --git a/esphome/components/dallas_temp/__init__.py b/esphome/components/dallas_temp/__init__.py index 3f73044ca8..6a7af3ad4d 100644 --- a/esphome/components/dallas_temp/__init__.py +++ b/esphome/components/dallas_temp/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ssieb"] +DOMAIN = "dallas_temp" diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 2721383ae8..45101e4981 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -5,6 +5,8 @@ namespace esphome::dallas_temp { ESPHOME_LOG_TAG(TAG, "dallas.temp.sensor"); +static constexpr uint32_t CONVERSION_TIMEOUT_ID = 0; + static const uint8_t DALLAS_MODEL_DS18S20 = 0x10; static const uint8_t DALLAS_MODEL_DS18B20 = 0x28; static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44; @@ -44,7 +46,7 @@ void DallasTemperatureSensor::update() { this->send_command_(DALLAS_COMMAND_START_CONVERSION); - this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] { + this->set_timeout(CONVERSION_TIMEOUT_ID, this->millis_to_wait_for_conversion_(), [this] { if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) { this->publish_state(NAN); return; diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py index c0d7d0aa62..01d0eef857 100644 --- a/esphome/components/daly_bms/__init__.py +++ b/esphome/components/daly_bms/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ADDRESS, CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@s1lvi0"] +DOMAIN = "daly_bms" MULTI_CONF = True DEPENDENCIES = ["uart"] diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 906e9762bf..aca0901d2b 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -22,6 +22,7 @@ dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") # is enabled DEPENDENCIES = ["api"] CODEOWNERS = ["@esphome/core"] +DOMAIN = "dashboard_import" def validate_import_url(value: Any) -> str: diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 3a33e5e004..20fd258cb0 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -33,6 +33,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@rfdarter", "@jesserockz"] +DOMAIN = "datetime" IS_PLATFORM_COMPONENT = True @@ -42,9 +43,6 @@ DateEntity = datetime_ns.class_("DateEntity", DateTimeBase) TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase) DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase) -DateTimeStateTrigger = datetime_ns.class_( - "DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime) -) OnTimeTrigger = datetime_ns.class_( "OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity) @@ -74,11 +72,7 @@ def _validate_time_present(config: ConfigType) -> ConfigType: _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Schema( { - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), } ) @@ -143,9 +137,16 @@ async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None: await mqtt.register_mqtt_component(mqtt_, config) if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) + # The state callback carries nothing; the automation gets the value as an ESPTime. for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf) + await automation.build_callback_automation( + var, + "add_on_state_callback", + [(cg.ESPTime, "x")], + conf, + params=[], + forward=[automation.parent_ref(var).state_as_esptime()], + ) if CONF_TIME_ID in config: rtc = await cg.get_variable(config[CONF_TIME_ID]) diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index f99debb692..fdbb193006 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -31,14 +31,4 @@ class DateTimeBase : public EntityBase { #endif }; -class DateTimeStateTrigger final : public Trigger { - public: - explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) { - parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); }); - } - - protected: - DateTimeBase *parent_; -}; - } // namespace esphome::datetime diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 898d7d6c0e..889cfaaa68 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -17,6 +17,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["logger"] +DOMAIN = "debug" CONF_DEBUG_ID = "debug_id" debug_ns = cg.esphome_ns.namespace("debug") diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 9c7622d116..97f8550c5b 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -58,7 +58,7 @@ void DebugComponent::loop() { if (new_free_heap < this->free_heap_ / 2) { this->free_heap_ = new_free_heap; ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); - this->status_momentary_warning("heap", 1000); + this->status_momentary_warning(1000); } #ifdef USE_SENSOR diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index f020c8cd77..f3a5f3d770 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -1,6 +1,8 @@ #include "debug_component.h" #ifdef USE_ZEPHYR +#include #include +#include "esphome/core/alloc_helpers.h" #include "esphome/core/log.h" #include #include @@ -399,18 +401,17 @@ size_t DebugComponent::get_device_info_(std::span #endif } #endif - auto uicr = [](volatile uint32_t *data, uint8_t size) { - std::string res; + // 13 words of 8 hex digits, separated by spaces + char uicr_buf[13 * 9]; + auto uicr = [&uicr_buf](volatile uint32_t *data, uint8_t size) { + size_t len = 0; for (size_t i = 0; i < size; i++) { - if (i > 0) { - res += ' '; - } - res += format_hex_pretty(data[i], '\0', false); + len = buf_append_printf(uicr_buf, sizeof(uicr_buf), len, i > 0 ? " %08" PRIX32 : "%08" PRIX32, data[i]); } - return res; + return uicr_buf; }; - ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); - ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); + ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13)); + ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12)); #ifdef ESPHOME_LOG_HAS_VERBOSE log_peripherals_info(); #endif diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 06286933b9..2b820c9be6 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -491,6 +491,7 @@ for _name, _call in ( ) +DOMAIN = "deep_sleep" FILTER_SOURCE_FILES = filter_source_files_from_platform( { "deep_sleep_esp32.cpp": { diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index eef392ad5c..7cf90bc33e 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -111,7 +111,17 @@ void DeepSleepComponent::deep_sleep_() { if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); - // Single pin wakeup (ext0) - ESP32, S2, S3 only +#ifndef SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP +#ifdef USE_GPIO_HOLD + // Variants without per-pin deep sleep hold need the global enable + gpio_deep_sleep_hold_en(); +#else + if (this->wakeup_pin_ != nullptr) + gpio_deep_sleep_hold_en(); +#endif +#endif + + // Single pin wakeup (ext0) - ESP32, S2, S3 only #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ !defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) @@ -124,11 +134,6 @@ void DeepSleepComponent::deep_sleep_() { } gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); gpio_hold_en(gpio_pin); -#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP - // Some ESP32 variants support holding a single GPIO during deep sleep without this function - // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep - gpio_deep_sleep_hold_en(); -#endif bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; diff --git a/esphome/components/delonghi/__init__.py b/esphome/components/delonghi/__init__.py index 0a81eb2da7..ba66077c84 100644 --- a/esphome/components/delonghi/__init__.py +++ b/esphome/components/delonghi/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@grob6000"] +DOMAIN = "delonghi" diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 75feaa65af..8f2d5ded20 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -76,6 +76,7 @@ AUTO_LOAD = [ "text_sensor", "valve", ] +DOMAIN = "demo" demo_ns = cg.esphome_ns.namespace("demo") DemoAlarmControlPanel = demo_ns.class_( diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index e85d2a17ba..79cfc8f8ca 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -33,7 +33,7 @@ class DemoAlarmControlPanel final : public AlarmControlPanel, public Component { case ACP_STATE_ARMED_AWAY: if (this->get_requires_code_to_arm()) { if (!code.has_value() || *code != "1234") { - this->status_momentary_error("invalid_code", 5000); + this->status_momentary_error(5000); return; } } @@ -42,7 +42,7 @@ class DemoAlarmControlPanel final : public AlarmControlPanel, public Component { case ACP_STATE_DISARMED: if (this->get_requires_code()) { if (!code.has_value() || *code != "1234") { - this->status_momentary_error("invalid_code", 5000); + this->status_momentary_error(5000); return; } } diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h index aa12c885f4..eeb188a463 100644 --- a/esphome/components/demo/demo_cover.h +++ b/esphome/components/demo/demo_cover.h @@ -36,6 +36,8 @@ class DemoCover final : public cover::Cover, public Component { } protected: + static constexpr uint32_t MOVE_TIMEOUT_ID = 0; + void control(const cover::CoverCall &call) override { auto pos = call.get_position(); if (pos.has_value()) { @@ -43,7 +45,7 @@ class DemoCover final : public cover::Cover, public Component { this->current_operation = target > this->position ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; - this->set_timeout("move", 2000, [this, target]() { + this->set_timeout(MOVE_TIMEOUT_ID, 2000, [this, target]() { this->current_operation = cover::COVER_OPERATION_IDLE; this->position = target; this->publish_state(); @@ -54,7 +56,7 @@ class DemoCover final : public cover::Cover, public Component { this->tilt = *tilt; } if (call.get_stop()) { - this->cancel_timeout("move"); + this->cancel_timeout(MOVE_TIMEOUT_ID); } this->publish_state(); diff --git a/esphome/components/dew_point/__init__.py b/esphome/components/dew_point/__init__.py index 3b852436c3..ab61f55f0b 100644 --- a/esphome/components/dew_point/__init__.py +++ b/esphome/components/dew_point/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@CFlix"] +DOMAIN = "dew_point" diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 296afcd318..d4ec9d6590 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -9,6 +9,7 @@ from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_VOLUME DEPENDENCIES = ["uart"] CODEOWNERS = ["@glmnet"] +DOMAIN = "dfplayer" dfplayer_ns = cg.esphome_ns.namespace("dfplayer") DFPlayer = dfplayer_ns.class_("DFPlayer", cg.Component) diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index 6334e10ac5..d4e7aed91d 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -12,6 +12,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@niklasweber"] DEPENDENCIES = ["uart"] +DOMAIN = "dfrobot_sen0395" MULTI_CONF = True dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395") diff --git a/esphome/components/dht/__init__.py b/esphome/components/dht/__init__.py index 71a87b6ae5..9def421103 100644 --- a/esphome/components/dht/__init__.py +++ b/esphome/components/dht/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@OttoWinter"] +DOMAIN = "dht" diff --git a/esphome/components/dht12/__init__.py b/esphome/components/dht12/__init__.py index e69de29bb2..0078da3300 100644 --- a/esphome/components/dht12/__init__.py +++ b/esphome/components/dht12/__init__.py @@ -0,0 +1 @@ +DOMAIN = "dht12" diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index bb493ae8d3..791242cda1 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -20,11 +20,13 @@ from esphome.const import ( CONF_ON_VALUE, CONF_SWITCH, CONF_TEXT, - CONF_TRIGGER_ID, CONF_TYPE, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@numo68"] +DOMAIN = "display_menu_base" display_menu_base_ns = cg.esphome_ns.namespace("display_menu_base") @@ -85,26 +87,6 @@ MENU_MODES = { CONF_JOYSTICK: MenuMode.MENU_MODE_JOYSTICK, } -DisplayMenuOnEnterTrigger = display_menu_base_ns.class_( - "DisplayMenuOnEnterTrigger", automation.Trigger -) - -DisplayMenuOnLeaveTrigger = display_menu_base_ns.class_( - "DisplayMenuOnLeaveTrigger", automation.Trigger -) - -DisplayMenuOnValueTrigger = display_menu_base_ns.class_( - "DisplayMenuOnValueTrigger", automation.Trigger -) - -DisplayMenuOnNextTrigger = display_menu_base_ns.class_( - "DisplayMenuOnNextTrigger", automation.Trigger -) - -DisplayMenuOnPrevTrigger = display_menu_base_ns.class_( - "DisplayMenuOnPrevTrigger", automation.Trigger -) - def validate_format(format): if re.search(r"^%[+-]*(\d+)?(\.\d+)?[fg]$", format) is None: @@ -128,44 +110,20 @@ MENU_ITEM_COMMON_SCHEMA = cv.Schema( MENU_ITEM_ENTER_LEAVE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend( { - cv.Optional(CONF_ON_ENTER): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayMenuOnEnterTrigger - ), - } - ), - cv.Optional(CONF_ON_LEAVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayMenuOnLeaveTrigger - ), - } - ), + cv.Optional(CONF_ON_ENTER): automation.validate_automation({}), + cv.Optional(CONF_ON_LEAVE): automation.validate_automation({}), } ) MENU_ITEM_VALUE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend( { - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayMenuOnValueTrigger - ), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), } ) MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA = MENU_ITEM_ENTER_LEAVE_SCHEMA.extend( { - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayMenuOnValueTrigger - ), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), } ) @@ -229,20 +187,8 @@ MENU_ITEM_SCHEMA = cv.typed_schema( cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCustom), cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean, cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_ON_NEXT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayMenuOnNextTrigger - ), - } - ), - cv.Optional(CONF_ON_PREV): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayMenuOnPrevTrigger - ), - } - ), + cv.Optional(CONF_ON_NEXT): automation.validate_automation({}), + cv.Optional(CONF_ON_PREV): automation.validate_automation({}), } ), }, @@ -255,20 +201,8 @@ DISPLAY_MENU_BASE_SCHEMA = cv.Schema( cv.Optional(CONF_ACTIVE, default=True): cv.boolean, cv.GenerateID(CONF_ROOT_ITEM_ID): cv.declare_id(MenuItemMenu), cv.Optional(CONF_MODE, default=CONF_ROTARY): cv.enum(MENU_MODES), - cv.Optional(CONF_ON_ENTER): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayMenuOnEnterTrigger - ), - } - ), - cv.Optional(CONF_ON_LEAVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayMenuOnLeaveTrigger - ), - } - ), + cv.Optional(CONF_ON_ENTER): automation.validate_automation({}), + cv.Optional(CONF_ON_LEAVE): automation.validate_automation({}), cv.Required(CONF_ITEMS): cv.All( cv.ensure_list(MENU_ITEM_SCHEMA), cv.Length(min=1) ), @@ -301,6 +235,24 @@ automation.register_apply_condition( ) +# Each item callback carries nothing; the automation receives the item itself. +_ITEM_CALLBACKS = ( + (CONF_ON_ENTER, "add_on_enter_callback"), + (CONF_ON_LEAVE, "add_on_leave_callback"), + (CONF_ON_VALUE, "add_on_value_callback"), + (CONF_ON_NEXT, "add_on_next_callback"), + (CONF_ON_PREV, "add_on_prev_callback"), +) + + +async def _build_item_automations(item: MockObj, config: ConfigType) -> None: + for conf_key, callback_method in _ITEM_CALLBACKS: + for conf in config.get(conf_key, []): + await automation.build_parent_callback_automation( + item, callback_method, (MenuItemConstPtr, "it"), conf + ) + + async def menu_item_to_code(menu, config, parent): if config[CONF_TYPE] in MENU_ITEMS_WITH_SPECIALIZED_CLASSES: item = cg.new_Pvariable(config[CONF_ID]) @@ -339,21 +291,7 @@ async def menu_item_to_code(menu, config, parent): cg.add(item.set_switch_variable(var)) cg.add(item.set_on_text(config[CONF_ON_TEXT])) cg.add(item.set_off_text(config[CONF_OFF_TEXT])) - for conf in config.get(CONF_ON_ENTER, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) - await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) - for conf in config.get(CONF_ON_LEAVE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) - await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) - await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) - for conf in config.get(CONF_ON_NEXT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) - await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) - for conf in config.get(CONF_ON_PREV, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) - await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) + await _build_item_automations(item, config) async def display_menu_to_code(menu, config): @@ -363,9 +301,4 @@ async def display_menu_to_code(menu, config): cg.add(menu.set_mode(config[CONF_MODE])) for c in config[CONF_ITEMS]: await menu_item_to_code(menu, c, root_item) - for conf in config.get(CONF_ON_ENTER, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item) - await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) - for conf in config.get(CONF_ON_LEAVE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item) - await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) + await _build_item_automations(root_item, config) diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h deleted file mode 100644 index 477a971d47..0000000000 --- a/esphome/components/display_menu_base/automation.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "display_menu_base.h" - -namespace esphome::display_menu_base { - -class DisplayMenuOnEnterTrigger final : public Trigger { - public: - explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) { - parent->add_on_enter_callback([this]() { this->trigger(this->parent_); }); - } - - protected: - MenuItem *parent_; -}; - -class DisplayMenuOnLeaveTrigger final : public Trigger { - public: - explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) { - parent->add_on_leave_callback([this]() { this->trigger(this->parent_); }); - } - - protected: - MenuItem *parent_; -}; - -class DisplayMenuOnValueTrigger final : public Trigger { - public: - explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) { - parent->add_on_value_callback([this]() { this->trigger(this->parent_); }); - } - - protected: - MenuItem *parent_; -}; - -class DisplayMenuOnNextTrigger final : public Trigger { - public: - explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) { - parent->add_on_next_callback([this]() { this->trigger(this->parent_); }); - } - - protected: - MenuItemCustom *parent_; -}; - -class DisplayMenuOnPrevTrigger final : public Trigger { - public: - explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) { - parent->add_on_prev_callback([this]() { this->trigger(this->parent_); }); - } - - protected: - MenuItemCustom *parent_; -}; - -} // namespace esphome::display_menu_base diff --git a/esphome/components/dlms_meter/__init__.py b/esphome/components/dlms_meter/__init__.py index 00a1694cc3..eed702b6f1 100644 --- a/esphome/components/dlms_meter/__init__.py +++ b/esphome/components/dlms_meter/__init__.py @@ -1,4 +1,3 @@ -import logging import re from typing import Any @@ -13,12 +12,12 @@ from esphome.const import ( CONF_RECEIVE_TIMEOUT, ) from esphome.core import CORE +from esphome.cpp_generator import MockObj from esphome.types import ConfigType -_LOGGER = logging.getLogger(__name__) - CODEOWNERS = ["@SimonFischer04", "@Tomer27cz", "@latonita", "@PolarGoose"] DEPENDENCIES = ["uart"] +DOMAIN = "dlms_meter" CONF_DLMS_METER_ID = "dlms_meter_id" CONF_DECRYPTION_KEY = "decryption_key" @@ -29,19 +28,17 @@ CONF_SKIP_CRC = "skip_crc" CONF_DEFAULT_OBIS = "default_obis" CONF_PROVIDER = "provider" +DLMS_PARSER_VERSION = "2.2.0" + dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter") DlmsMeterComponent = dlms_meter_component_ns.class_( "DlmsMeterComponent", cg.Component, uart.UARTDevice ) +CustomPattern = dlms_meter_component_ns.struct("CustomPattern") +ObisId = cg.global_ns.namespace("dlms_parser").class_("ObisId") -def obis_code(value: Any) -> str: - # Normalize the OBIS code to the strict A.B.C.D.E.F format - bytes_list = parse_obis_code_bytes(value) - return ".".join(str(b) for b in bytes_list) - - -def parse_obis_code_bytes(value: Any) -> list[int]: +def obis_string_to_byte_list(value: Any) -> list[int]: value = cv.string(value) normalized = re.sub(r"[\-\:\*]", ".", value) parts = normalized.split(".") @@ -59,82 +56,51 @@ def parse_obis_code_bytes(value: Any) -> list[int]: return bytes_list +def to_obis_id_struct(value: list[int]) -> cg.Expression: + return ObisId(*value) + + +_request_sensor_slot = cg.slot_counter("DLMS_MAX_SENSORS") +_request_text_sensor_slot = cg.slot_counter("DLMS_MAX_TEXT_SENSORS") +_request_binary_sensor_slot = cg.slot_counter("DLMS_MAX_BINARY_SENSORS") + + +def register_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None: + _request_sensor_slot(str(hub)) + cg.add(hub.register_sensor(to_obis_id_struct(obis), var)) + + +def register_text_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None: + _request_text_sensor_slot(str(hub)) + cg.add(hub.register_text_sensor(to_obis_id_struct(obis), var)) + + +def register_binary_sensor(hub: MockObj, obis: list[int], var: MockObj) -> None: + _request_binary_sensor_slot(str(hub)) + cg.add(hub.register_binary_sensor(to_obis_id_struct(obis), var)) + + def custom_pattern_dict(value: Any) -> ConfigType: if isinstance(value, str): return {CONF_PATTERN: value} return value -def validate_custom_pattern(value: ConfigType) -> ConfigType: - if CONF_DEFAULT_OBIS in value and CONF_NAME not in value: - raise cv.Invalid(f"'{CONF_DEFAULT_OBIS}' requires '{CONF_NAME}' to be set") - return value - - -def validate_provider_deprecation(config: ConfigType) -> ConfigType: - if CONF_PROVIDER in config: - provider = str(config[CONF_PROVIDER]).lower() - if provider == "netznoe": - _LOGGER.warning( - "The 'provider: netznoe' option is deprecated and will be removed in 2026.11.0. " - "The required custom patterns have been added automatically for this release, but you must update your configuration.\n" - "Please remove the 'provider' key and explicitly replace it with the following:\n\n" - "custom_patterns:\n" - ' - pattern: "L, TSTR"\n' - ' name: "MeterID"\n' - ' default_obis: "0.0.96.1.0.255"\n' - ' - pattern: "F, TDTM"\n' - ' name: "DateTime"\n' - ' default_obis: "0.0.1.0.0.255"\n' - ) - patterns = config.get(CONF_CUSTOM_PATTERNS, []) - - # Ensure "L, TSTR" for MeterID is present - if not any(p.get(CONF_PATTERN) == "L, TSTR" for p in patterns): - patterns.append( - { - CONF_PATTERN: "L, TSTR", - CONF_NAME: "MeterID", - CONF_DEFAULT_OBIS: [0, 0, 96, 1, 0, 255], - CONF_PRIORITY: 0, - } - ) - - # Ensure "F, TDTM" for DateTime is present - if not any(p.get(CONF_PATTERN) == "F, TDTM" for p in patterns): - patterns.append( - { - CONF_PATTERN: "F, TDTM", - CONF_NAME: "DateTime", - CONF_DEFAULT_OBIS: [0, 0, 1, 0, 0, 255], - CONF_PRIORITY: 0, - } - ) - - config[CONF_CUSTOM_PATTERNS] = patterns - else: - _LOGGER.warning( - "The 'provider' option is deprecated and will be removed in 2026.11.0. " - "The dlms_parser library now handles quirks dynamically. " - "Please remove this option from your configuration." - ) - return config - - CUSTOM_PATTERN_SCHEMA = cv.All( custom_pattern_dict, cv.Schema( { cv.Required(CONF_PATTERN): cv.string, - cv.Optional(CONF_NAME): cv.string, + cv.Optional(CONF_NAME, default="CUSTOM"): cv.string, cv.Optional(CONF_PRIORITY, default=0): cv.int_, - cv.Optional(CONF_DEFAULT_OBIS): parse_obis_code_bytes, + cv.Optional( + CONF_DEFAULT_OBIS, default="0.0.0.0.0.0" + ): obis_string_to_byte_list, } ), - validate_custom_pattern, ) -CONFIG_SCHEMA = cv.All( +CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(DlmsMeterComponent), @@ -142,115 +108,57 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_AUTH_KEY): cv.bind_key(name="Authentication key"), cv.Optional(CONF_CUSTOM_PATTERNS): cv.ensure_list(CUSTOM_PATTERN_SCHEMA), cv.Optional(CONF_SKIP_CRC, default=False): cv.boolean, - cv.Optional(CONF_PROVIDER): cv.string, + # Removed in 2026.11.0 - kept to provide helpful error message + # Remove before 2027.5.0 + cv.Optional(CONF_PROVIDER): cv.invalid( + "The 'provider' option has been removed in ESPHome 2026.11.0.\n" + "For 'provider: netznoe', replace it with:\n\n" + "custom_patterns:\n" + ' - pattern: "L, TSTR"\n' + ' name: "MeterID"\n' + ' default_obis: "0.0.96.1.0.255"\n' + ' - pattern: "F, TDTM"\n' + ' name: "DateTime"\n' + ' default_obis: "0.0.1.0.0.255"\n\n' + "For any other provider, remove the option" + ), cv.Optional( CONF_RECEIVE_TIMEOUT, default="1000ms" ): cv.positive_time_period_milliseconds, } ) .extend(uart.UART_DEVICE_SCHEMA) - .extend(cv.COMPONENT_SCHEMA), - validate_provider_deprecation, + .extend(cv.COMPONENT_SCHEMA) ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("dlms_meter", require_rx=True) async def to_code(config: ConfigType) -> None: - dec_key_expr = cg.RawExpression("std::nullopt") - if dec_key := config.get(CONF_DECRYPTION_KEY): - key_bytes = [str(int(dec_key[i : i + 2], 16)) for i in range(0, 32, 2)] - dec_key_expr = cg.RawExpression( - f"std::array{{{', '.join(key_bytes)}}}" + custom_patterns = [ + cg.StructInitializer( + CustomPattern, + (CONF_PATTERN, pattern[CONF_PATTERN]), + (CONF_NAME, pattern[CONF_NAME]), + (CONF_PRIORITY, pattern[CONF_PRIORITY]), + (CONF_DEFAULT_OBIS, to_obis_id_struct(pattern[CONF_DEFAULT_OBIS])), ) - - auth_key_expr = cg.RawExpression("std::nullopt") - if auth_key := config.get(CONF_AUTH_KEY): - key_bytes = [str(int(auth_key[i : i + 2], 16)) for i in range(0, 32, 2)] - auth_key_expr = cg.RawExpression( - f"std::array{{{', '.join(key_bytes)}}}" - ) - - patterns = [] - if custom_patterns := config.get(CONF_CUSTOM_PATTERNS): - for p in custom_patterns: - name_expr = cg.RawExpression("std::nullopt") - if name_val := p.get(CONF_NAME): - name_expr = name_val - - if obis_vals := p.get(CONF_DEFAULT_OBIS): - obis_expr = cg.RawExpression( - f"std::array{{{obis_vals[0]}, {obis_vals[1]}, {obis_vals[2]}, {obis_vals[3]}, {obis_vals[4]}, {obis_vals[5]}}}" - ) - else: - obis_expr = cg.RawExpression("std::nullopt") - - patterns.append( - cg.ArrayInitializer( - p[CONF_PATTERN], - name_expr, - p.get(CONF_PRIORITY, 0), - obis_expr, - ) - ) - - patterns_expr = ( - cg.ArrayInitializer(*patterns) if patterns else cg.RawExpression("{}") - ) + for pattern in config.get(CONF_CUSTOM_PATTERNS, []) + ] var = cg.new_Pvariable( config[CONF_ID], config[CONF_RECEIVE_TIMEOUT], config[CONF_SKIP_CRC], - dec_key_expr, - auth_key_expr, - patterns_expr, + config.get(CONF_DECRYPTION_KEY, cg.nullptr), + config.get(CONF_AUTH_KEY, cg.nullptr), + cg.ArrayInitializer(*custom_patterns, multiline=True), ) - hub_id = config[CONF_ID].id - - sensor_count = 0 - for sens_conf in CORE.config.get("sensor", []): - if ( - sens_conf.get("platform") == "dlms_meter" - and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id - ): - if CONF_OBIS_CODE in sens_conf: - sensor_count += 1 - else: - from .sensor import NUMERIC_KEYS - - sensor_count += sum(1 for key in NUMERIC_KEYS if key in sens_conf) - - text_sensor_count = 0 - for sens_conf in CORE.config.get("text_sensor", []): - if ( - sens_conf.get("platform") == "dlms_meter" - and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id - ): - if CONF_OBIS_CODE in sens_conf: - text_sensor_count += 1 - else: - from .text_sensor import TEXT_KEYS - - text_sensor_count += sum(1 for key in TEXT_KEYS if key in sens_conf) - - binary_sensor_count = 0 - for sens_conf in CORE.config.get("binary_sensor", []): - if ( - sens_conf.get("platform") == "dlms_meter" - and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id - ): - binary_sensor_count += 1 - - cg.add_define("DLMS_MAX_SENSORS", sensor_count) - cg.add_define("DLMS_MAX_TEXT_SENSORS", text_sensor_count) - cg.add_define("DLMS_MAX_BINARY_SENSORS", binary_sensor_count) - await cg.register_component(var, config) await uart.register_uart_device(var, config) if CORE.is_esp32: - esp32.add_idf_component(name="esphome/dlms_parser", ref="1.1.0") + esp32.add_idf_component(name="esphome/dlms_parser", ref=DLMS_PARSER_VERSION) else: - cg.add_library("esphome/dlms_parser", "1.1.0") + cg.add_library("esphome/dlms_parser", DLMS_PARSER_VERSION) diff --git a/esphome/components/dlms_meter/binary_sensor/__init__.py b/esphome/components/dlms_meter/binary_sensor/__init__.py index a15e58b957..2a51ad0990 100644 --- a/esphome/components/dlms_meter/binary_sensor/__init__.py +++ b/esphome/components/dlms_meter/binary_sensor/__init__.py @@ -3,14 +3,20 @@ from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.types import ConfigType -from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code +from .. import ( + CONF_DLMS_METER_ID, + CONF_OBIS_CODE, + DlmsMeterComponent, + obis_string_to_byte_list, + register_binary_sensor, +) DEPENDENCIES = ["dlms_meter"] CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( { cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), - cv.Required(CONF_OBIS_CODE): obis_code, + cv.Required(CONF_OBIS_CODE): obis_string_to_byte_list, } ) @@ -18,4 +24,4 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) var = await binary_sensor.new_binary_sensor(config) - cg.add(hub.register_binary_sensor(config[CONF_OBIS_CODE], var)) + register_binary_sensor(hub, config[CONF_OBIS_CODE], var) diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp index c34f712660..32d1817e0e 100644 --- a/esphome/components/dlms_meter/dlms_meter.cpp +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -1,6 +1,11 @@ +// Ignore Zephyr. It doesn't have any encryption library. +#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST) + #include "dlms_meter.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" +#include #include namespace esphome::dlms_meter { @@ -31,54 +36,40 @@ static void log_callback(dlms_parser::LogLevel level, const char *fmt, va_list a } } -DlmsMeterComponent::DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check, - std::optional> decryption_key, - std::optional> authentication_key, +DlmsMeterComponent::DlmsMeterComponent(const uint32_t receive_timeout_ms, const bool skip_crc_check, + const char *decryption_key, const char *authentication_key, std::vector custom_patterns) : receive_timeout_ms_(receive_timeout_ms), skip_crc_check_(skip_crc_check), custom_patterns_(std::move(custom_patterns)), - parser_(&decryptor_) { + parser_([this](const dlms_parser::AxdrCapture &capture) { this->on_data_(capture); }, &this->decryptor_) { dlms_parser::Logger::set_log_function(log_callback); - if (decryption_key.has_value()) { -#ifdef DLMS_METER_NO_CRYPTO - ESP_LOGE(TAG, "Decryption is not supported on this platform (no compatible crypto library found)"); -#else - auto opt_key = dlms_parser::Aes128GcmDecryptionKey::from_bytes(decryption_key.value()); - if (opt_key) { - this->parser_.set_decryption_key(*opt_key); + if (decryption_key != nullptr) { + const auto key = dlms_parser::Aes128GcmDecryptionKey::from_hex(decryption_key); + if (key.has_value()) { + this->parser_.set_decryption_key(*key); } else { ESP_LOGE(TAG, "Failed to set decryption key: invalid key format"); } -#endif } - if (authentication_key.has_value()) { -#ifdef DLMS_METER_NO_CRYPTO - ESP_LOGE(TAG, "Authentication is not supported on this platform (no compatible crypto library found)"); -#else - auto opt_key = dlms_parser::Aes128GcmAuthenticationKey::from_bytes(authentication_key.value()); - if (opt_key) { - this->parser_.set_authentication_key(*opt_key); + if (authentication_key != nullptr) { + const auto key = dlms_parser::Aes128GcmAuthenticationKey::from_hex(authentication_key); + if (key.has_value()) { + this->parser_.set_authentication_key(*key); } else { ESP_LOGE(TAG, "Failed to set authentication key: invalid key format"); } -#endif } this->parser_.set_skip_crc_check(this->skip_crc_check_); this->parser_.load_default_patterns(); - for (const auto &pattern : this->custom_patterns_) { - if (pattern.default_obis.has_value() && pattern.name.has_value()) { - this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority, - pattern.default_obis.value()); - } else if (pattern.name.has_value()) { - this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority); - } else { - this->parser_.register_pattern(pattern.pattern.c_str()); - } + + for (const auto &custom_pattern : this->custom_patterns_) { + this->parser_.register_pattern(custom_pattern.name, custom_pattern.pattern, custom_pattern.priority, + custom_pattern.default_obis); } } @@ -89,36 +80,36 @@ void DlmsMeterComponent::dump_config() { ESP_LOGCONFIG(TAG, " Receive Timeout: %" PRIu32 " ms", this->receive_timeout_ms_); ESP_LOGCONFIG(TAG, " Skip CRC Check: %s", YESNO(this->skip_crc_check_)); - for (const auto &pattern : this->custom_patterns_) { - if (pattern.default_obis.has_value() && pattern.name.has_value()) { - const auto &obis = pattern.default_obis.value(); - ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d, default_obis: %d.%d.%d.%d.%d.%d)", - pattern.pattern.c_str(), pattern.name->c_str(), pattern.priority, obis[0], obis[1], obis[2], - obis[3], obis[4], obis[5]); - } else if (pattern.name.has_value()) { - ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d)", pattern.pattern.c_str(), - pattern.name->c_str(), pattern.priority); - } else { - ESP_LOGCONFIG(TAG, " Custom Pattern: '%s'", pattern.pattern.c_str()); - } + std::array obis_buf; + + for (const auto &custom_pattern : this->custom_patterns_) { + const auto default_obis = custom_pattern.default_obis.to_string(obis_buf); + ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d, default_obis: %.*s)", custom_pattern.pattern, + custom_pattern.name, custom_pattern.priority, static_cast(default_obis.size()), + default_obis.data()); } #ifdef USE_SENSOR - for (const auto &entry : this->sensors_) { - LOG_SENSOR(" ", "Numeric Sensor (OBIS)", entry.sensor); - ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str()); + for (const auto &item : this->sensors_) { + LOG_SENSOR(" ", "Numeric Sensor (OBIS)", item.sensor); + const auto obis = item.obis.to_string(obis_buf); + ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast(obis.size()), obis.data()); } #endif + #ifdef USE_TEXT_SENSOR - for (const auto &entry : this->text_sensors_) { - LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", entry.sensor); - ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str()); + for (const auto &item : this->text_sensors_) { + LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", item.sensor); + const auto obis = item.obis.to_string(obis_buf); + ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast(obis.size()), obis.data()); } #endif + #ifdef USE_BINARY_SENSOR - for (const auto &entry : this->binary_sensors_) { - LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", entry.sensor); - ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str()); + for (const auto &item : this->binary_sensors_) { + LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", item.sensor); + const auto obis = item.obis.to_string(obis_buf); + ESP_LOGCONFIG(TAG, " OBIS: %.*s", static_cast(obis.size()), obis.data()); } #endif } @@ -166,71 +157,73 @@ void DlmsMeterComponent::read_rx_buffer_() { void DlmsMeterComponent::process_frame_() { ESP_LOGV(TAG, "Processing frame of size: %zu bytes", this->bytes_accumulated_); - auto callback = [this](const char *obis_code, float float_val, const char *str_val, bool is_numeric) { - this->on_data_(obis_code, float_val, str_val, is_numeric); - }; - - this->parser_.parse({this->rx_buffer_.data(), this->bytes_accumulated_}, callback); + this->parser_.parse({this->rx_buffer_.data(), this->bytes_accumulated_}); this->bytes_accumulated_ = 0; } -void DlmsMeterComponent::on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric) { +void DlmsMeterComponent::on_data_(const dlms_parser::AxdrCapture &capture) { int updated_count = 0; + if (capture.is_numeric()) { +#if defined(USE_SENSOR) || defined(USE_BINARY_SENSOR) + const float value = capture.value_as_float_with_scaler_applied(); +#endif #ifdef USE_SENSOR - if (is_numeric) { - for (auto &item : this->sensors_) { - if (item.obis_code == obis_code) { - item.sensor->publish_state(float_val); + for (const auto &item : this->sensors_) { + if (item.obis == capture.obis) { + item.sensor->publish_state(value); updated_count++; } } - } #endif - -#ifdef USE_TEXT_SENSOR - if (!is_numeric && str_val != nullptr) { - for (auto &item : this->text_sensors_) { - if (item.obis_code == obis_code) { - item.sensor->publish_state(str_val); - updated_count++; - } - } - } -#endif - #ifdef USE_BINARY_SENSOR - if (is_numeric) { - bool state = float_val != 0.0f; - for (auto &item : this->binary_sensors_) { - if (item.obis_code == obis_code) { - item.sensor->publish_state(state); + for (const auto &item : this->binary_sensors_) { + if (item.obis == capture.obis) { + item.sensor->publish_state(value != 0.0f); updated_count++; } } - } #endif + } else { +#ifdef USE_TEXT_SENSOR + for (const auto &item : this->text_sensors_) { + if (item.obis == capture.obis) { + std::array value_buf; + const auto value = capture.value_as_string(value_buf); + item.sensor->publish_state(value.data(), value.size()); + updated_count++; + } + } +#endif + } if (updated_count == 0) { - ESP_LOGV(TAG, "Received OBIS %s, but no sensors are registered for it.", obis_code); + std::array obis_buf; + const auto obis = capture.obis.to_string(obis_buf); + ESP_LOGV(TAG, "Received OBIS %.*s, but no sensors are registered for it.", static_cast(obis.size()), + obis.data()); } } #ifdef USE_SENSOR -void DlmsMeterComponent::register_sensor(const std::string &obis_code, sensor::Sensor *sensor) { - this->sensors_.push_back({obis_code, sensor}); +void DlmsMeterComponent::register_sensor(const dlms_parser::ObisId &obis, sensor::Sensor *sensor) { + this->sensors_.push_back({obis, sensor}); } #endif + #ifdef USE_TEXT_SENSOR -void DlmsMeterComponent::register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor) { - this->text_sensors_.push_back({obis_code, sensor}); +void DlmsMeterComponent::register_text_sensor(const dlms_parser::ObisId &obis, text_sensor::TextSensor *sensor) { + this->text_sensors_.push_back({obis, sensor}); } #endif + #ifdef USE_BINARY_SENSOR -void DlmsMeterComponent::register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor) { - this->binary_sensors_.push_back({obis_code, sensor}); +void DlmsMeterComponent::register_binary_sensor(const dlms_parser::ObisId &obis, binary_sensor::BinarySensor *sensor) { + this->binary_sensors_.push_back({obis, sensor}); } #endif } // namespace esphome::dlms_meter + +#endif diff --git a/esphome/components/dlms_meter/dlms_meter.h b/esphome/components/dlms_meter/dlms_meter.h index fc4721843f..2d7898f4d3 100644 --- a/esphome/components/dlms_meter/dlms_meter.h +++ b/esphome/components/dlms_meter/dlms_meter.h @@ -1,9 +1,13 @@ #pragma once +// Ignore Zephyr. It doesn't have any encryption library. +#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST) + +#include +#include + #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esphome/components/uart/uart.h" @@ -19,12 +23,6 @@ #include -#include -#include -#include -#include -#include - #if __has_include() #include #elif !defined(USE_ESP8266) && __has_include() @@ -35,9 +33,12 @@ #elif __has_include() #include #else -#define DLMS_METER_NO_CRYPTO +#error "The platform doesn't provide a compatible encryption library for dlms_parser" #endif +namespace esphome::dlms_meter { + +// Entity storage is sized by codegen; a platform with no entities emits no define #ifndef DLMS_MAX_SENSORS static constexpr uint8_t DLMS_MAX_SENSORS = 0; #endif @@ -48,82 +49,64 @@ static constexpr uint8_t DLMS_MAX_TEXT_SENSORS = 0; static constexpr uint8_t DLMS_MAX_BINARY_SENSORS = 0; #endif -namespace esphome::dlms_meter { - -#ifdef DLMS_METER_NO_CRYPTO -// Fallback dummy decryptor for platforms without supported crypto (e.g., Zephyr during clang-tidy) -class Aes128GcmDecryptorDummy : public dlms_parser::Aes128GcmDecryptor { - public: - void set_decryption_key(const dlms_parser::Aes128GcmDecryptionKey &key) override {} - bool decrypt_in_place(std::span iv, std::span ciphertext_and_plaintext, - std::span aad, std::span tag) override { - return false; - } -}; -#endif - -#if __has_include() -using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorTfPsa; -#elif !defined(USE_ESP8266) && __has_include() -using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorMbedTls; -#elif __has_include() -using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorBearSsl; -#else -using Aes128GcmDecryptorImpl = Aes128GcmDecryptorDummy; -#endif - #ifdef USE_SENSOR struct SensorItem { - std::string obis_code; + dlms_parser::ObisId obis; sensor::Sensor *sensor; }; #endif #ifdef USE_TEXT_SENSOR struct TextSensorItem { - std::string obis_code; + dlms_parser::ObisId obis; text_sensor::TextSensor *sensor; }; #endif #ifdef USE_BINARY_SENSOR struct BinarySensorItem { - std::string obis_code; + dlms_parser::ObisId obis; binary_sensor::BinarySensor *sensor; }; #endif struct CustomPattern { - std::string pattern; - std::optional name; - int priority{0}; - std::optional> default_obis; + const char *pattern; + const char *name; + int priority; + dlms_parser::ObisId default_obis; }; +#if __has_include() +using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorTfPsa; +#elif !defined(USE_ESP8266) && __has_include() +using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorMbedTls; +#else +using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorBearSsl; +#endif + class DlmsMeterComponent final : public Component, public uart::UARTDevice { public: - DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check, - std::optional> decryption_key, - std::optional> authentication_key, - std::vector custom_patterns); + DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check, const char *decryption_key, + const char *authentication_key, std::vector custom_patterns); void setup() override; void dump_config() override; void loop() override; #ifdef USE_SENSOR - void register_sensor(const std::string &obis_code, sensor::Sensor *sensor); + void register_sensor(const dlms_parser::ObisId &obis, sensor::Sensor *sensor); #endif #ifdef USE_TEXT_SENSOR - void register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor); + void register_text_sensor(const dlms_parser::ObisId &obis, text_sensor::TextSensor *sensor); #endif #ifdef USE_BINARY_SENSOR - void register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor); + void register_binary_sensor(const dlms_parser::ObisId &obis, binary_sensor::BinarySensor *sensor); #endif protected: void read_rx_buffer_(); void flush_rx_buffer_(); void process_frame_(); - void on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric); + void on_data_(const dlms_parser::AxdrCapture &capture); std::array rx_buffer_; size_t bytes_accumulated_{0}; @@ -149,3 +132,5 @@ class DlmsMeterComponent final : public Component, public uart::UARTDevice { }; } // namespace esphome::dlms_meter + +#endif diff --git a/esphome/components/dlms_meter/sensor/__init__.py b/esphome/components/dlms_meter/sensor/__init__.py index 8ded150cd0..15b9deb012 100644 --- a/esphome/components/dlms_meter/sensor/__init__.py +++ b/esphome/components/dlms_meter/sensor/__init__.py @@ -1,159 +1,65 @@ -import logging - import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -from esphome.const import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, - UNIT_AMPERE, - UNIT_VOLT, - UNIT_WATT, - UNIT_WATT_HOURS, -) from esphome.types import ConfigType -from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code - -_LOGGER = logging.getLogger(__name__) +from .. import ( + CONF_DLMS_METER_ID, + CONF_OBIS_CODE, + DlmsMeterComponent, + obis_string_to_byte_list, + register_sensor, +) DEPENDENCIES = ["dlms_meter"] -NUMERIC_KEYS = { - "voltage_l1": "1.0.32.7.0.255", - "voltage_l2": "1.0.52.7.0.255", - "voltage_l3": "1.0.72.7.0.255", - "current_l1": "1.0.31.7.0.255", - "current_l2": "1.0.51.7.0.255", - "current_l3": "1.0.71.7.0.255", - "active_power_plus": "1.0.1.7.0.255", - "active_power_minus": "1.0.2.7.0.255", - "active_energy_plus": "1.0.1.8.0.255", - "active_energy_minus": "1.0.2.8.0.255", - "reactive_energy_plus": "1.0.3.8.0.255", - "reactive_energy_minus": "1.0.4.8.0.255", - "power_factor": "1.0.13.7.0.255", +# Removed in 2026.11.0 - kept to provide helpful error message +# Remove before 2027.5.0 +_MEASUREMENT = "state_class: measurement" +_VOLTAGE = f"unit_of_measurement: V, accuracy_decimals: 1, device_class: voltage, {_MEASUREMENT}" +_CURRENT = f"unit_of_measurement: A, accuracy_decimals: 2, device_class: current, {_MEASUREMENT}" +_POWER = ( + f"unit_of_measurement: W, accuracy_decimals: 0, device_class: power, {_MEASUREMENT}" +) +_ENERGY = ( + "unit_of_measurement: Wh, accuracy_decimals: 0, device_class: energy, " + "state_class: total_increasing" +) +_POWER_FACTOR = f"accuracy_decimals: 3, device_class: power_factor, {_MEASUREMENT}" +REMOVED_KEYS = { + "voltage_l1": ("1.0.32.7.0.255", _VOLTAGE), + "voltage_l2": ("1.0.52.7.0.255", _VOLTAGE), + "voltage_l3": ("1.0.72.7.0.255", _VOLTAGE), + "current_l1": ("1.0.31.7.0.255", _CURRENT), + "current_l2": ("1.0.51.7.0.255", _CURRENT), + "current_l3": ("1.0.71.7.0.255", _CURRENT), + "active_power_plus": ("1.0.1.7.0.255", _POWER), + "active_power_minus": ("1.0.2.7.0.255", _POWER), + "active_energy_plus": ("1.0.1.8.0.255", _ENERGY), + "active_energy_minus": ("1.0.2.8.0.255", _ENERGY), + "reactive_energy_plus": ("1.0.3.8.0.255", _ENERGY), + "reactive_energy_minus": ("1.0.4.8.0.255", _ENERGY), + "power_factor": ("1.0.13.7.0.255", _POWER_FACTOR), } -DYNAMIC_SCHEMA = sensor.sensor_schema().extend( + +CONFIG_SCHEMA = sensor.sensor_schema().extend( { cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), - cv.Required(CONF_OBIS_CODE): obis_code, + cv.Required(CONF_OBIS_CODE): obis_string_to_byte_list, + **{ + cv.Optional(key): cv.invalid( + f"The predefined '{key}' key was removed in ESPHome 2026.11.0. " + f"Add a separate '- platform: dlms_meter' sensor with " + f'obis_code: "{obis}" and {settings}' + ) + for key, (obis, settings) in REMOVED_KEYS.items() + }, } ) -def deprecation_warning(config: ConfigType) -> ConfigType: - _LOGGER.warning( - "The dlms_meter sensor schema using predefined keys (e.g., 'voltage_l1') is deprecated and will be removed in 2026.11.0. " - "Please update your configuration to use the new schema with 'obis_code'." - ) - return config - - -OLD_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), - cv.Optional("voltage_l1"): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("voltage_l2"): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("voltage_l3"): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("current_l1"): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("current_l2"): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("current_l3"): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("active_power_plus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=0, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("active_power_minus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=0, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("active_energy_plus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional("active_energy_minus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional("reactive_energy_plus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional("reactive_energy_minus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional("power_factor"): sensor.sensor_schema( - accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, - ), - } - ).extend(cv.COMPONENT_SCHEMA), - deprecation_warning, -) - - -CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA) - - async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) - - if obis := config.get(CONF_OBIS_CODE): - var = await sensor.new_sensor(config) - cg.add(hub.register_sensor(obis, var)) - else: - for key, obis_val in NUMERIC_KEYS.items(): - if sensor_config := config.get(key): - sens = await sensor.new_sensor(sensor_config) - cg.add(hub.register_sensor(obis_val, sens)) + var = await sensor.new_sensor(config) + register_sensor(hub, config[CONF_OBIS_CODE], var) diff --git a/esphome/components/dlms_meter/text_sensor/__init__.py b/esphome/components/dlms_meter/text_sensor/__init__.py index c2ff0779ee..4e2e48487a 100644 --- a/esphome/components/dlms_meter/text_sensor/__init__.py +++ b/esphome/components/dlms_meter/text_sensor/__init__.py @@ -1,60 +1,43 @@ -import logging - import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.types import ConfigType -from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code - -_LOGGER = logging.getLogger(__name__) +from .. import ( + CONF_DLMS_METER_ID, + CONF_OBIS_CODE, + DlmsMeterComponent, + obis_string_to_byte_list, + register_text_sensor, +) DEPENDENCIES = ["dlms_meter"] -TEXT_KEYS = { +# Removed in 2026.11.0 - kept to provide helpful error message +# Remove before 2027.5.0 +REMOVED_KEYS = { "timestamp": "0.0.1.0.0.255", "meternumber": "0.0.96.1.0.255", } -DYNAMIC_SCHEMA = text_sensor.text_sensor_schema().extend( + +CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend( { cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), - cv.Required(CONF_OBIS_CODE): obis_code, + cv.Required(CONF_OBIS_CODE): obis_string_to_byte_list, + **{ + cv.Optional(key): cv.invalid( + f"The predefined '{key}' key was removed in ESPHome 2026.11.0. " + f"Add a separate '- platform: dlms_meter' text sensor with " + f'obis_code: "{obis}" instead' + ) + for key, obis in REMOVED_KEYS.items() + }, } ) -def deprecation_warning(config: ConfigType) -> ConfigType: - _LOGGER.warning( - "The dlms_meter text_sensor schema using predefined keys (e.g., 'timestamp') is deprecated and will be removed in 2026.11.0. " - "Please update your configuration to use the new schema with 'obis_code'." - ) - return config - - -OLD_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), - cv.Optional("timestamp"): text_sensor.text_sensor_schema(), - cv.Optional("meternumber"): text_sensor.text_sensor_schema(), - } - ).extend(cv.COMPONENT_SCHEMA), - deprecation_warning, -) - - -CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA) - - async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) - - if obis := config.get(CONF_OBIS_CODE): - var = await text_sensor.new_text_sensor(config) - cg.add(hub.register_text_sensor(obis, var)) - else: - for key, obis_val in TEXT_KEYS.items(): - if text_sensor_config := config.get(key): - sens = await text_sensor.new_text_sensor(text_sensor_config) - cg.add(hub.register_text_sensor(obis_val, sens)) + var = await text_sensor.new_text_sensor(config) + register_text_sensor(hub, config[CONF_OBIS_CODE], var) diff --git a/esphome/components/dps310/__init__.py b/esphome/components/dps310/__init__.py index e69de29bb2..52f2a4105e 100644 --- a/esphome/components/dps310/__init__.py +++ b/esphome/components/dps310/__init__.py @@ -0,0 +1 @@ +DOMAIN = "dps310" diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index abb34d6598..080f101747 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -6,6 +6,8 @@ namespace esphome::dps310 { ESPHOME_LOG_TAG(TAG, "dps310"); +static constexpr uint32_t READ_RETRY_TIMEOUT_ID = 0; + void DPS310Component::setup() { uint8_t coef_data_raw[DPS310_NUM_COEF_REGS]; auto timer = DPS310_INIT_TIMEOUT; @@ -126,7 +128,7 @@ void DPS310Component::read_() { this->update_in_progress_ = false; this->status_clear_warning(); } else { - this->set_timeout("dps310", 10, [this]() { this->read_(); }); + this->set_timeout(READ_RETRY_TIMEOUT_ID, 10, [this]() { this->read_(); }); } } diff --git a/esphome/components/ds1307/__init__.py b/esphome/components/ds1307/__init__.py index e69de29bb2..fa275ded95 100644 --- a/esphome/components/ds1307/__init__.py +++ b/esphome/components/ds1307/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ds1307" diff --git a/esphome/components/ds1603l/__init__.py b/esphome/components/ds1603l/__init__.py index e69de29bb2..81c65f4fd3 100644 --- a/esphome/components/ds1603l/__init__.py +++ b/esphome/components/ds1603l/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ds1603l" diff --git a/esphome/components/ds2484/__init__.py b/esphome/components/ds2484/__init__.py index 3d9f24ff19..f101dedc01 100644 --- a/esphome/components/ds2484/__init__.py +++ b/esphome/components/ds2484/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@mrk-its"] +DOMAIN = "ds2484" diff --git a/esphome/components/ds248x/__init__.py b/esphome/components/ds248x/__init__.py index a2e2a87ed0..3a2cb34747 100644 --- a/esphome/components/ds248x/__init__.py +++ b/esphome/components/ds248x/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_SLEEP_PIN, CONF_TYPE from esphome.types import ConfigType CODEOWNERS = ["@tomwellnitz"] +DOMAIN = "ds248x" MULTI_CONF = True DEPENDENCIES = ["i2c"] diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 96a1e75668..07dbf55831 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -3,6 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg from esphome.components import uart +from esphome.components.uart import DOMAIN as UART_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_ID, @@ -16,6 +17,7 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@glmnet", "@PolarGoose"] +DOMAIN = "dsmr" MULTI_CONF = True @@ -91,7 +93,7 @@ async def to_code(config: ConfigType) -> None: def final_validate(config: ConfigType) -> None: full_config = fv.full_config.get() - for uart_conf in full_config["uart"]: + for uart_conf in full_config[UART_DOMAIN]: if uart_conf[CONF_ID] == config[CONF_UART_ID]: rx_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE] if rx_buffer_size < 1500: diff --git a/esphome/components/duty_cycle/__init__.py b/esphome/components/duty_cycle/__init__.py index e69de29bb2..8d11f04488 100644 --- a/esphome/components/duty_cycle/__init__.py +++ b/esphome/components/duty_cycle/__init__.py @@ -0,0 +1 @@ +DOMAIN = "duty_cycle" diff --git a/esphome/components/duty_time/__init__.py b/esphome/components/duty_time/__init__.py index b708cee80b..258253e4eb 100644 --- a/esphome/components/duty_time/__init__.py +++ b/esphome/components/duty_time/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@dudanov"] +DOMAIN = "duty_time" diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py index 3b1eb99e60..dbe9b1e45c 100644 --- a/esphome/components/e131/__init__.py +++ b/esphome/components/e131/__init__.py @@ -9,6 +9,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["socket"] DEPENDENCIES = ["network"] +DOMAIN = "e131" e131_ns = cg.esphome_ns.namespace("e131") E131AddressableLightEffect = e131_ns.class_( diff --git a/esphome/components/ee895/__init__.py b/esphome/components/ee895/__init__.py index e69de29bb2..82043233c3 100644 --- a/esphome/components/ee895/__init__.py +++ b/esphome/components/ee895/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ee895" diff --git a/esphome/components/ektf2232/__init__.py b/esphome/components/ektf2232/__init__.py index e69de29bb2..03f83e2a58 100644 --- a/esphome/components/ektf2232/__init__.py +++ b/esphome/components/ektf2232/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ektf2232" diff --git a/esphome/components/emc2101/__init__.py b/esphome/components/emc2101/__init__.py index 6cd122fb96..024ff3c893 100644 --- a/esphome/components/emc2101/__init__.py +++ b/esphome/components/emc2101/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_INVERTED, CONF_RESOLUTION from esphome.types import ConfigType CODEOWNERS = ["@ellull"] +DOMAIN = "emc2101" DEPENDENCIES = ["i2c"] diff --git a/esphome/components/emmeti/__init__.py b/esphome/components/emmeti/__init__.py index e69de29bb2..ec6d2835b0 100644 --- a/esphome/components/emmeti/__init__.py +++ b/esphome/components/emmeti/__init__.py @@ -0,0 +1 @@ +DOMAIN = "emmeti" diff --git a/esphome/components/emontx/__init__.py b/esphome/components/emontx/__init__.py index 7eae375cd6..0262f8681f 100644 --- a/esphome/components/emontx/__init__.py +++ b/esphome/components/emontx/__init__.py @@ -3,6 +3,8 @@ from dataclasses import dataclass, field from esphome import automation import esphome.codegen as cg from esphome.components import uart +from esphome.components.sensor import DOMAIN as SENSOR_DOMAIN +from esphome.components.uart import DOMAIN as UART_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_COMMAND, @@ -63,13 +65,13 @@ def final_validate(config: ConfigType) -> None: hub_id = str(config[CONF_ID]) sensor_count = sum( 1 - for s in full_config.get("sensor", []) - if s.get("platform") == "emontx" and str(s.get(CONF_EMONTX_ID)) == hub_id + for s in full_config.get(SENSOR_DOMAIN, []) + if s.get("platform") == DOMAIN and str(s.get(CONF_EMONTX_ID)) == hub_id ) _get_data().sensor_counts[hub_id] = sensor_count # Ensure UART RX buffer size is large enough to handle data bursts from firmware - for uart_conf in full_config["uart"]: + for uart_conf in full_config[UART_DOMAIN]: if uart_conf[CONF_ID] == config[CONF_UART_ID]: current_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE] if current_buffer_size < MINIMUM_RX_BUFFER_SIZE: diff --git a/esphome/components/endstop/__init__.py b/esphome/components/endstop/__init__.py index e69de29bb2..c064448409 100644 --- a/esphome/components/endstop/__init__.py +++ b/esphome/components/endstop/__init__.py @@ -0,0 +1 @@ +DOMAIN = "endstop" diff --git a/esphome/components/ens160/__init__.py b/esphome/components/ens160/__init__.py index e69de29bb2..e59458427d 100644 --- a/esphome/components/ens160/__init__.py +++ b/esphome/components/ens160/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ens160" diff --git a/esphome/components/ens160_base/__init__.py b/esphome/components/ens160_base/__init__.py index 6da0d10af9..b41575c717 100644 --- a/esphome/components/ens160_base/__init__.py +++ b/esphome/components/ens160_base/__init__.py @@ -22,6 +22,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@vincentscode", "@latonita"] +DOMAIN = "ens160_base" ens160_ns = cg.esphome_ns.namespace("ens160_base") diff --git a/esphome/components/ens160_i2c/__init__.py b/esphome/components/ens160_i2c/__init__.py index e69de29bb2..605acf0f53 100644 --- a/esphome/components/ens160_i2c/__init__.py +++ b/esphome/components/ens160_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ens160_i2c" diff --git a/esphome/components/ens160_spi/__init__.py b/esphome/components/ens160_spi/__init__.py index e69de29bb2..88ad91e632 100644 --- a/esphome/components/ens160_spi/__init__.py +++ b/esphome/components/ens160_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ens160_spi" diff --git a/esphome/components/ens210/__init__.py b/esphome/components/ens210/__init__.py index e69de29bb2..58f5d4f25f 100644 --- a/esphome/components/ens210/__init__.py +++ b/esphome/components/ens210/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ens210" diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index fa1d459407..5815fc9038 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -14,6 +14,8 @@ namespace esphome::ens210 { ESPHOME_LOG_TAG(TAG, "ens210"); +static constexpr uint32_t DATA_TIMEOUT_ID = 0; + // ENS210 chip constants static const uint8_t ENS210_BOOTING_MS = 2; // Booting time in ms (also after reset, or going to high power) static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS = @@ -149,7 +151,7 @@ void ENS210Component::update() { return; } // Wait for measurement to complete - this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() { + this->set_timeout(DATA_TIMEOUT_ID, uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() { int temperature_data, temperature_status, humidity_data, humidity_status; uint8_t data[6]; uint32_t h_val_data, t_val_data; diff --git a/esphome/components/epaper_spi/__init__.py b/esphome/components/epaper_spi/__init__.py index f70ffa9520..da3685c263 100644 --- a/esphome/components/epaper_spi/__init__.py +++ b/esphome/components/epaper_spi/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@esphome/core"] +DOMAIN = "epaper_spi" diff --git a/esphome/components/es7210/__init__.py b/esphome/components/es7210/__init__.py index e69de29bb2..0038d29040 100644 --- a/esphome/components/es7210/__init__.py +++ b/esphome/components/es7210/__init__.py @@ -0,0 +1 @@ +DOMAIN = "es7210" diff --git a/esphome/components/es7243e/__init__.py b/esphome/components/es7243e/__init__.py index e69de29bb2..6968b84c93 100644 --- a/esphome/components/es7243e/__init__.py +++ b/esphome/components/es7243e/__init__.py @@ -0,0 +1 @@ +DOMAIN = "es7243e" diff --git a/esphome/components/es8156/__init__.py b/esphome/components/es8156/__init__.py index e69de29bb2..f1e7f4d4e7 100644 --- a/esphome/components/es8156/__init__.py +++ b/esphome/components/es8156/__init__.py @@ -0,0 +1 @@ +DOMAIN = "es8156" diff --git a/esphome/components/es8156/audio_dac.py b/esphome/components/es8156/audio_dac.py index 305aa92125..ebba75e70d 100644 --- a/esphome/components/es8156/audio_dac.py +++ b/esphome/components/es8156/audio_dac.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import i2c from esphome.components.audio_dac import AudioDac +from esphome.components.speaker import DOMAIN as SPEAKER_DOMAIN import esphome.config_validation as cv from esphome.const import CONF_AUDIO_DAC, CONF_BITS_PER_SAMPLE, CONF_ID import esphome.final_validate as fv @@ -27,7 +28,7 @@ def _final_validate(config: ConfigType) -> None: full_config = fv.full_config.get() # Check all speaker configurations for ones that reference this es8156 - speaker_configs = full_config.get("speaker", []) + speaker_configs = full_config.get(SPEAKER_DOMAIN, []) for speaker_config in speaker_configs: audio_dac_id = speaker_config.get(CONF_AUDIO_DAC) if ( diff --git a/esphome/components/es8311/__init__.py b/esphome/components/es8311/__init__.py index e69de29bb2..975291c4a7 100644 --- a/esphome/components/es8311/__init__.py +++ b/esphome/components/es8311/__init__.py @@ -0,0 +1 @@ +DOMAIN = "es8311" diff --git a/esphome/components/es8388/__init__.py b/esphome/components/es8388/__init__.py index e69de29bb2..e8e2e361ba 100644 --- a/esphome/components/es8388/__init__.py +++ b/esphome/components/es8388/__init__.py @@ -0,0 +1 @@ +DOMAIN = "es8388" diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 040fd7fe26..b57949ccd0 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -231,7 +231,7 @@ bool ES8388::set_dac_output(DacOutputLine line) { optional ES8388::get_dac_power() { uint8_t dac_power; if (!this->read_byte(ES8388_DACPOWER, &dac_power)) { - this->status_momentary_warning("dacpower_read"); + this->status_momentary_warning(); return {}; } switch (dac_power) { @@ -274,7 +274,7 @@ bool ES8388::set_adc_input_mic(AdcInputMicLine line) { optional ES8388::get_mic_input() { uint8_t mic_input; if (!this->read_byte(ES8388_ADCCONTROL2, &mic_input)) { - this->status_momentary_warning("adccontrol2_read"); + this->status_momentary_warning(); return {}; } switch (mic_input) { diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index c825deaefd..aa4f78de69 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -15,7 +15,6 @@ from esphome.components.const import ( CONF_ENABLE_OTA_DOWNGRADE_PROTECTION, CONF_IGNORE_NOT_FOUND, ) -from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_ADVANCED, @@ -106,6 +105,7 @@ from .gpio import esp32_pin_to_code # noqa: F401 _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["preferences"] CODEOWNERS = ["@esphome/core"] +DOMAIN = "esp32" IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" @@ -2925,6 +2925,11 @@ async def to_code(config): # produce reproducible outputs and downstream tooling can reuse artifacts. add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True) + # Static destructors never run, so skip registering them. See atexit_stubs.cpp. + # --undefined: libsrc.a is scanned before the IDF libraries that also register them. + cg.add_build_flag("-Wl,--wrap=__cxa_atexit") + cg.add_build_flag("-Wl,--undefined=__wrap___cxa_atexit") + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") @@ -3961,10 +3966,3 @@ def process_stacktrace(config, line, backtrace_state): _decode_pc(config, addr.group()) return backtrace_state - - -# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which -# are instantiated solely by the pin schema codegen (esp32_pin_to_code) -FILTER_SOURCE_FILES = filter_source_files_from_defines( - {"gpio.cpp": "USE_ESP32_INTERNAL_GPIO"} -) diff --git a/esphome/components/esp32/atexit_stubs.cpp b/esphome/components/esp32/atexit_stubs.cpp new file mode 100644 index 0000000000..1230a56681 --- /dev/null +++ b/esphome/components/esp32/atexit_stubs.cpp @@ -0,0 +1,20 @@ +/* + * Linker wrap stub for __cxa_atexit(). + * + * GCC registers the destructor of every global and function local static + * object with __cxa_atexit(). The firmware never returns from main or calls + * exit(), so these destructors never run, but the first registration links + * newlib's exit handler tables (about 400 bytes of RAM on ESP32). + */ + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +namespace esphome::esp32 {} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; } +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP32 diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index 78326249ad..182bc5f2d6 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -531,6 +531,9 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { // Xtensa: walk the backtrace using the public API if (info->frame != nullptr) { auto *xt_frame = (XtExcFrame *) info->frame; + // IDF leaves addr unset for SoC-level panics (cache error, interrupt WDT) + if (info->pseudo_excause) + s_raw_crash_data.pc = xt_frame->pc; if (!g_panic_abort) { // Abort-class frames carry no useful cause/vaddr: TWDT task snapshots // never wrote them and abort() traps describe only the synthetic trap. diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 6e4de6aa51..59788e76dc 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -1,9 +1,10 @@ #include "esphome/core/defines.h" -// Also defines the core ISRInternalGPIOPin methods; those are only reachable -// via ESP32InternalGPIOPin::to_isr(), so the same define gates both safely. -#if defined(USE_ESP32) && defined(USE_ESP32_INTERNAL_GPIO) +// Always built: InternalGPIOPin is this class, so components that merely hold a +// pointer to it reference these methods even in configs without a pin. +#ifdef USE_ESP32 #include "gpio.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "driver/gpio.h" #include "driver/rtc_io.h" @@ -49,17 +50,52 @@ struct ISRPinArg { gpio_num_t pin; gpio::Flags flags; bool inverted; +#ifdef USE_GPIO_HOLD + bool hold; +#endif #if defined(USE_ESP32_VARIANT_ESP32) bool use_rtc; int rtc_pin; #endif }; +#ifdef USE_GPIO_HOLD +// Re-latch a held pad onto the values just written to its registers. +static inline void refresh_hold(const ESP32InternalGPIOPin &pin) { + pin.set_hold(false); + pin.set_hold(true); +} + +static inline void IRAM_ATTR isr_refresh_hold(const ISRPinArg *arg) { + if (!arg->hold) + return; +#if defined(USE_ESP32_VARIANT_ESP32) + if (arg->use_rtc) { + rtcio_hal_hold_disable(arg->rtc_pin); + rtcio_hal_hold_enable(arg->rtc_pin); + return; + } +#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + // GPIO0-21 are held from the RTC domain; the digital hold mask is zero for them + if (arg->pin < SOC_RTCIO_PIN_COUNT) { + rtcio_hal_hold_disable(arg->pin); + rtcio_hal_hold_enable(arg->pin); + return; + } +#endif + gpio_hal_hold_dis(&GPIO_HAL, arg->pin); + gpio_hal_hold_en(&GPIO_HAL, arg->pin); +} +#endif + ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) arg->pin = this->get_pin_num(); arg->flags = gpio::FLAG_NONE; arg->inverted = this->pin_flags_.inverted; +#ifdef USE_GPIO_HOLD + arg->hold = this->get_hold(); +#endif #if defined(USE_ESP32_VARIANT_ESP32) arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num()); if (arg->use_rtc) @@ -68,7 +104,7 @@ ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { return ISRInternalGPIOPin((void *) arg); } -void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { +void ESP32InternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const { gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE; switch (type) { case gpio::INTERRUPT_RISING_EDGE: @@ -104,7 +140,23 @@ size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const { return snprintf(buffer, len, "GPIO%" PRIu32, static_cast(this->pin_)); } +#ifdef USE_GPIO_HOLD +void ESP32InternalGPIOPin::set_hold(bool hold) const { + if (!this->get_hold()) + return; + if (hold) { + gpio_hold_en(this->get_pin_num()); + } else { + gpio_hold_dis(this->get_pin_num()); + } +} +#endif + void ESP32InternalGPIOPin::setup() { +#ifdef USE_GPIO_HOLD + // Hold before gpio_config so the pad keeps its state while the registers change + this->set_hold(true); +#endif gpio_config_t conf{}; conf.pin_bit_mask = 1ULL << static_cast(this->pin_); conf.mode = flags_to_mode(this->flags_); @@ -115,6 +167,18 @@ void ESP32InternalGPIOPin::setup() { if (this->flags_ & gpio::FLAG_OUTPUT) { gpio_set_drive_capability(this->get_pin_num(), this->get_drive_strength()); } +#ifdef USE_GPIO_HOLD + // gpio_hold_dis logs an error on input-only pads + if (GPIO_IS_VALID_OUTPUT_GPIO(this->get_pin_num())) { + if (!this->get_hold()) { + // Release a hold left behind by an earlier boot + gpio_hold_dis(this->get_pin_num()); + } else if (this->flags_ & gpio::FLAG_INPUT) { + // Inputs apply the config now so reads work; outputs wait for the first write + refresh_hold(*this); + } + } +#endif } void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { @@ -129,6 +193,9 @@ void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { pull_mode = GPIO_PULLDOWN_ONLY; } gpio_set_pull_mode(this->get_pin_num(), pull_mode); +#ifdef USE_GPIO_HOLD + refresh_hold(*this); +#endif } bool ESP32InternalGPIOPin::digital_read() { @@ -136,6 +203,9 @@ bool ESP32InternalGPIOPin::digital_read() { } void ESP32InternalGPIOPin::digital_write(bool value) { gpio_set_level(this->get_pin_num(), value != this->pin_flags_.inverted ? 1 : 0); +#ifdef USE_GPIO_HOLD + refresh_hold(*this); +#endif } void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->get_pin_num()); } @@ -143,6 +213,8 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->ge using namespace esp32; +// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- some gpio_hal functions use MMIO at fixed addresses +// internally bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { auto *arg = reinterpret_cast(this->arg_); return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted; @@ -151,6 +223,9 @@ bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { auto *arg = reinterpret_cast(this->arg_); gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted); +#ifdef USE_GPIO_HOLD + isr_refresh_hold(arg); +#endif } void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { @@ -202,9 +277,13 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { gpio_hal_input_disable(&GPIO_HAL, arg->pin); } } +#ifdef USE_GPIO_HOLD + isr_refresh_hold(arg); +#endif arg->flags = flags; } +// NOLINTEND(clang-analyzer-core.FixedAddressDereference) } // namespace esphome -#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO +#endif // USE_ESP32 diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index aeff5af51c..6ac1673127 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -1,7 +1,8 @@ #pragma once #ifdef USE_ESP32 -#include "esphome/core/hal.h" +#include "esphome/core/gpio_pin.h" +#include #include namespace esphome::esp32 { @@ -10,7 +11,7 @@ namespace esphome::esp32 { static_assert(GPIO_NUM_MAX <= 256, "gpio_num_t has too many values for uint8_t"); static_assert(GPIO_DRIVE_CAP_MAX <= 4, "gpio_drive_cap_t has too many values for 2-bit field"); -class ESP32InternalGPIOPin final : public InternalGPIOPin { +class ESP32InternalGPIOPin final : public GPIOPin { public: void set_pin(gpio_num_t pin) { this->pin_ = static_cast(pin); } void set_inverted(bool inverted) { this->pin_flags_.inverted = inverted; } @@ -24,16 +25,30 @@ class ESP32InternalGPIOPin final : public InternalGPIOPin { bool digital_read() override; void digital_write(bool value) override; size_t dump_summary(char *buffer, size_t len) const override; - void detach_interrupt() const override; - ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return this->pin_; } + void detach_interrupt() const; + template void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const { + this->attach_interrupt_(reinterpret_cast(func), arg, type); + } + ISRInternalGPIOPin to_isr() const; + uint8_t get_pin() const { return this->pin_; } gpio::Flags get_flags() const override { return this->flags_; } - bool is_inverted() const override { return this->pin_flags_.inverted; } + bool is_inverted() const { return this->pin_flags_.inverted; } + bool is_internal() override { return true; } gpio_num_t get_pin_num() const { return static_cast(this->pin_); } gpio_drive_cap_t get_drive_strength() const { return static_cast(this->pin_flags_.drive_strength); } +#ifdef USE_GPIO_HOLD + /// Apply or release the pad hold. Does nothing unless the pin is configured with hold_state. + void set_hold(bool hold) const; + bool is_held() const override { + if (!this->get_hold()) + return false; + esp_reset_reason_t reason = esp_reset_reason(); + return reason == ESP_RST_DEEPSLEEP || reason == ESP_RST_SW; + } +#endif protected: - void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const; // Memory layout: 8 bytes total on 32-bit systems // - 3 bytes for members below diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 98aac209ec..7753574bc8 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -5,6 +5,7 @@ from typing import Any from esphome import pins import esphome.codegen as cg +from esphome.components.const import CONF_HOLD_STATE import esphome.config_validation as cv from esphome.const import ( CONF_ID, @@ -248,6 +249,7 @@ ESP32_PIN_SCHEMA = cv.All( cv.float_with_unit("current", "mA", optional_unit=True), cv.enum(DRIVE_STRENGTHS), ), + cv.Optional(CONF_HOLD_STATE, default=False): cv.boolean, } ), validate_gpio_pin, @@ -257,7 +259,6 @@ ESP32_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA) async def esp32_pin_to_code(config): - cg.add_define("USE_ESP32_INTERNAL_GPIO") var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) @@ -267,7 +268,11 @@ async def esp32_pin_to_code(config): cg.add(var.set_inverted(True)) if CONF_DRIVE_STRENGTH in config: cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) - cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + flags = pins.gpio_flags_expr(config[CONF_MODE]) + if config[CONF_HOLD_STATE]: + flags = flags | cg.gpio_Flags.FLAG_HOLD + cg.add_define("USE_GPIO_HOLD") + cg.add(var.set_flags(flags)) return var diff --git a/esphome/components/esp32/gpio_common.py b/esphome/components/esp32/gpio_common.py new file mode 100644 index 0000000000..a12bebf6e9 --- /dev/null +++ b/esphome/components/esp32/gpio_common.py @@ -0,0 +1,26 @@ +"""Pin checks shared by the ESP32 variant validators.""" + +from logging import Logger +from typing import Any + +from esphome.components.const import CONF_HOLD_STATE +from esphome.const import CONF_NUMBER + + +def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None: + if num in usb_jtag_pins: + logger.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + num, + ) + + +def check_usb_jtag_hold( + conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger +) -> None: + num = conf[CONF_NUMBER] + if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins: + logger.warning( + "GPIO%d cannot hold at low level during wakeup from deep sleep.", num + ) diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index fec257f90b..11c62359c1 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -1,6 +1,7 @@ import logging from typing import Any +from esphome.components.const import CONF_HOLD_STATE import esphome.config_validation as cv from esphome.const import ( CONF_INPUT, @@ -53,6 +54,7 @@ def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]: is_output = mode[CONF_OUTPUT] is_pullup = mode[CONF_PULLUP] is_pulldown = mode[CONF_PULLDOWN] + is_hold = value.get(CONF_HOLD_STATE) if is_input: # All ESP32 pins support input mode @@ -70,6 +72,14 @@ def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]: raise cv.Invalid( f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] ) + if is_hold and 34 <= num <= 39: + raise cv.Invalid( + f"GPIO{num} (34-39) is input-only and cannot be held.", + [CONF_HOLD_STATE], + ) + if is_hold and num == 20: + # Not in the digital hold mask and not an RTC pad, so the driver cannot hold it + raise cv.Invalid("GPIO20 has no hold function.", [CONF_HOLD_STATE]) check_strapping_pin(value, _ESP32_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index 6eb002f3f0..c4624a93a4 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + _ESP32C3_SPI_PSRAM_PINS = { 12: "SPIHD", 13: "SPIWP", @@ -28,12 +30,7 @@ def esp32_c3_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" ) - if value in _ESP32C3_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32C3_USB_JTAG_PINS, _LOGGER) return value @@ -49,6 +46,7 @@ def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32C3_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32C3_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c5.py b/esphome/components/esp32/gpio_esp32_c5.py index 639ed64c9e..b3f4c319c6 100644 --- a/esphome/components/esp32/gpio_esp32_c5.py +++ b/esphome/components/esp32/gpio_esp32_c5.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h _ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3} @@ -20,6 +22,8 @@ _ESP32C5_SPI_PSRAM_PINS = { _ESP32C5_STRAPPING_PINS = {2, 7, 27, 28} +_ESP32C5_USB_JTAG_PINS = {13, 14} + _LOGGER = logging.getLogger(__name__) @@ -31,6 +35,7 @@ def esp32_c5_validate_gpio_pin(value: int) -> int: f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})" ) + check_usb_jtag_pin(value, _ESP32C5_USB_JTAG_PINS, _LOGGER) return value @@ -45,6 +50,8 @@ def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32C5_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index bd7bb9e220..9eba375e26 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h _ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7} @@ -32,12 +34,7 @@ def esp32_c6_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})" ) - if value in _ESP32C6_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32C6_USB_JTAG_PINS, _LOGGER) return value @@ -52,6 +49,7 @@ def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32C6_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c61.py b/esphome/components/esp32/gpio_esp32_c61.py index 2f3abe6a0f..3ae8f1c292 100644 --- a/esphome/components/esp32/gpio_esp32_c61.py +++ b/esphome/components/esp32/gpio_esp32_c61.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM _ESP32C61_SPI_PSRAM_PINS = { 14: "SPICS0", @@ -18,6 +20,8 @@ _ESP32C61_SPI_PSRAM_PINS = { _ESP32C61_STRAPPING_PINS = {8, 9} +_ESP32C61_USB_JTAG_PINS = {12, 13} + _LOGGER = logging.getLogger(__name__) @@ -29,6 +33,7 @@ def esp32_c61_validate_gpio_pin(value: int) -> int: f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})" ) + check_usb_jtag_pin(value, _ESP32C61_USB_JTAG_PINS, _LOGGER) return value @@ -43,5 +48,7 @@ def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32-C61 pins support input mode pass + check_usb_jtag_hold(value, _ESP32C61_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 9dd6537694..41520bb4b7 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} _ESP32H2_USB_JTAG_PINS = {26, 27} @@ -24,12 +26,7 @@ def esp32_h2_validate_gpio_pin(value: int) -> int: "See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html", value, ) - if value in _ESP32H2_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32H2_USB_JTAG_PINS, _LOGGER) return value @@ -44,5 +41,6 @@ def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32H2_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_h21.py b/esphome/components/esp32/gpio_esp32_h21.py index 5ab1b7c074..a13fe3e4cc 100644 --- a/esphome/components/esp32/gpio_esp32_h21.py +++ b/esphome/components/esp32/gpio_esp32_h21.py @@ -5,17 +5,22 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # Partial set from the ESP-IDF / esptool boot-mode docs: # https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html # The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section. _ESP32H21_STRAPPING_PINS: set[int] = {13, 14} +_ESP32H21_USB_JTAG_PINS = {17, 18} + _LOGGER = logging.getLogger(__name__) def esp32_h21_validate_gpio_pin(value: int) -> int: if value < 0 or value > 25: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)") + check_usb_jtag_pin(value, _ESP32H21_USB_JTAG_PINS, _LOGGER) return value @@ -30,5 +35,7 @@ def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32H21_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_h4.py b/esphome/components/esp32/gpio_esp32_h4.py index 86a4d55858..0cbd83c50c 100644 --- a/esphome/components/esp32/gpio_esp32_h4.py +++ b/esphome/components/esp32/gpio_esp32_h4.py @@ -5,17 +5,22 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # Partial set from the ESP-IDF / esptool boot-mode docs: # https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html # The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section. _ESP32H4_STRAPPING_PINS: set[int] = {13, 14} +_ESP32H4_USB_JTAG_PINS = {13, 14} + _LOGGER = logging.getLogger(__name__) def esp32_h4_validate_gpio_pin(value: int) -> int: if value < 0 or value > 39: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") + check_usb_jtag_pin(value, _ESP32H4_USB_JTAG_PINS, _LOGGER) return value @@ -30,5 +35,7 @@ def esp32_h4_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32H4_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32H4_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 6e9227c501..69fdf5dd22 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf _ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} @@ -18,12 +20,7 @@ _LOGGER = logging.getLogger(__name__) def esp32_p4_validate_gpio_pin(value: int) -> int: if value < 0 or value > 54: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") - if value in _ESP32P4_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32P4_USB_JTAG_PINS, _LOGGER) return value @@ -38,6 +35,7 @@ def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32P4_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py index 4978f48a1c..0de621789d 100644 --- a/esphome/components/esp32/gpio_esp32_s2.py +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -1,6 +1,7 @@ import logging from typing import Any +from esphome.components.const import CONF_HOLD_STATE import esphome.config_validation as cv from esphome.const import ( CONF_INPUT, @@ -70,6 +71,11 @@ def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: raise cv.Invalid( f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] ) + if value.get(CONF_HOLD_STATE) and num == 46: + raise cv.Invalid( + f"GPIO{num} is input-only and cannot be held.", + [CONF_HOLD_STATE], + ) check_strapping_pin(value, _ESP32S2_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index db8c520533..d38c0a8011 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -12,6 +12,8 @@ from esphome.const import ( from esphome.pins import PIN_SCHEMA_REGISTRY, check_strapping_pin from esphome.types import ConfigType +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + _ESP32S3_SPI_PSRAM_PINS = { 26: "SPICS1", 27: "SPIHD", @@ -53,12 +55,7 @@ def esp32_s3_validate_gpio_pin(value: int) -> int: # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") - if value in _ESP32S3_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32S3_USB_JTAG_PINS, _LOGGER) return value @@ -73,6 +70,7 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32S3_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_s31.py b/esphome/components/esp32/gpio_esp32_s31.py index 7ccb7cdb90..a6a3b39193 100644 --- a/esphome/components/esp32/gpio_esp32_s31.py +++ b/esphome/components/esp32/gpio_esp32_s31.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # Per the ESP32-S31 IDF DOCS and datasheet: # https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html # https://documentation.espressif.com/esp32-s31_datasheet_en.pdf @@ -16,6 +18,8 @@ _ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61} # LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table. _ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6} +_ESP32S31_USB_JTAG_PINS = {33, 34} + _LOGGER = logging.getLogger(__name__) @@ -28,6 +32,7 @@ def esp32_s31_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used." ) + check_usb_jtag_pin(value, _ESP32S31_USB_JTAG_PINS, _LOGGER) return value @@ -46,6 +51,8 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32S31_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32S31_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/hal.h b/esphome/components/esp32/hal.h index 2180f07f6c..c01dcc858e 100644 --- a/esphome/components/esp32/hal.h +++ b/esphome/components/esp32/hal.h @@ -9,6 +9,7 @@ #include #include +#include "esphome/core/main_task.h" #include "esphome/core/time_conversion.h" #ifndef PROGMEM @@ -31,6 +32,10 @@ __attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrC // NOLINTNEXTLINE(readability-redundant-declaration) extern "C" int64_t esp_timer_get_time(void); +/// Before setup() stores the handle every task counts as background; the wake is a no-op until then. +__attribute__((always_inline)) inline bool is_main_loop_thread() { + return xTaskGetCurrentTaskHandle() == esphome_main_task_handle; +} __attribute__((always_inline)) inline void yield() { vPortYield(); } __attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } __attribute__((always_inline)) inline uint32_t micros() { return static_cast(esp_timer_get_time()); } diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index ef95243bc1..48f2e3c946 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -22,6 +22,8 @@ from esphome.components.esp32 import ( request_bluetooth, ) from esphome.components.esp32.const import VARIANT_ESP32C2 +from esphome.components.esp32_hosted import DOMAIN as ESP32_HOSTED_DOMAIN +from esphome.components.psram import DOMAIN as PSRAM_DOMAIN from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( @@ -277,7 +279,7 @@ DEFAULT_MAX_CONNECTIONS = 3 IDF_MAX_CONNECTIONS = 9 # Connection slot tracking keys -KEY_ESP32_BLE = "esp32_ble" +KEY_ESP32_BLE = DOMAIN KEY_USED_CONNECTION_SLOTS = "used_connection_slots" # Export for use by other components (bluetooth_proxy, etc.) @@ -361,7 +363,7 @@ CONFIG_SCHEMA = cv.Schema( cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS) ), cv.Optional(CONF_USE_PSRAM): cv.All( - cv.only_on_esp32, cv.requires_component("psram"), cv.boolean + cv.only_on_esp32, cv.requires_component(PSRAM_DOMAIN), cv.boolean ), } ).extend(cv.COMPONENT_SCHEMA) @@ -404,7 +406,7 @@ def consume_connection_slots( """ def _consume_connection_slots(config: MutableMapping) -> MutableMapping: - data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {}) + data: dict[str, Any] = CORE.data.setdefault(DOMAIN, {}) slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) slots.extend([consumer] * value) return config @@ -418,7 +420,7 @@ def validate_connection_slots(max_connections: int) -> None: if CORE.testing_mode: return - ble_data = CORE.data.get(KEY_ESP32_BLE, {}) + ble_data = CORE.data.get(DOMAIN, {}) used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, []) num_used = len(used_slots) @@ -473,7 +475,7 @@ def final_validation(config: ConfigType) -> None: validate_connection_slots(max_connections) # Check if hosted bluetooth is being used - if "esp32_hosted" in full_config: + if ESP32_HOSTED_DOMAIN in full_config: from esphome.components.esp32_hosted import uses_esp_hosted_3x add_idf_sdkconfig_option("CONFIG_BT_CLASSIC_ENABLED", False) diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index e9c44284e4..7498228f4c 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -9,6 +9,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] +DOMAIN = "esp32_ble_beacon" esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) diff --git a/esphome/components/esp32_ble_client/__init__.py b/esphome/components/esp32_ble_client/__init__.py index 55619f1fc0..dbb409b17e 100644 --- a/esphome/components/esp32_ble_client/__init__.py +++ b/esphome/components/esp32_ble_client/__init__.py @@ -4,6 +4,7 @@ from esphome.components import esp32_ble_tracker AUTO_LOAD = ["esp32_ble_tracker"] CODEOWNERS = ["@jesserockz", "@bdraco"] DEPENDENCIES = ["esp32"] +DOMAIN = "esp32_ble_client" esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client") BLEClientBase = esp32_ble_client_ns.class_( diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 68fccac288..dde47b0c5e 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -150,6 +150,8 @@ void BLEClientBase::connect() { // A registration whose event never arrived must not block this connection's release. this->services_released_ = false; this->pending_notify_regs_ = 0; + // A CONNECT_EVT seen while idle can leave the id of a link this attempt does not own. + this->conn_id_ = UNSET_CONN_ID; // Enable loop for state processing this->enable_loop(); // Immediately transition to CONNECTING to prevent duplicate connection attempts @@ -350,7 +352,6 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (!this->check_addr(param->open.remote_bda)) return false; this->log_gattc_lifecycle_event_("OPEN"); - // conn_id was already set in ESP_GATTC_CONNECT_EVT this->service_count_ = 0; // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an @@ -362,19 +363,24 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } - if (this->state() != espbt::ClientState::CONNECTING) { - // This should not happen but lets log it in case it does - // because it means we have a bad assumption about how the - // ESP BT stack works. + const bool open_failed = param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN; + // Bluedroid reports a failed open as DISCONNECT_EVT then a failing OPEN_EVT, with no CONNECT_EVT, + // so DISCONNECTING with no id is expected for a failure. Anything else means a bad assumption + // about how the ESP BT stack works, so log it. + const bool expected_failure = + open_failed && this->state() == espbt::ClientState::DISCONNECTING && this->conn_id_ == UNSET_CONN_ID; + if (this->state() != espbt::ClientState::CONNECTING && !expected_failure) { ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state()), param->open.status); } - if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { + if (open_failed) { this->log_gattc_warning_("Connection open", param->open.status); // Connection was never established so CLOSE_EVT may not follow this->set_idle_(); break; } + // An open on a link that is already up sends no CONNECT_EVT, so take the id here. + this->conn_id_ = param->open.conn_id; if (this->want_disconnect_) { // Disconnect was requested after connecting started, // but before the connection was established. Now that we have diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 9355d58859..75a099a3c4 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -5,7 +5,7 @@ import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.const import CONF_DESCRIPTION, CONF_MANUFACTURER from esphome.components.esp32 import request_bluetooth -from esphome.components.esp32_ble import BTLoggers, bt_uuid +from esphome.components.esp32_ble import DOMAIN as ESP32_BLE_DOMAIN, BTLoggers, bt_uuid import esphome.config_validation as cv from esphome.config_validation import UNDEFINED from esphome.const import ( @@ -311,7 +311,7 @@ def final_validate_config(config) -> None: max_clients = config[CONF_MAX_CLIENTS] if max_clients > 1: full_config = fv.full_config.get() - ble_config = full_config.get("esp32_ble", {}) + ble_config = full_config.get(ESP32_BLE_DOMAIN, {}) max_connections = ble_config.get( "max_connections", esp32_ble.DEFAULT_MAX_CONNECTIONS ) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 3c41d22903..2d4e1ed7e9 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -33,6 +33,7 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["camera"] DEPENDENCIES = ["esp32"] +DOMAIN = "esp32_camera" esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index d54d5c6937..c063eb88be 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@ayufan"] AUTO_LOAD = ["camera"] DEPENDENCIES = ["network"] +DOMAIN = "esp32_camera_web_server" MULTI_CONF = True esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") diff --git a/esphome/components/esp32_can/__init__.py b/esphome/components/esp32_can/__init__.py index e69de29bb2..eb2b5b2aaa 100644 --- a/esphome/components/esp32_can/__init__.py +++ b/esphome/components/esp32_can/__init__.py @@ -0,0 +1 @@ +DOMAIN = "esp32_can" diff --git a/esphome/components/esp32_dac/__init__.py b/esphome/components/esp32_dac/__init__.py index e69de29bb2..6aa72c6456 100644 --- a/esphome/components/esp32_dac/__init__.py +++ b/esphome/components/esp32_dac/__init__.py @@ -0,0 +1 @@ +DOMAIN = "esp32_dac" diff --git a/esphome/components/esp32_hall/__init__.py b/esphome/components/esp32_hall/__init__.py index e69de29bb2..a8e97842ae 100644 --- a/esphome/components/esp32_hall/__init__.py +++ b/esphome/components/esp32_hall/__init__.py @@ -0,0 +1 @@ +DOMAIN = "esp32_hall" diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index d78941ea4c..f84f9c6464 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -28,6 +28,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@swoboda1337"] DEPENDENCIES = ["esp32"] +DOMAIN = "esp32_hosted" # esp32_ble raises the task watchdog around the remote BT controller bring-up AUTO_LOAD = ["watchdog"] diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index a213a78778..9d3562ed7b 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -7,6 +7,7 @@ from esphome.core import CORE from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] +DOMAIN = "esp32_rmt" VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} diff --git a/esphome/components/esp32_rmt_led_strip/__init__.py b/esphome/components/esp32_rmt_led_strip/__init__.py index e69de29bb2..a33110a7d3 100644 --- a/esphome/components/esp32_rmt_led_strip/__init__.py +++ b/esphome/components/esp32_rmt_led_strip/__init__.py @@ -0,0 +1 @@ +DOMAIN = "esp32_rmt_led_strip" diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index ede6beb9b6..bdd16bc2ef 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -31,6 +31,7 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["binary_sensor"] DEPENDENCIES = ["esp32"] +DOMAIN = "esp32_touch" CONF_DEBOUNCE_COUNT = "debounce_count" CONF_DENOISE_GRADE = "denoise_grade" diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 57fac2dc98..d95cbd239b 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -71,6 +71,7 @@ CONF_ENABLE_SCANF_FLOAT = "enable_scanf_float" _SCANF_FLOAT_RE = re.compile(r"scanf\s*\([^;]*?%[*\d.]*[hlL]*[feEgGaAF]") CODEOWNERS = ["@esphome/core"] +DOMAIN = "esp8266" _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True @@ -443,6 +444,8 @@ async def to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO") cg.add_build_flag("-Wno-nonnull-compare") + # .rodata is linked into RAM on ESP8266; keep switch lookup tables (CSWTCH) out of it + cg.add_build_flag("-fno-tree-switch-conversion") if use_platformio: cg.add_platformio_option("framework", "arduino") cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index ae09bdb91c..1d97b2c1e5 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP8266 #include "gpio.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome::esp8266 { @@ -74,7 +75,7 @@ ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const { return ISRInternalGPIOPin((void *) arg); } -void ESP8266GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { +void ESP8266GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const { uint8_t arduino_mode = 0; switch (type) { case gpio::INTERRUPT_RISING_EDGE: diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index 57ef06106a..84968d9752 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -2,12 +2,12 @@ #ifdef USE_ESP8266 -#include "esphome/core/hal.h" +#include "esphome/core/gpio_pin.h" #include namespace esphome::esp8266 { -class ESP8266GPIOPin final : public InternalGPIOPin { +class ESP8266GPIOPin final : public GPIOPin { public: void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } @@ -18,14 +18,18 @@ class ESP8266GPIOPin final : public InternalGPIOPin { bool digital_read() override; void digital_write(bool value) override; size_t dump_summary(char *buffer, size_t len) const override; - void detach_interrupt() const override; - ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return pin_; } + void detach_interrupt() const; + template void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const { + this->attach_interrupt_(reinterpret_cast(func), arg, type); + } + ISRInternalGPIOPin to_isr() const; + uint8_t get_pin() const { return pin_; } gpio::Flags get_flags() const override { return flags_; } - bool is_inverted() const override { return inverted_; } + bool is_inverted() const { return inverted_; } + bool is_internal() override { return true; } protected: - void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const; uint8_t pin_; bool inverted_{}; diff --git a/esphome/components/esp8266/hal.h b/esphome/components/esp8266/hal.h index f3b33da692..1a54065541 100644 --- a/esphome/components/esp8266/hal.h +++ b/esphome/components/esp8266/hal.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -40,6 +41,9 @@ void delay_microseconds_safe(uint32_t us); /// which is ISR-safe) so this helper is unused on this platform. __attribute__((always_inline)) inline bool in_isr_context() { return false; } +/// SDK SYS callbacks and interrupt handlers cannot yield, so they are not the loop context. +__attribute__((always_inline)) inline bool is_main_loop_thread() { return can_yield(); } + __attribute__((always_inline)) inline void yield() { ::yield(); } __attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } void delay(uint32_t ms); diff --git a/esphome/components/esp8266_pwm/__init__.py b/esphome/components/esp8266_pwm/__init__.py index e69de29bb2..741b06bb8b 100644 --- a/esphome/components/esp8266_pwm/__init__.py +++ b/esphome/components/esp8266_pwm/__init__.py @@ -0,0 +1 @@ +DOMAIN = "esp8266_pwm" diff --git a/esphome/components/espectre/__init__.py b/esphome/components/espectre/__init__.py index a46d6507fd..4f10db996c 100644 --- a/esphome/components/espectre/__init__.py +++ b/esphome/components/espectre/__init__.py @@ -10,8 +10,9 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.components.wifi import DOMAIN as WIFI_DOMAIN import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_WIFI +from esphome.const import CONF_ID from esphome.core import CORE from esphome.types import ConfigType @@ -162,7 +163,7 @@ async def to_code(config: ConfigType) -> None: if (off_hits := config.get(CONF_MOTION_OFF_HITS)) is not None: cg.add(var.set_motion_off_hits(off_hits)) if esp32.get_esp32_variant() == VARIANT_ESP32C5: - band = CORE.config[CONF_WIFI].get(wifi.CONF_BAND_MODE, "AUTO") + band = CORE.config[WIFI_DOMAIN].get(wifi.CONF_BAND_MODE, "AUTO") cg.add( var.set_wifi_band_policy( { diff --git a/esphome/components/esphome/__init__.py b/esphome/components/esphome/__init__.py new file mode 100644 index 0000000000..8bcc59dd2d --- /dev/null +++ b/esphome/components/esphome/__init__.py @@ -0,0 +1 @@ +DOMAIN = "esphome" diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 0cb2657ea1..dc4fc5b47e 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -6,7 +6,12 @@ from esphome.components.noise import ( new_psk_progmem, static_encryption_key, ) -from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.components.ota import ( + BASE_OTA_SCHEMA, + DOMAIN as OTA_DOMAIN, + OTAComponent, + ota_to_code, +) from esphome.config_helpers import filter_source_files_from_defines, merge_config import esphome.config_validation as cv from esphome.const import ( @@ -31,6 +36,8 @@ from esphome.espota2 import CONF_ALLOW_PLAINTEXT_UPLOAD import esphome.final_validate as fv from esphome.types import ConfigType +from .. import DOMAIN + CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access" _LOGGER = logging.getLogger(__name__) @@ -56,12 +63,12 @@ ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", OTAComponent) def ota_esphome_final_validate(config: ConfigType) -> None: full_conf = fv.full_config.get() - full_ota_conf = full_conf[CONF_OTA] + full_ota_conf = full_conf[OTA_DOMAIN] new_ota_conf = [] merged_ota_esphome_configs_by_port = {} ports_with_merged_configs = [] for ota_conf in full_ota_conf: - if ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME: + if ota_conf.get(CONF_PLATFORM) == DOMAIN: if ( conf_port := ota_conf.get(CONF_PORT) ) not in merged_ota_esphome_configs_by_port: @@ -182,7 +189,7 @@ def ota_esphome_final_validate(config: ConfigType) -> None: CONF_WEB_SERVER, ) - full_conf[CONF_OTA] = new_ota_conf + full_conf[OTA_DOMAIN] = new_ota_conf fv.full_config.set(full_conf) if len(ports_with_merged_configs) > 0: diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 318e1e5282..e6b19c42f0 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -635,7 +635,7 @@ error: this->cleanup_connection_(); - this->status_momentary_error("err", 5000); + this->status_momentary_error(5000); #ifdef USE_OTA_STATE_LISTENER this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index dc36885f2d..aef8fd17b5 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -4,7 +4,9 @@ from esphome import automation, core import esphome.codegen as cg from esphome.components import wifi from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant +from esphome.components.esp32_hosted import DOMAIN as ESP32_HOSTED_DOMAIN from esphome.components.udp import CONF_ON_RECEIVE +from esphome.components.wifi import DOMAIN as WIFI_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -23,6 +25,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] AUTO_LOAD = ["network"] +DOMAIN = "espnow" peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) @@ -140,7 +143,7 @@ def _validate_variant(config: ConfigType) -> ConfigType: return config if variant != VARIANT_ESP32P4: raise cv.Invalid(f"ESP-NOW is not supported on {variant} (no Wi-Fi radio)") - if "esp32_hosted" not in fv.full_config.get(): + if ESP32_HOSTED_DOMAIN not in fv.full_config.get(): raise cv.Invalid(f"ESP-NOW on {variant} requires the esp32_hosted component") return config @@ -176,7 +179,7 @@ async def to_code(config: ConfigType) -> None: include_builtin_idf_component("esp_wifi") - if CONF_WIFI in CORE.config: + if WIFI_DOMAIN in CORE.config: # Track the Wi-Fi channel via connect events instead of polling every loop wifi.request_wifi_connect_state_listener() if wifi_channel := config.get(CONF_CHANNEL): diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 3fd468764a..4d888a9f37 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -406,7 +406,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl if (packet == nullptr) { this->send_packet_queue_.increment_dropped_count(); ESP_LOGE(TAG, "Failed to allocate send packet from pool"); - this->status_momentary_warning("send-packet-pool-full"); + this->status_momentary_warning(); return ESP_ERR_ESPNOW_NO_MEM; } // Load the packet data @@ -434,7 +434,7 @@ void ESPNowComponent::send_() { if (packet->callback_ != nullptr) { packet->callback_(err); } - this->status_momentary_warning("send-failed"); + this->status_momentary_warning(); this->send_packet_pool_.release(packet); this->current_send_packet_ = nullptr; // Reset current packet return; @@ -447,7 +447,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) { } if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) { - this->status_momentary_warning("peer-add-failed"); + this->status_momentary_warning(); return ESP_ERR_INVALID_MAC; } @@ -462,7 +462,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) { char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(peer, peer_buf); ESP_LOGE(TAG, "Failed to add peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err))); - this->status_momentary_warning("peer-add-failed"); + this->status_momentary_warning(); return err; } } @@ -492,7 +492,7 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) { char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(peer, peer_buf); ESP_LOGE(TAG, "Failed to delete peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err))); - this->status_momentary_warning("peer-del-failed"); + this->status_momentary_warning(); return err; } } diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ff05caca83..58915ff96d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -import logging from esphome import automation, pins from esphome.automation import Condition @@ -61,13 +60,12 @@ import esphome.final_validate as fv from esphome.types import ConfigType AUTO_LOAD = ["network"] -LOGGER = logging.getLogger(__name__) +DOMAIN = "ethernet" # Key for tracking IP state listener count in CORE.data ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners" # Key for tracking configured ethernet type ETHERNET_TYPE_KEY = "ethernet_type" -KEY_ETHERNET = "ethernet" def request_ethernet_ip_state_listener() -> None: @@ -214,13 +212,6 @@ CLK_MODES = { "CLK_OUT": emac_rmii_clock_mode_t.EMAC_CLK_OUT, } -CLK_MODES_DEPRECATED = { - "GPIO0_IN": ("CLK_EXT_IN", 0), - "GPIO0_OUT": ("CLK_OUT", 0), - "GPIO16_OUT": ("CLK_OUT", 16), - "GPIO17_OUT": ("CLK_OUT", 17), -} - spi_host_device_t = cg.global_ns.enum("spi_host_device_t") SPI_INTERFACE_MAP = { @@ -387,23 +378,6 @@ def _validate(config: ConfigType) -> ConfigType: get_esp32_variant, ) - if CONF_CLK_MODE in config: - mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated. " - "Please replace 'clk_mode: %s' with:\n" - " clk:\n" - " mode: %s\n" - " pin: %s\n" - "Removal scheduled for 2026.11.0.", - config[CONF_CLK_MODE], - mode, - pin, - ) - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) - del config[CONF_CLK_MODE] - elif CONF_CLK not in config: - raise cv.Invalid("'clk' is a required option for [ethernet].") variant = get_esp32_variant() if variant not in (VARIANT_ESP32, VARIANT_ESP32P4): raise cv.Invalid( @@ -452,10 +426,17 @@ RMII_SCHEMA = cv.All( { cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_CLK_MODE): cv.enum( - CLK_MODES_DEPRECATED, upper=True, space="_" + # Removed in 2026.11.0 - kept to provide helpful error message + # Remove before 2027.5.0 + cv.Optional(CONF_CLK_MODE): cv.invalid( + "The 'clk_mode' option has been removed in ESPHome 2026.11.0.\n" + "Replace it with a 'clk:' block containing 'mode:' and 'pin:':\n" + " GPIO0_IN -> mode: CLK_EXT_IN, pin: GPIO0\n" + " GPIO0_OUT -> mode: CLK_OUT, pin: GPIO0\n" + " GPIO16_OUT -> mode: CLK_OUT, pin: GPIO16\n" + " GPIO17_OUT -> mode: CLK_OUT, pin: GPIO17" ), - cv.Optional(CONF_CLK): CLK_SCHEMA, + cv.Required(CONF_CLK): CLK_SCHEMA, cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), @@ -672,7 +653,7 @@ async def to_code(config: ConfigType) -> None: # enable_on_boot defaults to true in C++ - only set if false if not config[CONF_ENABLE_ON_BOOT]: cg.add(var.set_enable_on_boot(False)) - CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE] + CORE.data.setdefault(DOMAIN, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE] if CONF_MANUAL_IP in config: cg.add_define("USE_ETHERNET_MANUAL_IP") @@ -913,7 +894,7 @@ _define_filter = filter_source_files_from_defines( def _filter_source_files() -> list[str]: excluded = _platform_filter() + _define_filter() - eth_data = CORE.data.get(KEY_ETHERNET, {}) + eth_data = CORE.data.get(DOMAIN, {}) eth_type = eth_data.get(ETHERNET_TYPE_KEY) # Only compile the custom JL1101 driver when JL1101 is configured # and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x) diff --git a/esphome/components/ethernet_info/__init__.py b/esphome/components/ethernet_info/__init__.py index 312688d3fd..3f72edbc7d 100644 --- a/esphome/components/ethernet_info/__init__.py +++ b/esphome/components/ethernet_info/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@gtjadsonsantos"] +DOMAIN = "ethernet_info" diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index d02863b9ee..c40130634f 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -27,6 +27,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType CODEOWNERS = ["@nohat"] +DOMAIN = "event" IS_PLATFORM_COMPONENT = True DEVICE_CLASSES = [ diff --git a/esphome/components/exponential_moving_average/__init__.py b/esphome/components/exponential_moving_average/__init__.py index c58ce8a01e..7a389785e7 100644 --- a/esphome/components/exponential_moving_average/__init__.py +++ b/esphome/components/exponential_moving_average/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "exponential_moving_average" diff --git a/esphome/components/exposure_notifications/__init__.py b/esphome/components/exposure_notifications/__init__.py index 4f7e698e23..adcfa83717 100644 --- a/esphome/components/exposure_notifications/__init__.py +++ b/esphome/components/exposure_notifications/__init__.py @@ -10,6 +10,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter"] AUTO_LOAD = ["ble_device_base"] +DOMAIN = "exposure_notifications" exposure_notifications_ns = cg.esphome_ns.namespace("exposure_notifications") ExposureNotification = exposure_notifications_ns.struct("ExposureNotification") diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 504b1ae679..8994805432 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -6,7 +6,6 @@ from esphome import git, loader import esphome.config_validation as cv from esphome.const import ( CONF_COMPONENTS, - CONF_EXTERNAL_COMPONENTS, CONF_PASSWORD, CONF_PATH, CONF_REF, @@ -22,7 +21,7 @@ from esphome.core import CORE, TimePeriodSeconds _LOGGER = logging.getLogger(__name__) -DOMAIN = CONF_EXTERNAL_COMPONENTS +DOMAIN = "external_components" CONFIG_SCHEMA = cv.ensure_list( diff --git a/esphome/components/ezo/__init__.py b/esphome/components/ezo/__init__.py index e69de29bb2..60408a40ee 100644 --- a/esphome/components/ezo/__init__.py +++ b/esphome/components/ezo/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ezo" diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py index 1157ef1fb0..aed071e8cf 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( CODEOWNERS = ["@carlos-sarmiento"] DEPENDENCIES = ["i2c"] +DOMAIN = "ezo_pmp" MULTI_CONF = True CONF_VOLUME_PER_MINUTE = "volume_per_minute" diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index a9064eb18f..e2376c217c 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -15,6 +15,7 @@ from esphome.final_validate import full_config from esphome.types import ConfigType CODEOWNERS = ["@anatoly-savchenkov"] +DOMAIN = "factory_reset" factory_reset_ns = cg.esphome_ns.namespace("factory_reset") FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 526499fe07..ac5469fc42 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -38,6 +38,7 @@ from esphome.core.entity_helpers import ( setup_entity, ) +DOMAIN = "fan" IS_PLATFORM_COMPONENT = True fan_ns = cg.esphome_ns.namespace("fan") diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index e2fc8578cd..8936627aaa 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -12,6 +12,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter"] +DOMAIN = "fastled_base" fastled_base_ns = cg.esphome_ns.namespace("fastled_base") FastLEDLightOutput = fastled_base_ns.class_( "FastLEDLightOutput", light.AddressableLight diff --git a/esphome/components/fastled_clockless/__init__.py b/esphome/components/fastled_clockless/__init__.py index e69de29bb2..05af618c2e 100644 --- a/esphome/components/fastled_clockless/__init__.py +++ b/esphome/components/fastled_clockless/__init__.py @@ -0,0 +1 @@ +DOMAIN = "fastled_clockless" diff --git a/esphome/components/fastled_spi/__init__.py b/esphome/components/fastled_spi/__init__.py index e69de29bb2..26451add65 100644 --- a/esphome/components/fastled_spi/__init__.py +++ b/esphome/components/fastled_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "fastled_spi" diff --git a/esphome/components/feedback/__init__.py b/esphome/components/feedback/__init__.py index 9ae2df986d..cfac77e797 100644 --- a/esphome/components/feedback/__init__.py +++ b/esphome/components/feedback/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ianchi"] +DOMAIN = "feedback" diff --git a/esphome/components/file/__init__.py b/esphome/components/file/__init__.py index f70ffa9520..e98751dac5 100644 --- a/esphome/components/file/__init__.py +++ b/esphome/components/file/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@esphome/core"] +DOMAIN = "file" diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 0266a9caad..0e522ced25 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -26,6 +26,7 @@ from esphome.const import ( CODEOWNERS = ["@OnFreund", "@loongyh", "@alexborro"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor", "sensor"] +DOMAIN = "fingerprint_grow" MULTI_CONF = True CONF_FINGERPRINT_GROW_ID = "fingerprint_grow_id" diff --git a/esphome/components/fs3000/__init__.py b/esphome/components/fs3000/__init__.py index e69de29bb2..c1a6b39d9b 100644 --- a/esphome/components/fs3000/__init__.py +++ b/esphome/components/fs3000/__init__.py @@ -0,0 +1 @@ +DOMAIN = "fs3000" diff --git a/esphome/components/ft5x06/__init__.py b/esphome/components/ft5x06/__init__.py index dceea71dd0..9770f30e43 100644 --- a/esphome/components/ft5x06/__init__.py +++ b/esphome/components/ft5x06/__init__.py @@ -2,5 +2,6 @@ import esphome.codegen as cg CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["i2c"] +DOMAIN = "ft5x06" ft5x06_ns = cg.esphome_ns.namespace("ft5x06") diff --git a/esphome/components/ft63x6/__init__.py b/esphome/components/ft63x6/__init__.py index b6d7d3580e..565da54fe1 100644 --- a/esphome/components/ft63x6/__init__.py +++ b/esphome/components/ft63x6/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@gpambrozio"] +DOMAIN = "ft63x6" diff --git a/esphome/components/fujitsu_general/__init__.py b/esphome/components/fujitsu_general/__init__.py index e69de29bb2..fdb21576be 100644 --- a/esphome/components/fujitsu_general/__init__.py +++ b/esphome/components/fujitsu_general/__init__.py @@ -0,0 +1 @@ +DOMAIN = "fujitsu_general" diff --git a/esphome/components/gcja5/__init__.py b/esphome/components/gcja5/__init__.py index 122ffaf6ed..9df2024e2a 100644 --- a/esphome/components/gcja5/__init__.py +++ b/esphome/components/gcja5/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@gcormier"] +DOMAIN = "gcja5" diff --git a/esphome/components/gdk101/__init__.py b/esphome/components/gdk101/__init__.py index f98af3f863..e0ede6136d 100644 --- a/esphome/components/gdk101/__init__.py +++ b/esphome/components/gdk101/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@Szewcson"] +DOMAIN = "gdk101" DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/gl_r01_i2c/__init__.py b/esphome/components/gl_r01_i2c/__init__.py index e69de29bb2..23c5e19b18 100644 --- a/esphome/components/gl_r01_i2c/__init__.py +++ b/esphome/components/gl_r01_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "gl_r01_i2c" diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index 8adf4e983c..a35e228045 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -12,6 +12,7 @@ from esphome.core import CoroPriority, coroutine_with_priority from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "globals" globals_ns = cg.esphome_ns.namespace("globals") GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component) RestoringGlobalsComponent = globals_ns.class_( diff --git a/esphome/components/gp2y1010au0f/__init__.py b/esphome/components/gp2y1010au0f/__init__.py index e69de29bb2..36c643103c 100644 --- a/esphome/components/gp2y1010au0f/__init__.py +++ b/esphome/components/gp2y1010au0f/__init__.py @@ -0,0 +1 @@ +DOMAIN = "gp2y1010au0f" diff --git a/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp b/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp index 552261155f..ac98a01fd5 100644 --- a/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp +++ b/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp @@ -7,6 +7,9 @@ namespace esphome::gp2y1010au0f { ESPHOME_LOG_TAG(TAG, "gp2y1010au0f"); + +static constexpr uint32_t READ_TIMEOUT_ID = 0; + static const float MIN_VOLTAGE = 0.0f; static const float MAX_VOLTAGE = 4.0f; @@ -22,7 +25,7 @@ void GP2Y1010AU0FSensor::dump_config() { void GP2Y1010AU0FSensor::update() { is_sampling_ = true; - this->set_timeout("read", this->sample_duration_, [this]() { + this->set_timeout(READ_TIMEOUT_ID, this->sample_duration_, [this]() { this->is_sampling_ = false; if (this->num_samples_ == 0) return; diff --git a/esphome/components/gp8403/__init__.py b/esphome/components/gp8403/__init__.py index 17c88b6875..583b1d1568 100644 --- a/esphome/components/gp8403/__init__.py +++ b/esphome/components/gp8403/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@jesserockz", "@sebydocky"] DEPENDENCIES = ["i2c"] +DOMAIN = "gp8403" MULTI_CONF = True gp8403_ns = cg.esphome_ns.namespace("gp8403") diff --git a/esphome/components/gpio/__init__.py b/esphome/components/gpio/__init__.py index 07ebb64cd2..e529b2276d 100644 --- a/esphome/components/gpio/__init__.py +++ b/esphome/components/gpio/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg CODEOWNERS = ["@esphome/core"] +DOMAIN = "gpio" gpio_ns = cg.esphome_ns.namespace("gpio") diff --git a/esphome/components/gpio/output/__init__.py b/esphome/components/gpio/output/__init__.py index ab242c643f..645dd61f80 100644 --- a/esphome/components/gpio/output/__init__.py +++ b/esphome/components/gpio/output/__init__.py @@ -1,8 +1,10 @@ from esphome import pins import esphome.codegen as cg from esphome.components import output +from esphome.components.const import CONF_HOLD_STATE import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_PIN +from esphome.const import CONF_ID, CONF_PIN, CONF_POWER_SUPPLY +import esphome.final_validate as fv from esphome.types import ConfigType from .. import gpio_ns @@ -17,6 +19,27 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) +def _require_hold(ps_config: ConfigType) -> ConfigType: + if not ps_config[CONF_PIN].get(CONF_HOLD_STATE): + raise cv.Invalid( + f"The power supply pin must also set {CONF_HOLD_STATE} " + f"when an output it powers sets it", + [CONF_PIN], + ) + return ps_config + + +def _final_validate(config: ConfigType) -> None: + if not config[CONF_PIN].get(CONF_HOLD_STATE): + return + if (ps_id := config.get(CONF_POWER_SUPPLY)) is None: + return + fv.id_declaration_match_schema(_require_hold)(ps_id) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await output.register_output(var, config) diff --git a/esphome/components/gpio/output/gpio_binary_output.h b/esphome/components/gpio/output/gpio_binary_output.h index 496afd131b..06e3647482 100644 --- a/esphome/components/gpio/output/gpio_binary_output.h +++ b/esphome/components/gpio/output/gpio_binary_output.h @@ -11,9 +11,12 @@ class GPIOBinaryOutput final : public output::BinaryOutput, public Component { void set_pin(GPIOPin *pin) { pin_ = pin; } void setup() override { - this->turn_off(); + const bool held = this->pin_->is_held(); + if (!held) + this->turn_off(); this->pin_->setup(); - this->turn_off(); + if (!held) + this->turn_off(); } void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 4daea8d877..450a97eb19 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -12,8 +12,9 @@ float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; void GPIOSwitch::setup() { bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); - // write state before setup - this->control(initial_state); + // write state before setup unless the pin kept it across the reset + if (!this->pin_->is_held()) + this->control(initial_state); this->pin_->setup(); // write after setup again for other IOs this->control(initial_state); diff --git a/esphome/components/gpio_expander/__init__.py b/esphome/components/gpio_expander/__init__.py index 0c7199b6df..cf6be6e8e0 100644 --- a/esphome/components/gpio_expander/__init__.py +++ b/esphome/components/gpio_expander/__init__.py @@ -3,6 +3,8 @@ import esphome.config_validation as cv from esphome.const import CONF_ALLOW_OTHER_USES, CONF_INTERRUPT_PIN, CONF_INVERTED from esphome.types import ConfigType +DOMAIN = "gpio_expander" + def validate_interrupt_pin(value: ConfigType) -> ConfigType: # The expander components own INT polarity (active-low, hardcoded falling-edge ISR) diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 94a36a0afa..cc77dff496 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -33,6 +33,7 @@ DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor"] CODEOWNERS = ["@coogle", "@ximex"] +DOMAIN = "gps" gps_ns = cg.esphome_ns.namespace("gps") GPS = gps_ns.class_("GPS", cg.PollingComponent, uart.UARTDevice) diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index 1b99491f9c..0aee597334 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -32,6 +32,7 @@ from esphome.const import ( from esphome.types import ConfigType CODEOWNERS = ["@synco"] +DOMAIN = "graph" DEPENDENCIES = ["display", "sensor"] MULTI_CONF = True diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py index 668a0d74d1..a398aea619 100644 --- a/esphome/components/graphical_display_menu/__init__.py +++ b/esphome/components/graphical_display_menu/__init__.py @@ -13,7 +13,6 @@ from esphome.const import ( CONF_FONT, CONF_FOREGROUND_COLOR, CONF_ID, - CONF_TRIGGER_ID, ) from esphome.types import ConfigType @@ -29,11 +28,9 @@ MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArgument MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator( "const" ) -GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_( - "GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger -) CODEOWNERS = ["@MrMDavidson"] +DOMAIN = "graphical_display_menu" AUTO_LOAD = ["display_menu_base"] @@ -48,13 +45,7 @@ CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend( cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string), cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), - cv.Optional(CONF_ON_REDRAW): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - GraphicalDisplayMenuOnRedrawTrigger - ) - } - ), + cv.Optional(CONF_ON_REDRAW): automation.validate_automation({}), } ) ) @@ -91,9 +82,8 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_background_color(background_color)) for conf in config.get(CONF_ON_REDRAW, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf + await automation.build_callback_automation( + var, "add_on_redraw_callback", [(GraphicalDisplayMenuConstPtr, "it")], conf ) await display_menu_to_code(var, config) diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 8761e5fb4c..5f5b3e8d42 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -241,6 +241,6 @@ void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, co "draw_item should be called."); } -void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); } +void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(this); } } // namespace esphome::graphical_display_menu diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h index 13c0f9d73f..1f1d8ff3fc 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.h +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -70,17 +70,7 @@ class GraphicalDisplayMenu final : public display_menu_base::DisplayMenuComponen Color foreground_color_{COLOR_ON}; Color background_color_{COLOR_OFF}; - CallbackManager on_redraw_callbacks_{}; -}; - -class GraphicalDisplayMenuOnRedrawTrigger final : public Trigger { - public: - explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) { - parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); }); - } - - protected: - GraphicalDisplayMenu *parent_; + LazyCallbackManager on_redraw_callbacks_{}; }; } // namespace graphical_display_menu diff --git a/esphome/components/gree/__init__.py b/esphome/components/gree/__init__.py index 2dd9ac0f1c..67489b3876 100644 --- a/esphome/components/gree/__init__.py +++ b/esphome/components/gree/__init__.py @@ -1,3 +1,5 @@ import esphome.codegen as cg +DOMAIN = "gree" + gree_ns = cg.esphome_ns.namespace("gree") diff --git a/esphome/components/grove_gas_mc_v2/__init__.py b/esphome/components/grove_gas_mc_v2/__init__.py index e69de29bb2..31a956ac6f 100644 --- a/esphome/components/grove_gas_mc_v2/__init__.py +++ b/esphome/components/grove_gas_mc_v2/__init__.py @@ -0,0 +1 @@ +DOMAIN = "grove_gas_mc_v2" diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index 2d44d88323..0874613c5c 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( DEPENDENCIES = ["i2c"] CODEOWNERS = ["@max246"] +DOMAIN = "grove_tb6612fng" MULTI_CONF = True diff --git a/esphome/components/growatt_solar/__init__.py b/esphome/components/growatt_solar/__init__.py index e69de29bb2..078e836783 100644 --- a/esphome/components/growatt_solar/__init__.py +++ b/esphome/components/growatt_solar/__init__.py @@ -0,0 +1 @@ +DOMAIN = "growatt_solar" diff --git a/esphome/components/gsl3670/__init__.py b/esphome/components/gsl3670/__init__.py index c58ce8a01e..24d19ee887 100644 --- a/esphome/components/gsl3670/__init__.py +++ b/esphome/components/gsl3670/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "gsl3670" diff --git a/esphome/components/gt911/__init__.py b/esphome/components/gt911/__init__.py index 1f7ecd1d5e..2b45ba5efe 100644 --- a/esphome/components/gt911/__init__.py +++ b/esphome/components/gt911/__init__.py @@ -2,5 +2,6 @@ import esphome.codegen as cg CODEOWNERS = ["@jesserockz", "@clydebarrow"] DEPENDENCIES = ["i2c"] +DOMAIN = "gt911" gt911_ns = cg.esphome_ns.namespace("gt911") diff --git a/esphome/components/haier/__init__.py b/esphome/components/haier/__init__.py index e69de29bb2..605d315185 100644 --- a/esphome/components/haier/__init__.py +++ b/esphome/components/haier/__init__.py @@ -0,0 +1 @@ +DOMAIN = "haier" diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 5c4d498912..ba76053686 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -4,13 +4,14 @@ from esphome import automation import esphome.codegen as cg from esphome.components import climate, logger, uart from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode +from esphome.components.logger import DOMAIN as LOGGER_DOMAIN +from esphome.components.wifi import DOMAIN as WIFI_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_BEEPER, CONF_CURRENT_TEMPERATURE, CONF_DISPLAY, CONF_LEVEL, - CONF_LOGGER, CONF_LOGS, CONF_MAX_TEMPERATURE, CONF_MIN_TEMPERATURE, @@ -22,7 +23,6 @@ from esphome.const import ( CONF_TARGET_TEMPERATURE, CONF_TEMPERATURE_STEP, CONF_VISUAL, - CONF_WIFI, ) from esphome.cpp_generator import MockObjClass import esphome.final_validate as fv @@ -326,9 +326,9 @@ automation.register_apply_action( def _final_validate(config) -> None: full_config = fv.full_config.get() - if CONF_LOGGER in full_config: + if LOGGER_DOMAIN in full_config: _level = "NONE" - logger_config = full_config[CONF_LOGGER] + logger_config = full_config[LOGGER_DOMAIN] if CONF_LOGS in logger_config: if "haier.protocol" in logger_config[CONF_LOGS]: _level = logger_config[CONF_LOGS]["haier.protocol"] @@ -344,7 +344,7 @@ def _final_validate(config) -> None: "No logger component found, logging for Haier protocol is disabled" ) cg.add_build_flag("-DHAIER_LOG_LEVEL=0") - if config.get(CONF_WIFI_SIGNAL) and CONF_WIFI not in full_config: + if config.get(CONF_WIFI_SIGNAL) and WIFI_DOMAIN not in full_config: raise cv.Invalid( f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration" ) diff --git a/esphome/components/havells_solar/__init__.py b/esphome/components/havells_solar/__init__.py index e69de29bb2..f6fa38817c 100644 --- a/esphome/components/havells_solar/__init__.py +++ b/esphome/components/havells_solar/__init__.py @@ -0,0 +1 @@ +DOMAIN = "havells_solar" diff --git a/esphome/components/hbridge/__init__.py b/esphome/components/hbridge/__init__.py index 7eae863ff5..bbf8313de0 100644 --- a/esphome/components/hbridge/__init__.py +++ b/esphome/components/hbridge/__init__.py @@ -1,3 +1,5 @@ import esphome.codegen as cg +DOMAIN = "hbridge" + hbridge_ns = cg.esphome_ns.namespace("hbridge") diff --git a/esphome/components/hc8/__init__.py b/esphome/components/hc8/__init__.py index e1028456b0..bbcfae894a 100644 --- a/esphome/components/hc8/__init__.py +++ b/esphome/components/hc8/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@omartijn"] +DOMAIN = "hc8" diff --git a/esphome/components/hdc1080/__init__.py b/esphome/components/hdc1080/__init__.py index e69de29bb2..7c1f276f3d 100644 --- a/esphome/components/hdc1080/__init__.py +++ b/esphome/components/hdc1080/__init__.py @@ -0,0 +1 @@ +DOMAIN = "hdc1080" diff --git a/esphome/components/hdc2010/__init__.py b/esphome/components/hdc2010/__init__.py index badf9dbb0c..839dc630b2 100644 --- a/esphome/components/hdc2010/__init__.py +++ b/esphome/components/hdc2010/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@optimusprimespace", "@ssieb"] +DOMAIN = "hdc2010" diff --git a/esphome/components/hdc2080/__init__.py b/esphome/components/hdc2080/__init__.py index 341ea61048..e1ab22cc41 100644 --- a/esphome/components/hdc2080/__init__.py +++ b/esphome/components/hdc2080/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@G-Pereira", "@jesserockz"] +DOMAIN = "hdc2080" diff --git a/esphome/components/hdc302x/__init__.py b/esphome/components/hdc302x/__init__.py index d8165281bf..2fbfb0191c 100644 --- a/esphome/components/hdc302x/__init__.py +++ b/esphome/components/hdc302x/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@joshuasing"] +DOMAIN = "hdc302x" diff --git a/esphome/components/hdc302x/hdc302x.cpp b/esphome/components/hdc302x/hdc302x.cpp index b45b3b612f..d542848657 100644 --- a/esphome/components/hdc302x/hdc302x.cpp +++ b/esphome/components/hdc302x/hdc302x.cpp @@ -8,6 +8,8 @@ namespace esphome::hdc302x { ESPHOME_LOG_TAG(TAG, "hdc302x.sensor"); +static constexpr uint32_t HEATER_OFF_TIMEOUT_ID = 0; + // Commands (per datasheet Table 7-4) static const uint8_t HDC302X_CMD_SOFT_RESET[2] = {0x30, 0xa2}; static const uint8_t HDC302X_CMD_CLEAR_STATUS_REGISTER[2] = {0x30, 0x41}; @@ -68,14 +70,14 @@ void HDC302XComponent::start_heater(uint16_t power, uint32_t duration_ms) { return; } this->heater_active_ = true; - this->cancel_timeout("heater_off"); + this->cancel_timeout(HEATER_OFF_TIMEOUT_ID); if (duration_ms > 0) { - this->set_timeout("heater_off", duration_ms, [this]() { this->stop_heater(); }); + this->set_timeout(HEATER_OFF_TIMEOUT_ID, duration_ms, [this]() { this->stop_heater(); }); } } void HDC302XComponent::stop_heater() { - this->cancel_timeout("heater_off"); + this->cancel_timeout(HEATER_OFF_TIMEOUT_ID); if (!this->disable_heater_()) { ESP_LOGW(TAG, "Heater stop failed"); } diff --git a/esphome/components/he60r/__init__.py b/esphome/components/he60r/__init__.py index c58ce8a01e..070ba595a3 100644 --- a/esphome/components/he60r/__init__.py +++ b/esphome/components/he60r/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "he60r" diff --git a/esphome/components/heatpumpir/__init__.py b/esphome/components/heatpumpir/__init__.py index e69de29bb2..f0ef79b895 100644 --- a/esphome/components/heatpumpir/__init__.py +++ b/esphome/components/heatpumpir/__init__.py @@ -0,0 +1 @@ +DOMAIN = "heatpumpir" diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index eb23eb8ed5..2376a67416 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -187,15 +187,18 @@ void HeatpumpIRClimate::transmit_state() { swing_h_cmd = HDIR_SWING; } + // MitsubishiMSCHeatpumpIR only knows FAN_1..FAN_3 and sends anything else as auto, + // so low/medium/high have to start at FAN_1 there instead of FAN_2. + const bool three_speed_fan = this->protocol_ == PROTOCOL_MITSUBISHI_MSC; switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) { case climate::CLIMATE_FAN_LOW: - fan_speed_cmd = FAN_2; + fan_speed_cmd = three_speed_fan ? FAN_1 : FAN_2; break; case climate::CLIMATE_FAN_MEDIUM: - fan_speed_cmd = FAN_3; + fan_speed_cmd = three_speed_fan ? FAN_2 : FAN_3; break; case climate::CLIMATE_FAN_HIGH: - fan_speed_cmd = FAN_4; + fan_speed_cmd = three_speed_fan ? FAN_3 : FAN_4; break; case climate::CLIMATE_FAN_AUTO: default: diff --git a/esphome/components/hitachi_ac344/__init__.py b/esphome/components/hitachi_ac344/__init__.py index e69de29bb2..15dac43260 100644 --- a/esphome/components/hitachi_ac344/__init__.py +++ b/esphome/components/hitachi_ac344/__init__.py @@ -0,0 +1 @@ +DOMAIN = "hitachi_ac344" diff --git a/esphome/components/hitachi_ac424/__init__.py b/esphome/components/hitachi_ac424/__init__.py index 10f2c27fe8..8372b84c7e 100644 --- a/esphome/components/hitachi_ac424/__init__.py +++ b/esphome/components/hitachi_ac424/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@sourabhjaiswal"] +DOMAIN = "hitachi_ac424" diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index b8040e5579..72d3cc2ada 100644 --- a/esphome/components/hlk_fm22x/__init__.py +++ b/esphome/components/hlk_fm22x/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( CODEOWNERS = ["@OnFreund"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"] +DOMAIN = "hlk_fm22x" MULTI_CONF = True CONF_HLK_FM22X_ID = "hlk_fm22x_id" diff --git a/esphome/components/hlw8012/__init__.py b/esphome/components/hlw8012/__init__.py index e69de29bb2..3da6262ce7 100644 --- a/esphome/components/hlw8012/__init__.py +++ b/esphome/components/hlw8012/__init__.py @@ -0,0 +1 @@ +DOMAIN = "hlw8012" diff --git a/esphome/components/hlw8032/__init__.py b/esphome/components/hlw8032/__init__.py index 4908e10037..fc9c674659 100644 --- a/esphome/components/hlw8032/__init__.py +++ b/esphome/components/hlw8032/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@rici4kubicek"] +DOMAIN = "hlw8032" diff --git a/esphome/components/hm3301/__init__.py b/esphome/components/hm3301/__init__.py index e69de29bb2..eb45989270 100644 --- a/esphome/components/hm3301/__init__.py +++ b/esphome/components/hm3301/__init__.py @@ -0,0 +1 @@ +DOMAIN = "hm3301" diff --git a/esphome/components/hmac_md5/__init__.py b/esphome/components/hmac_md5/__init__.py index e37eb9b116..72f9aa60fe 100644 --- a/esphome/components/hmac_md5/__init__.py +++ b/esphome/components/hmac_md5/__init__.py @@ -2,5 +2,6 @@ import esphome.config_validation as cv AUTO_LOAD = ["md5"] CODEOWNERS = ["@dwmw2"] +DOMAIN = "hmac_md5" CONFIG_SCHEMA = cv.Schema({}) diff --git a/esphome/components/hmac_sha256/__init__.py b/esphome/components/hmac_sha256/__init__.py index 158d740dc5..4f42c968a3 100644 --- a/esphome/components/hmac_sha256/__init__.py +++ b/esphome/components/hmac_sha256/__init__.py @@ -2,5 +2,6 @@ import esphome.config_validation as cv AUTO_LOAD = ["sha256"] CODEOWNERS = ["@dwmw2"] +DOMAIN = "hmac_sha256" CONFIG_SCHEMA = cv.Schema({}) diff --git a/esphome/components/hmc5883l/__init__.py b/esphome/components/hmc5883l/__init__.py index e69de29bb2..5326507e44 100644 --- a/esphome/components/hmc5883l/__init__.py +++ b/esphome/components/hmc5883l/__init__.py @@ -0,0 +1 @@ +DOMAIN = "hmc5883l" diff --git a/esphome/components/hoermann_hcp/__init__.py b/esphome/components/hoermann_hcp/__init__.py index 958b495c2e..addd1b299b 100644 --- a/esphome/components/hoermann_hcp/__init__.py +++ b/esphome/components/hoermann_hcp/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@zweckj"] DEPENDENCIES = ["modbus"] +DOMAIN = "hoermann_hcp" MULTI_CONF = True CONF_HOERMANN_HCP_ID = "hoermann_hcp_id" diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 9fead561af..3851bc3c0b 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -8,6 +8,7 @@ import esphome.final_validate as fv from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter", "@esphome/core"] +DOMAIN = "homeassistant" homeassistant_ns = cg.esphome_ns.namespace("homeassistant") diff --git a/esphome/components/honeywell_hih_i2c/__init__.py b/esphome/components/honeywell_hih_i2c/__init__.py index 8d13fcb152..5808cb6fc8 100644 --- a/esphome/components/honeywell_hih_i2c/__init__.py +++ b/esphome/components/honeywell_hih_i2c/__init__.py @@ -1,3 +1,4 @@ """Support for Honeywell HumidIcon HIH""" CODEOWNERS = ["@Benichou34"] +DOMAIN = "honeywell_hih_i2c" diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp index a237f3bd60..352d3f7f01 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -9,6 +9,8 @@ namespace esphome::honeywell_hih_i2c { ESPHOME_LOG_TAG(TAG, "honeywell_hih.i2c"); +static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0; + static const uint8_t REQUEST_CMD[1] = {0x00}; // Measurement Request Format static const uint16_t MAX_COUNT = 0x3FFE; // 2^14 - 2 @@ -68,13 +70,13 @@ void HoneywellHIComponent::update() { this->start_measurement_(); // The measurement cycle duration is typically 36.65 ms for temperature and humidity readings. - this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout_(); }); + this->set_timeout(MEASUREMENT_TIMEOUT_ID, 100, [this] { this->measurement_timeout_(); }); } void HoneywellHIComponent::loop() { if (this->measurement_running_ && this->is_measurement_ready_()) { this->measurement_running_ = false; - this->cancel_timeout("meas_timeout"); + this->cancel_timeout(MEASUREMENT_TIMEOUT_ID); this->read_sensor_data_(); } } diff --git a/esphome/components/honeywellabp/__init__.py b/esphome/components/honeywellabp/__init__.py index 03440d675d..e6e1fd8915 100644 --- a/esphome/components/honeywellabp/__init__.py +++ b/esphome/components/honeywellabp/__init__.py @@ -1 +1,3 @@ """Support for Honeywell ABP""" + +DOMAIN = "honeywellabp" diff --git a/esphome/components/honeywellabp2_i2c/__init__.py b/esphome/components/honeywellabp2_i2c/__init__.py index 29a910eca9..1205355bc4 100644 --- a/esphome/components/honeywellabp2_i2c/__init__.py +++ b/esphome/components/honeywellabp2_i2c/__init__.py @@ -1,3 +1,4 @@ """Support for Honeywell ABP2""" CODEOWNERS = ["@jpfaff"] +DOMAIN = "honeywellabp2_i2c" diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp index 891eb4447d..2ca3fb821c 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp @@ -11,6 +11,8 @@ static const uint8_t STATUS_MATH_SAT = 0; ESPHOME_LOG_TAG(TAG, "honeywellabp2"); +static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0; + void HONEYWELLABP2Sensor::read_sensor_data() { if (this->read(raw_data_, 7) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -62,7 +64,7 @@ float HONEYWELLABP2Sensor::get_temperature() { return this->last_temperature_; } void HONEYWELLABP2Sensor::loop() { if (this->measurement_running_) { if (this->is_measurement_ready()) { - this->cancel_timeout("meas_timeout"); + this->cancel_timeout(MEASUREMENT_TIMEOUT_ID); this->read_sensor_data(); if (pressure_sensor_ != nullptr) { @@ -79,7 +81,7 @@ void HONEYWELLABP2Sensor::update() { ESP_LOGV(TAG, "Update Honeywell ABP2 Sensor"); this->start_measurement(); - this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout(); }); + this->set_timeout(MEASUREMENT_TIMEOUT_ID, 100, [this] { this->measurement_timeout(); }); } void HONEYWELLABP2Sensor::dump_config() { diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 16e4c81d34..d0ec6f2535 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -22,6 +22,7 @@ from .gpio import host_pin_to_code # noqa: F401 CODEOWNERS = ["@esphome/core", "@clydebarrow"] AUTO_LOAD = ["network", "preferences"] +DOMAIN = "host" IS_TARGET_PLATFORM = True diff --git a/esphome/components/host/gpio.cpp b/esphome/components/host/gpio.cpp index c1fd56e945..b57bd41188 100644 --- a/esphome/components/host/gpio.cpp +++ b/esphome/components/host/gpio.cpp @@ -1,6 +1,7 @@ #ifdef USE_HOST #include "gpio.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { @@ -20,7 +21,7 @@ ISRInternalGPIOPin HostGPIOPin::to_isr() const { return ISRInternalGPIOPin((void *) arg); } -void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { +void HostGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const { ESP_LOGD(TAG, "Attaching interrupt %p to pin %d and mode %d", func, pin_, (uint32_t) type); } void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); } diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index bd2d09257b..fd3ebefe1f 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -2,11 +2,11 @@ #ifdef USE_HOST -#include "esphome/core/hal.h" +#include "esphome/core/gpio_pin.h" namespace esphome::host { -class HostGPIOPin final : public InternalGPIOPin { +class HostGPIOPin final : public GPIOPin { public: void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } @@ -17,14 +17,18 @@ class HostGPIOPin final : public InternalGPIOPin { bool digital_read() override; void digital_write(bool value) override; size_t dump_summary(char *buffer, size_t len) const override; - void detach_interrupt() const override; - ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return pin_; } + void detach_interrupt() const; + template void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const { + this->attach_interrupt_(reinterpret_cast(func), arg, type); + } + ISRInternalGPIOPin to_isr() const; + uint8_t get_pin() const { return pin_; } gpio::Flags get_flags() const override { return flags_; } - bool is_inverted() const override { return inverted_; } + bool is_inverted() const { return inverted_; } + bool is_internal() override { return true; } protected: - void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const; uint8_t pin_; bool inverted_{}; diff --git a/esphome/components/host/hal.h b/esphome/components/host/hal.h index 12abf6684d..c767632772 100644 --- a/esphome/components/host/hal.h +++ b/esphome/components/host/hal.h @@ -3,6 +3,7 @@ #ifdef USE_HOST #include +#include #include #define IRAM_ATTR @@ -16,6 +17,16 @@ namespace esphome { /// Host has no ISR concept. __attribute__((always_inline)) inline bool in_isr_context() { return false; } +inline pthread_t &main_loop_thread() { + static pthread_t thread{}; + return thread; +} + +/// arch_init() stores the thread before any component runs, so no unset case exists. +__attribute__((always_inline)) inline bool is_main_loop_thread() { + return pthread_equal(pthread_self(), main_loop_thread()) != 0; +} + __attribute__((always_inline)) inline void yield() { ::sched_yield(); } void delay(uint32_t ms); @@ -25,7 +36,7 @@ uint64_t millis_64(); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) uint32_t arch_get_cpu_cycle_count(); -__attribute__((always_inline)) inline void arch_init() {} +__attribute__((always_inline)) inline void arch_init() { main_loop_thread() = pthread_self(); } __attribute__((always_inline)) inline void arch_feed_wdt() {} __attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return 1000000000U; } diff --git a/esphome/components/hrxl_maxsonar_wr/__init__.py b/esphome/components/hrxl_maxsonar_wr/__init__.py index e69de29bb2..2210989d9f 100644 --- a/esphome/components/hrxl_maxsonar_wr/__init__.py +++ b/esphome/components/hrxl_maxsonar_wr/__init__.py @@ -0,0 +1 @@ +DOMAIN = "hrxl_maxsonar_wr" diff --git a/esphome/components/hte501/__init__.py b/esphome/components/hte501/__init__.py index e69de29bb2..18bffe2b22 100644 --- a/esphome/components/hte501/__init__.py +++ b/esphome/components/hte501/__init__.py @@ -0,0 +1 @@ +DOMAIN = "hte501" diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 5faf989686..9235971402 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -30,6 +30,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["network"] AUTO_LOAD = ["json", "watchdog"] +DOMAIN = "http_request" http_request_ns = cg.esphome_ns.namespace("http_request") HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index b7565e676d..deb9c0fec2 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -28,7 +28,7 @@ std::shared_ptr HttpRequestArduino::perform(const char *url, cons const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { - this->status_momentary_error("failed", 1000); + this->status_momentary_error(1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); return nullptr; } @@ -85,7 +85,7 @@ std::shared_ptr HttpRequestArduino::perform(const char *url, cons if (!status) { ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url); container->end(); - this->status_momentary_error("failed", 1000); + this->status_momentary_error(1000); return nullptr; } @@ -143,13 +143,13 @@ std::shared_ptr HttpRequestArduino::perform(const char *url, cons ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url, HTTPClient::errorToString(container->status_code).c_str()); - this->status_momentary_error("failed", 1000); + this->status_momentary_error(1000); container->end(); return nullptr; } if (!is_success(container->status_code)) { ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code); - this->status_momentary_error("failed", 1000); + this->status_momentary_error(1000); // Still return the container, so it can be used to get the status code and error message } diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index c32881dc1f..0130445acd 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -22,7 +22,7 @@ std::shared_ptr HttpRequestHost::perform(const char *url, const c const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { - this->status_momentary_error("failed", 1000); + this->status_momentary_error(1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); return nullptr; } @@ -101,7 +101,7 @@ std::shared_ptr HttpRequestHost::perform(const char *url, const c if (!result) { ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url, (unsigned) result.error()); container->end(); - this->status_momentary_error("failed", 1000); + this->status_momentary_error(1000); return nullptr; } App.feed_wdt(); @@ -109,7 +109,7 @@ std::shared_ptr HttpRequestHost::perform(const char *url, const c container->status_code = response.status; if (!is_success(response.status)) { ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, response.status); - this->status_momentary_error("failed", 1000); + this->status_momentary_error(1000); // Still return the container, so it can be used to get the status code and error message } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 1413b843b7..e429df9b97 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -54,7 +54,7 @@ std::shared_ptr HttpRequestIDF::perform(const char *url, const ch const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { - this->status_momentary_error("failed", ERROR_DURATION_MS); + this->status_momentary_error(ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); return nullptr; } @@ -71,7 +71,7 @@ std::shared_ptr HttpRequestIDF::perform(const char *url, const ch } else if (strcmp(method, "PATCH") == 0) { method_idf = HTTP_METHOD_PATCH; } else { - this->status_momentary_error("failed", ERROR_DURATION_MS); + this->status_momentary_error(ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed; Unsupported method"); return nullptr; } @@ -110,7 +110,7 @@ std::shared_ptr HttpRequestIDF::perform(const char *url, const ch esp_http_client_handle_t client = esp_http_client_init(&config); if (client == nullptr) { - this->status_momentary_error("failed", ERROR_DURATION_MS); + this->status_momentary_error(ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed; client could not be initialized"); return nullptr; } @@ -131,7 +131,7 @@ std::shared_ptr HttpRequestIDF::perform(const char *url, const ch esp_err_t err = esp_http_client_open(client, body_len); if (err != ESP_OK) { - this->status_momentary_error("failed", ERROR_DURATION_MS); + this->status_momentary_error(ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return nullptr; @@ -154,7 +154,7 @@ std::shared_ptr HttpRequestIDF::perform(const char *url, const ch } if (err != ESP_OK) { - this->status_momentary_error("failed", ERROR_DURATION_MS); + this->status_momentary_error(ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return nullptr; @@ -179,7 +179,7 @@ std::shared_ptr HttpRequestIDF::perform(const char *url, const ch err = esp_http_client_set_redirection(client); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err)); - this->status_momentary_error("failed", ERROR_DURATION_MS); + this->status_momentary_error(ERROR_DURATION_MS); esp_http_client_cleanup(client); return nullptr; } @@ -192,7 +192,7 @@ std::shared_ptr HttpRequestIDF::perform(const char *url, const ch err = esp_http_client_open(client, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err)); - this->status_momentary_error("failed", ERROR_DURATION_MS); + this->status_momentary_error(ERROR_DURATION_MS); esp_http_client_cleanup(client); return nullptr; } @@ -220,7 +220,7 @@ std::shared_ptr HttpRequestIDF::perform(const char *url, const ch } ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code); - this->status_momentary_error("failed", ERROR_DURATION_MS); + this->status_momentary_error(ERROR_DURATION_MS); return container; } diff --git a/esphome/components/htu21d/__init__.py b/esphome/components/htu21d/__init__.py index e69de29bb2..10e6e9d66b 100644 --- a/esphome/components/htu21d/__init__.py +++ b/esphome/components/htu21d/__init__.py @@ -0,0 +1 @@ +DOMAIN = "htu21d" diff --git a/esphome/components/htu31d/__init__.py b/esphome/components/htu31d/__init__.py index 039693cb30..bc17a0d886 100644 --- a/esphome/components/htu31d/__init__.py +++ b/esphome/components/htu31d/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@betterengineering"] +DOMAIN = "htu31d" diff --git a/esphome/components/hub75/__init__.py b/esphome/components/hub75/__init__.py index cd5441f749..3ee6a86415 100644 --- a/esphome/components/hub75/__init__.py +++ b/esphome/components/hub75/__init__.py @@ -1,6 +1,7 @@ from esphome.cpp_generator import MockObj CODEOWNERS = ["@stuartparmenter"] +DOMAIN = "hub75" # Use fully-qualified namespace to avoid collision with external hub75 library's global ::hub75 namespace hub75_ns = MockObj("::esphome::hub75", "::") diff --git a/esphome/components/hx711/__init__.py b/esphome/components/hx711/__init__.py index e69de29bb2..f9402be78e 100644 --- a/esphome/components/hx711/__init__.py +++ b/esphome/components/hx711/__init__.py @@ -0,0 +1 @@ +DOMAIN = "hx711" diff --git a/esphome/components/hydreon_rgxx/__init__.py b/esphome/components/hydreon_rgxx/__init__.py index b488bfc1b4..313ce9158c 100644 --- a/esphome/components/hydreon_rgxx/__init__.py +++ b/esphome/components/hydreon_rgxx/__init__.py @@ -3,6 +3,7 @@ from esphome.components import uart CODEOWNERS = ["@functionpointer"] DEPENDENCIES = ["uart"] +DOMAIN = "hydreon_rgxx" hydreon_rgxx_ns = cg.esphome_ns.namespace("hydreon_rgxx") RGModel = hydreon_rgxx_ns.enum("RGModel") diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 0afeb94bf3..36a7b1b351 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -4,6 +4,9 @@ namespace esphome::hydreon_rgxx { ESPHOME_LOG_TAG(TAG, "hydreon_rgxx.sensor"); + +static constexpr uint32_t REBOOT_INTERVAL_ID = 0; + static const int MAX_DATA_LENGTH_BYTES = 80; static const uint8_t ASCII_LF = 0x0A; #define HYDREON_RGXX_COMMA , @@ -138,7 +141,7 @@ void HydreonRGxxComponent::loop() { */ void HydreonRGxxComponent::schedule_reboot_() { this->boot_count_ = 0; - this->set_interval("reboot", 5000, [this]() { + this->set_interval(REBOOT_INTERVAL_ID, 5000, [this]() { if (this->boot_count_ < 0) { ESP_LOGW(TAG, "hydreon_rgxx failed to boot %d times", -this->boot_count_); } @@ -188,7 +191,7 @@ void HydreonRGxxComponent::process_line_() { } else { this->boot_count_++; } - this->cancel_interval("reboot"); + this->cancel_interval(REBOOT_INTERVAL_ID); this->no_response_count_ = 0; ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); diff --git a/esphome/components/hyt271/__init__.py b/esphome/components/hyt271/__init__.py index 2e88d4f366..70be33a00e 100644 --- a/esphome/components/hyt271/__init__.py +++ b/esphome/components/hyt271/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Philippe12"] +DOMAIN = "hyt271" diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp index 72d3940f30..a8374c4115 100644 --- a/esphome/components/hyt271/hyt271.cpp +++ b/esphome/components/hyt271/hyt271.cpp @@ -6,6 +6,8 @@ namespace esphome::hyt271 { ESPHOME_LOG_TAG(TAG, "hyt271"); +static constexpr uint32_t WAIT_CONVERT_TIMEOUT_ID = 0; + static const uint8_t HYT271_ADDRESS = 0x28; void HYT271Component::dump_config() { @@ -23,7 +25,7 @@ void HYT271Component::update() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); return; } - this->set_timeout("wait_convert", 50, [this]() { + this->set_timeout(WAIT_CONVERT_TIMEOUT_ID, 50, [this]() { uint8_t raw_data[4]; if (this->read(raw_data, 4) != i2c::ERROR_OK) { this->status_set_warning(); diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index b053125446..453c2513fa 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -38,7 +38,6 @@ from esphome.const import ( CONF_ADDRESS, CONF_DEVICE, CONF_FREQUENCY, - CONF_I2C, CONF_I2C_ID, CONF_ID, CONF_LOW_POWER_MODE, @@ -58,6 +57,7 @@ from esphome.cpp_generator import MockObj import esphome.final_validate as fv from esphome.types import ConfigType +DOMAIN = "i2c" LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") @@ -232,7 +232,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config: ConfigType) -> None: - full_config = fv.full_config.get()[CONF_I2C] + full_config = fv.full_config.get()[DOMAIN] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") if CORE.is_rp2: diff --git a/esphome/components/i2c_device/__init__.py b/esphome/components/i2c_device/__init__.py index f890fefdb9..a118cb3f17 100644 --- a/esphome/components/i2c_device/__init__.py +++ b/esphome/components/i2c_device/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["i2c"] CODEOWNERS = ["@gabest11"] +DOMAIN = "i2c_device" MULTI_CONF = True i2c_device_ns = cg.esphome_ns.namespace("i2c_device") diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index c5e82beb46..6e4e35590c 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -18,6 +18,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.components.microphone import DOMAIN as MICROPHONE_DOMAIN import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE from esphome.core import CORE @@ -27,6 +28,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] +DOMAIN = "i2s_audio" MULTI_CONF = True CONF_PDM = "pdm" @@ -39,7 +41,6 @@ CONF_I2S_MCLK_PIN = "i2s_mclk_pin" CONF_I2S_BCLK_PIN = "i2s_bclk_pin" CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" -CONF_I2S_AUDIO = "i2s_audio" CONF_I2S_AUDIO_ID = "i2s_audio_id" CONF_I2S_MODE = "i2s_mode" @@ -218,9 +219,9 @@ class I2SAudioData: def _get_data() -> I2SAudioData: - if CONF_I2S_AUDIO not in CORE.data: - CORE.data[CONF_I2S_AUDIO] = I2SAudioData() - return CORE.data[CONF_I2S_AUDIO] + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = I2SAudioData() + return CORE.data[DOMAIN] def _assign_ports() -> None: @@ -234,12 +235,12 @@ def _assign_ports() -> None: return full_config = fv.full_config.get() - i2s_configs = full_config[CONF_I2S_AUDIO] + i2s_configs = full_config[DOMAIN] # Find i2s_audio instances with microphones that require port 0 # (PDM and internal ADC only work on I2S port 0) port0_parent_id = None - for mic_config in full_config.get("microphone", []): + for mic_config in full_config.get(MICROPHONE_DOMAIN, []): if CONF_I2S_AUDIO_ID not in mic_config: continue if mic_config.get(CONF_PDM) or mic_config.get(CONF_ADC_TYPE) == "internal": @@ -262,7 +263,7 @@ def _assign_ports() -> None: def _final_validate(_: ConfigType) -> None: - i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] + i2s_audio_configs = fv.full_config.get()[DOMAIN] variant = get_esp32_variant() if variant not in I2S_PORTS: raise cv.Invalid(f"Unsupported variant {variant}") diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 79367d67e0..1345f0187b 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -397,7 +397,7 @@ void I2SAudioMicrophone::loop() { if (!this->start_driver_()) { ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); - this->status_momentary_error("driver_fail", 1000); + this->status_momentary_error(1000); this->stop_driver_(); // Stop/frees whatever possibly started break; } @@ -408,7 +408,7 @@ void I2SAudioMicrophone::loop() { if (this->task_handle_ == nullptr) { ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); - this->status_momentary_error("task_fail", 1000); + this->status_momentary_error(1000); this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } } diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index a57ef7f55c..8d586ea4e0 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -140,7 +140,7 @@ void I2SAudioSpeakerBase::loop() { if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) { ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); - this->status_momentary_error("driver-failure", 1000); + this->status_momentary_error(1000); break; } @@ -153,7 +153,7 @@ void I2SAudioSpeakerBase::loop() { if (this->speaker_task_handle_ == nullptr) { ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); - this->status_momentary_error("task-failure", 1000); + this->status_momentary_error(1000); this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } break; diff --git a/esphome/components/iaqcore/__init__.py b/esphome/components/iaqcore/__init__.py index e69de29bb2..216921250f 100644 --- a/esphome/components/iaqcore/__init__.py +++ b/esphome/components/iaqcore/__init__.py @@ -0,0 +1 @@ +DOMAIN = "iaqcore" diff --git a/esphome/components/icnt86/__init__.py b/esphome/components/icnt86/__init__.py index 07f3b4e31c..7736bfd393 100644 --- a/esphome/components/icnt86/__init__.py +++ b/esphome/components/icnt86/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@danepowell"] +DOMAIN = "icnt86" diff --git a/esphome/components/ili9341/__init__.py b/esphome/components/ili9341/__init__.py index e69de29bb2..95ff0c3056 100644 --- a/esphome/components/ili9341/__init__.py +++ b/esphome/components/ili9341/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ili9341" diff --git a/esphome/components/ili9xxx/__init__.py b/esphome/components/ili9xxx/__init__.py index 84888bbabc..2803b43915 100644 --- a/esphome/components/ili9xxx/__init__.py +++ b/esphome/components/ili9xxx/__init__.py @@ -1,3 +1,5 @@ +DOMAIN = "ili9xxx" + DEPRECATED_COMPONENT = """ The 'ili9xxx' component is deprecated and no new models will be added to it. New model PRs should target the newer and more performant 'mipi_spi' component. diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py index 9b57b6561f..1acc5e6ca0 100644 --- a/esphome/components/improv_base/__init__.py +++ b/esphome/components/improv_base/__init__.py @@ -8,6 +8,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "improv_base" CONF_NEXT_URL = "next_url" diff --git a/esphome/components/improv_ble/__init__.py b/esphome/components/improv_ble/__init__.py index 72ac586628..9dfc700f74 100644 --- a/esphome/components/improv_ble/__init__.py +++ b/esphome/components/improv_ble/__init__.py @@ -2,13 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import binary_sensor, improv_base, output import esphome.config_validation as cv -from esphome.const import ( - CONF_ID, - CONF_ON_START, - CONF_ON_STATE, - CONF_TRIGGER_ID, - PLATFORM_ESP32, -) +from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, PLATFORM_ESP32 from esphome.core import CORE from esphome.types import ConfigType @@ -31,6 +25,7 @@ def AUTO_LOAD() -> list[str]: CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["wifi"] +DOMAIN = "improv_ble" # Legacy top-level YAML key that routes here; esphome/loader.py and # esphome/config.py handle the warning and the key rename. @@ -58,20 +53,35 @@ State = improv_ns.enum("State") improv_ble_ns = cg.esphome_ns.namespace("improv_ble") ImprovBLEComponent = improv_ble_ns.class_("ImprovBLEComponent", cg.Component) -ImprovBLEProvisionedTrigger = improv_ble_ns.class_( - "ImprovBLEProvisionedTrigger", automation.Trigger.template() -) -ImprovBLEProvisioningTrigger = improv_ble_ns.class_( - "ImprovBLEProvisioningTrigger", automation.Trigger.template() -) -ImprovBLEStartTrigger = improv_ble_ns.class_( - "ImprovBLEStartTrigger", automation.Trigger.template() -) -ImprovBLEStateTrigger = improv_ble_ns.class_( - "ImprovBLEStateTrigger", automation.Trigger.template() -) -ImprovBLEStoppedTrigger = improv_ble_ns.class_( - "ImprovBLEStoppedTrigger", automation.Trigger.template() + +_STATE_PARAMS = [(State, "state"), (Error, "error")] +_NOT_FAILED = "!{parent}->is_failed()" + + +def _state_automation( + conf_key: str, args: list, states: str | None +) -> automation.CallbackAutomation: + # Every automation hangs off the state callback and is silent while the component has failed. + return automation.CallbackAutomation( + conf_key, + "add_on_state_callback", + args, + params=_STATE_PARAMS, + forward=[name for _, name in args], + when=_NOT_FAILED if states is None else f"({states}) && {_NOT_FAILED}", + ) + + +_CALLBACK_AUTOMATIONS = ( + _state_automation(CONF_ON_PROVISIONED, [], "state == improv::STATE_PROVISIONED"), + _state_automation(CONF_ON_PROVISIONING, [], "state == improv::STATE_PROVISIONING"), + _state_automation( + CONF_ON_START, + [], + "state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION", + ), + _state_automation(CONF_ON_STATE, _STATE_PARAMS, None), + _state_automation(CONF_ON_STOP, [], "state == improv::STATE_STOPPED"), ) @@ -92,41 +102,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ImprovBLEProvisionedTrigger - ), - } - ), - cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ImprovBLEProvisioningTrigger - ), - } - ), - cv.Optional(CONF_ON_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ImprovBLEStartTrigger - ), - } - ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ImprovBLEStateTrigger - ), - } - ), - cv.Optional(CONF_ON_STOP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ImprovBLEStoppedTrigger - ), - } - ), + cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation({}), + cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation({}), + cv.Optional(CONF_ON_START): automation.validate_automation({}), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_STOP): automation.validate_automation({}), } ) .extend(improv_base.IMPROV_SCHEMA) @@ -163,28 +143,6 @@ async def to_code(config: ConfigType) -> None: status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR]) cg.add(var.set_status_indicator(status_indicator)) - use_state_callback = False - for conf in config.get(CONF_ON_PROVISIONED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - use_state_callback = True - for conf in config.get(CONF_ON_PROVISIONING, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - use_state_callback = True - for conf in config.get(CONF_ON_START, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - use_state_callback = True - for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(State, "state"), (Error, "error")], conf - ) - use_state_callback = True - for conf in config.get(CONF_ON_STOP, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - use_state_callback = True - if use_state_callback: + if any(config.get(entry.conf_key) for entry in _CALLBACK_AUTOMATIONS): cg.add_define("USE_IMPROV_BLE_STATE_CALLBACK") + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/improv_ble/automation.h b/esphome/components/improv_ble/automation.h deleted file mode 100644 index 223a129238..0000000000 --- a/esphome/components/improv_ble/automation.h +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once -#ifdef USE_ESP32 -#ifdef USE_IMPROV_BLE_STATE_CALLBACK -#include "improv_ble_component.h" - -#include "esphome/core/automation.h" - -#include - -namespace esphome::improv_ble { - -class ImprovBLEProvisionedTrigger final : public Trigger<> { - public: - explicit ImprovBLEProvisionedTrigger(ImprovBLEComponent *parent) : parent_(parent) { - parent->add_on_state_callback([this](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) { - this->trigger(); - } - }); - } - - protected: - ImprovBLEComponent *parent_; -}; - -class ImprovBLEProvisioningTrigger final : public Trigger<> { - public: - explicit ImprovBLEProvisioningTrigger(ImprovBLEComponent *parent) : parent_(parent) { - parent->add_on_state_callback([this](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) { - this->trigger(); - } - }); - } - - protected: - ImprovBLEComponent *parent_; -}; - -class ImprovBLEStartTrigger final : public Trigger<> { - public: - explicit ImprovBLEStartTrigger(ImprovBLEComponent *parent) : parent_(parent) { - parent->add_on_state_callback([this](improv::State state, improv::Error error) { - if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && - !this->parent_->is_failed()) { - this->trigger(); - } - }); - } - - protected: - ImprovBLEComponent *parent_; -}; - -class ImprovBLEStateTrigger final : public Trigger { - public: - explicit ImprovBLEStateTrigger(ImprovBLEComponent *parent) : parent_(parent) { - parent->add_on_state_callback([this](improv::State state, improv::Error error) { - if (!this->parent_->is_failed()) { - this->trigger(state, error); - } - }); - } - - protected: - ImprovBLEComponent *parent_; -}; - -class ImprovBLEStoppedTrigger final : public Trigger<> { - public: - explicit ImprovBLEStoppedTrigger(ImprovBLEComponent *parent) : parent_(parent) { - parent->add_on_state_callback([this](improv::State state, improv::Error error) { - if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) { - this->trigger(); - } - }); - } - - protected: - ImprovBLEComponent *parent_; -}; - -} // namespace esphome::improv_ble - -#endif -#endif diff --git a/esphome/components/improv_ble/improv_ble_component.cpp b/esphome/components/improv_ble/improv_ble_component.cpp index 15adc49776..2029058899 100644 --- a/esphome/components/improv_ble/improv_ble_component.cpp +++ b/esphome/components/improv_ble/improv_ble_component.cpp @@ -21,6 +21,10 @@ namespace esphome::improv_ble { using namespace bytebuffer; ESPHOME_LOG_TAG(TAG, "improv_ble.component"); + +static constexpr uint32_t STOP_SERVICE_TIMEOUT_ID = 0; +static constexpr uint32_t WIFI_CONNECT_TIMEOUT_ID = 1; + static constexpr size_t IMPROV_MAX_LOG_BYTES = 128; static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; // command + data length + trailing byte @@ -322,7 +326,7 @@ void ImprovBLEComponent::stop() { // Wait before stopping the service to ensure all BLE clients see the state change. // This prevents clients from repeatedly reconnecting and wasting resources by allowing // them to observe that the device is provisioned before the service disappears. - this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] { + this->set_timeout(STOP_SERVICE_TIMEOUT_ID, STOP_ADVERTISING_DELAY, [this] { if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr) return; // Release first so removing the service UUID does not restart advertising on the way out @@ -398,7 +402,7 @@ void ImprovBLEComponent::process_incoming_data_() { ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); + this->set_timeout(WIFI_CONNECT_TIMEOUT_ID, 30000, [this]() { this->on_wifi_connect_timeout_(); }); this->incoming_data_.clear(); break; } @@ -438,7 +442,7 @@ void ImprovBLEComponent::check_wifi_connection_() { if (this->state_ == improv::STATE_PROVISIONING) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); this->connecting_sta_ = {}; - this->cancel_timeout("wifi-connect-timeout"); + this->cancel_timeout(WIFI_CONNECT_TIMEOUT_ID); // Build the URL list directly into a stack buffer with no heap allocation std::array buf; diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 0231791e9b..3e1aef21bb 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,15 +1,9 @@ import esphome.codegen as cg from esphome.components import improv_base, uart from esphome.components.esp32 import VARIANT_ESP32S3, get_esp32_variant -from esphome.components.logger import USB_CDC +from esphome.components.logger import DOMAIN as LOGGER_DOMAIN, USB_CDC import esphome.config_validation as cv -from esphome.const import ( - CONF_BAUD_RATE, - CONF_HARDWARE_UART, - CONF_ID, - CONF_LOGGER, - CONF_UART_ID, -) +from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_UART_ID from esphome.core import CORE import esphome.final_validate as fv from esphome.types import ConfigType @@ -17,6 +11,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["improv_base"] CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["logger", "network"] +DOMAIN = "improv_serial" improv_serial_ns = cg.esphome_ns.namespace("improv_serial") @@ -52,7 +47,7 @@ def validate_transport(config: ConfigType) -> None: # The host logger has no serial port for Improv to share if CORE.is_host: raise cv.Invalid("improv_serial on the host platform requires uart_id") - logger_conf = fv.full_config.get()[CONF_LOGGER] + logger_conf = fv.full_config.get()[LOGGER_DOMAIN] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") if CORE.is_esp32 and ( diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index ecb7bc98fb..1045467866 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -18,6 +18,8 @@ namespace esphome::improv_serial { ESPHOME_LOG_TAG(TAG, "improv_serial"); +static constexpr uint32_t WIFI_CONNECT_TIMEOUT_ID = 0; + void ImprovSerialComponent::setup() { global_improv_serial_component = this; #ifdef USE_IMPROV_SERIAL_UART @@ -73,7 +75,7 @@ void ImprovSerialComponent::loop() { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); this->connecting_sta_ = {}; - this->cancel_timeout("wifi-connect-timeout"); + this->cancel_timeout(WIFI_CONNECT_TIMEOUT_ID); this->set_state_(improv::STATE_PROVISIONED); this->send_settings_response_(improv::WIFI_SETTINGS); @@ -302,7 +304,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - this->set_timeout("wifi-connect-timeout", switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS, + this->set_timeout(WIFI_CONNECT_TIMEOUT_ID, switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS, [this]() { this->on_wifi_connect_timeout_(); }); #else // No Wi-Fi support compiled in; there is nothing to provision. diff --git a/esphome/components/ina219/__init__.py b/esphome/components/ina219/__init__.py index e69de29bb2..5e9d2cef17 100644 --- a/esphome/components/ina219/__init__.py +++ b/esphome/components/ina219/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ina219" diff --git a/esphome/components/ina226/__init__.py b/esphome/components/ina226/__init__.py index ca1f2caa31..9998623c82 100644 --- a/esphome/components/ina226/__init__.py +++ b/esphome/components/ina226/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Sergio303", "@latonita"] +DOMAIN = "ina226" diff --git a/esphome/components/ina260/__init__.py b/esphome/components/ina260/__init__.py index e69de29bb2..4728ef80f8 100644 --- a/esphome/components/ina260/__init__.py +++ b/esphome/components/ina260/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ina260" diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index b6cf0e38f3..7fdfd90bb6 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -34,6 +34,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@latonita"] +DOMAIN = "ina2xx_base" CONF_ADC_AVERAGING = "adc_averaging" CONF_ADC_RANGE = "adc_range" diff --git a/esphome/components/ina2xx_i2c/__init__.py b/esphome/components/ina2xx_i2c/__init__.py index e69de29bb2..d6c784bad7 100644 --- a/esphome/components/ina2xx_i2c/__init__.py +++ b/esphome/components/ina2xx_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ina2xx_i2c" diff --git a/esphome/components/ina2xx_spi/__init__.py b/esphome/components/ina2xx_spi/__init__.py index e69de29bb2..ae72c2d195 100644 --- a/esphome/components/ina2xx_spi/__init__.py +++ b/esphome/components/ina2xx_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ina2xx_spi" diff --git a/esphome/components/ina3221/__init__.py b/esphome/components/ina3221/__init__.py index e69de29bb2..f8f491ddec 100644 --- a/esphome/components/ina3221/__init__.py +++ b/esphome/components/ina3221/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ina3221" diff --git a/esphome/components/infrared/__init__.py b/esphome/components/infrared/__init__.py index 47ca5bf07f..2d3d478e58 100644 --- a/esphome/components/infrared/__init__.py +++ b/esphome/components/infrared/__init__.py @@ -19,6 +19,7 @@ from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@kbx81"] AUTO_LOAD = ["ir_rf_base"] +DOMAIN = "infrared" IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/inkbird_ibsth1_mini/__init__.py b/esphome/components/inkbird_ibsth1_mini/__init__.py index e69de29bb2..e0fbed17ca 100644 --- a/esphome/components/inkbird_ibsth1_mini/__init__.py +++ b/esphome/components/inkbird_ibsth1_mini/__init__.py @@ -0,0 +1 @@ +DOMAIN = "inkbird_ibsth1_mini" diff --git a/esphome/components/inkplate/__init__.py b/esphome/components/inkplate/__init__.py index 1c6013793a..36740332a5 100644 --- a/esphome/components/inkplate/__init__.py +++ b/esphome/components/inkplate/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@jesserockz", "@JosipKuci"] +DOMAIN = "inkplate" diff --git a/esphome/components/inkplate6/__init__.py b/esphome/components/inkplate6/__init__.py index e69de29bb2..2b198f666b 100644 --- a/esphome/components/inkplate6/__init__.py +++ b/esphome/components/inkplate6/__init__.py @@ -0,0 +1 @@ +DOMAIN = "inkplate6" diff --git a/esphome/components/integration/__init__.py b/esphome/components/integration/__init__.py index 71a87b6ae5..4afc7ce9e1 100644 --- a/esphome/components/integration/__init__.py +++ b/esphome/components/integration/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@OttoWinter"] +DOMAIN = "integration" diff --git a/esphome/components/internal_temperature/__init__.py b/esphome/components/internal_temperature/__init__.py index 9433ade13f..aa1671a015 100644 --- a/esphome/components/internal_temperature/__init__.py +++ b/esphome/components/internal_temperature/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Mat931"] +DOMAIN = "internal_temperature" diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py index 11c3e15b0d..adbe4349c6 100644 --- a/esphome/components/interval/__init__.py +++ b/esphome/components/interval/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_INTERVAL, CONF_STARTUP_DELAY from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "interval" interval_ns = cg.esphome_ns.namespace("interval") IntervalTrigger = interval_ns.class_( "IntervalTrigger", automation.Trigger.template(), cg.PollingComponent diff --git a/esphome/components/ir_rf_base/__init__.py b/esphome/components/ir_rf_base/__init__.py index 55536c131b..7a4cde3677 100644 --- a/esphome/components/ir_rf_base/__init__.py +++ b/esphome/components/ir_rf_base/__init__.py @@ -2,13 +2,14 @@ import esphome.codegen as cg from esphome.components import remote_base -from esphome.const import CONF_API +from esphome.components.api import DOMAIN as API_DOMAIN from esphome.core import CORE from esphome.core.entity_helpers import queue_entity_register from esphome.types import ConfigType CODEOWNERS = ["@kbx81", "@bdraco"] AUTO_LOAD = ["remote_base"] +DOMAIN = "ir_rf_base" ir_rf_base_ns = cg.esphome_ns.namespace("ir_rf_base") IrRfEntity = ir_rf_base_ns.class_("IrRfEntity", cg.EntityBase, cg.Component) @@ -24,7 +25,7 @@ async def attach_transmitter(var: cg.MockObj, config: ConfigType, key: str) -> N soon as the frame is handed over. """ await remote_base.register_transmittable(var, config, key) - if CONF_API in CORE.config: + if API_DOMAIN in CORE.config: cg.add_define("USE_IR_RF_TRANSMIT_COMPLETE") diff --git a/esphome/components/ir_rf_proxy/__init__.py b/esphome/components/ir_rf_proxy/__init__.py index bc4079ede7..45bce4ac6f 100644 --- a/esphome/components/ir_rf_proxy/__init__.py +++ b/esphome/components/ir_rf_proxy/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg CODEOWNERS = ["@kbx81"] +DOMAIN = "ir_rf_proxy" # Namespace and constants exported for infrared.py platform ir_rf_proxy_ns = cg.esphome_ns.namespace("ir_rf_proxy") diff --git a/esphome/components/it8951/__init__.py b/esphome/components/it8951/__init__.py index 7fc4ae2cd0..94bdf451d3 100644 --- a/esphome/components/it8951/__init__.py +++ b/esphome/components/it8951/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Passific", "@koosoli", "@limengdu"] +DOMAIN = "it8951" diff --git a/esphome/components/jsn_sr04t/__init__.py b/esphome/components/jsn_sr04t/__init__.py index ef6335f316..672487d32a 100644 --- a/esphome/components/jsn_sr04t/__init__.py +++ b/esphome/components/jsn_sr04t/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Mafus1"] +DOMAIN = "jsn_sr04t" diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index 618c56faa1..cac06a29d2 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -4,6 +4,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "json" json_ns = cg.esphome_ns.namespace("json") CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/kalman_combinator/__init__.py b/esphome/components/kalman_combinator/__init__.py index e69de29bb2..35782ff625 100644 --- a/esphome/components/kalman_combinator/__init__.py +++ b/esphome/components/kalman_combinator/__init__.py @@ -0,0 +1 @@ +DOMAIN = "kalman_combinator" diff --git a/esphome/components/kamstrup_kmp/__init__.py b/esphome/components/kamstrup_kmp/__init__.py index e69de29bb2..2446761ce9 100644 --- a/esphome/components/kamstrup_kmp/__init__.py +++ b/esphome/components/kamstrup_kmp/__init__.py @@ -0,0 +1 @@ +DOMAIN = "kamstrup_kmp" diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index dedd960c36..8dafb5da07 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -19,6 +19,7 @@ from esphome.cpp_generator import MockObj, literal from esphome.types import ConfigType, TemplateArgsType CODEOWNERS = ["@ssieb"] +DOMAIN = "key_collector" MULTI_CONF = True diff --git a/esphome/components/key_provider/__init__.py b/esphome/components/key_provider/__init__.py index f397382ff2..3008545f40 100644 --- a/esphome/components/key_provider/__init__.py +++ b/esphome/components/key_provider/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg CODEOWNERS = ["@ssieb"] +DOMAIN = "key_provider" key_provider_ns = cg.esphome_ns.namespace("key_provider") KeyProvider = key_provider_ns.class_("KeyProvider") diff --git a/esphome/components/kmeteriso/__init__.py b/esphome/components/kmeteriso/__init__.py index e69de29bb2..3316358096 100644 --- a/esphome/components/kmeteriso/__init__.py +++ b/esphome/components/kmeteriso/__init__.py @@ -0,0 +1 @@ +DOMAIN = "kmeteriso" diff --git a/esphome/components/kuntze/__init__.py b/esphome/components/kuntze/__init__.py index e69de29bb2..34a9637a09 100644 --- a/esphome/components/kuntze/__init__.py +++ b/esphome/components/kuntze/__init__.py @@ -0,0 +1 @@ +DOMAIN = "kuntze" diff --git a/esphome/components/lc709203f/__init__.py b/esphome/components/lc709203f/__init__.py index 3be68d174f..3076e38527 100644 --- a/esphome/components/lc709203f/__init__.py +++ b/esphome/components/lc709203f/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ilikecake"] +DOMAIN = "lc709203f" diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index 08ec395720..58efbd4884 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -7,6 +7,8 @@ from esphome.const import CONF_DATA, CONF_DIMENSIONS, CONF_POSITION from esphome.cpp_generator import MockObj from esphome.types import ConfigType +DOMAIN = "lcd_base" + CONF_USER_CHARACTERS = "user_characters" lcd_base_ns = cg.esphome_ns.namespace("lcd_base") diff --git a/esphome/components/lcd_gpio/__init__.py b/esphome/components/lcd_gpio/__init__.py index e69de29bb2..9df8039c3b 100644 --- a/esphome/components/lcd_gpio/__init__.py +++ b/esphome/components/lcd_gpio/__init__.py @@ -0,0 +1 @@ +DOMAIN = "lcd_gpio" diff --git a/esphome/components/lcd_menu/__init__.py b/esphome/components/lcd_menu/__init__.py index 88b8ac21d4..483a36491d 100644 --- a/esphome/components/lcd_menu/__init__.py +++ b/esphome/components/lcd_menu/__init__.py @@ -11,6 +11,7 @@ from esphome.core.entity_helpers import inherit_property_from from esphome.types import ConfigType CODEOWNERS = ["@numo68"] +DOMAIN = "lcd_menu" AUTO_LOAD = ["display_menu_base"] diff --git a/esphome/components/lcd_pcf8574/__init__.py b/esphome/components/lcd_pcf8574/__init__.py index e69de29bb2..cfeba4c863 100644 --- a/esphome/components/lcd_pcf8574/__init__.py +++ b/esphome/components/lcd_pcf8574/__init__.py @@ -0,0 +1 @@ +DOMAIN = "lcd_pcf8574" diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index d3179d9b30..e2728aa73c 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -9,6 +9,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] CODEOWNERS = ["@sebcaps", "@regevbr"] +DOMAIN = "ld2410" MULTI_CONF = True ld2410_ns = cg.esphome_ns.namespace("ld2410") diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index 5b523d0944..a8636f3df6 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -34,83 +34,35 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), @@ -123,29 +75,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), diff --git a/esphome/components/ld2412/__init__.py b/esphome/components/ld2412/__init__.py index 82db319861..fd63f78c3d 100644 --- a/esphome/components/ld2412/__init__.py +++ b/esphome/components/ld2412/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] CODEOWNERS = ["@Rihan9"] DEPENDENCIES = ["uart"] +DOMAIN = "ld2412" MULTI_CONF = True LD2412_ns = cg.esphome_ns.namespace("ld2412") diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index e236a7d5f3..f350fb7050 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -34,84 +34,36 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_LIGHTBULB, unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor ), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), @@ -124,29 +76,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), diff --git a/esphome/components/ld2420/__init__.py b/esphome/components/ld2420/__init__.py index 5a5aabeba0..87a54d278a 100644 --- a/esphome/components/ld2420/__init__.py +++ b/esphome/components/ld2420/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@descipher"] +DOMAIN = "ld2420" DEPENDENCIES = ["uart"] diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 4c37f4fcd1..a54e83f287 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -8,6 +8,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] CODEOWNERS = ["@hareeshmu"] +DOMAIN = "ld2450" MULTI_CONF = True ld2450_ns = cg.esphome_ns.namespace("ld2450") diff --git a/esphome/components/ld2450/sensor.py b/esphome/components/ld2450/sensor.py index e714d353d1..6f2056330b 100644 --- a/esphome/components/ld2450/sensor.py +++ b/esphome/components/ld2450/sensor.py @@ -46,41 +46,17 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( accuracy_decimals=0, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_ACCOUNT_GROUP, ), cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( accuracy_decimals=0, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_HUMAN_GREETING_PROXIMITY, ), cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( accuracy_decimals=0, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_ACCOUNT_SWITCH, ), } @@ -92,85 +68,37 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { cv.Optional(CONF_X): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_ALPHA_X_BOX_OUTLINE, unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_Y): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_ALPHA_Y_BOX_OUTLINE, unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( device_class=DEVICE_CLASS_SPEED, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_SPEEDOMETER_SLOW, unit_of_measurement=UNIT_MILLIMETER_PER_SECOND, ), cv.Optional(CONF_ANGLE): sensor.sensor_schema( - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP, unit_of_measurement=UNIT_DEGREES, accuracy_decimals=1, ), cv.Optional(CONF_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_MAP_MARKER_DISTANCE, unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_RESOLUTION): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE, unit_of_measurement=UNIT_MILLIMETER, ), @@ -183,41 +111,17 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( accuracy_decimals=0, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_MAP_MARKER_ACCOUNT, ), cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( accuracy_decimals=0, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_MAP_MARKER_ACCOUNT, ), cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( accuracy_decimals=0, - filters=[ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, - ], + filters=sensor.TIMEOUT_THROTTLE_FILTERS, icon=ICON_MAP_MARKER_ACCOUNT, ), } diff --git a/esphome/components/ld24xx/__init__.py b/esphome/components/ld24xx/__init__.py index 516af84856..97ff42626c 100644 --- a/esphome/components/ld24xx/__init__.py +++ b/esphome/components/ld24xx/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@kbx81"] +DOMAIN = "ld24xx" diff --git a/esphome/components/ld6002b/__init__.py b/esphome/components/ld6002b/__init__.py index af1e501a6a..c7529f9314 100644 --- a/esphome/components/ld6002b/__init__.py +++ b/esphome/components/ld6002b/__init__.py @@ -9,6 +9,7 @@ from .const import CONF_AUTO_WAKE, CONF_WAKEUP_PULSE CODEOWNERS = ["@hepter"] DEPENDENCIES = ["uart"] +DOMAIN = "ld6002b" MULTI_CONF = True ld6002b_ns = cg.esphome_ns.namespace("ld6002b") diff --git a/esphome/components/ld6002b/button/__init__.py b/esphome/components/ld6002b/button/__init__.py index a664890a86..a33e706eb6 100644 --- a/esphome/components/ld6002b/button/__init__.py +++ b/esphome/components/ld6002b/button/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import button +from esphome.components.select import DOMAIN as SELECT_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_AREA_ID, @@ -91,7 +92,7 @@ def final_validate(config: ConfigType) -> None: if config.get(CONF_APPLY_AREA): has_area_id_select = any( entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_AREA_ID) is not None - for entry in full_config.get("select", []) + for entry in full_config.get(SELECT_DOMAIN, []) ) if not has_area_id_select: raise cv.Invalid( diff --git a/esphome/components/ld6002b/ld6002b.cpp b/esphome/components/ld6002b/ld6002b.cpp index fdf1b15d06..93a2ea341f 100644 --- a/esphome/components/ld6002b/ld6002b.cpp +++ b/esphome/components/ld6002b/ld6002b.cpp @@ -620,7 +620,7 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_ this->process_command_queue_(); if (refresh_areas) { this->area_write_in_flight_ = false; - this->set_timeout(AREA_REFRESH_TIMEOUT, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); }); + this->set_timeout(AREA_REFRESH_TIMEOUT_ID, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); }); } return; } @@ -1215,7 +1215,7 @@ void LD6002BComponent::send_command_internal_(uint16_t type, const uint8_t *data std::memcpy(this->wake_scratch_.data(), data, len); } // A button pulse must not raise the pin in the middle of this one. - this->cancel_timeout(WAKE_BUTTON_TIMEOUT); + this->cancel_timeout(WAKE_BUTTON_TIMEOUT_ID); this->wake_pulse_pending_ = true; this->wakeup_pin_->digital_write(false); const uint8_t generation = this->send_generation_; @@ -1364,7 +1364,8 @@ void LD6002BComponent::wake_() { if (this->wakeup_pin_ == nullptr || this->wake_pulse_pending_) return; this->wakeup_pin_->digital_write(false); - this->set_timeout(WAKE_BUTTON_TIMEOUT, this->wakeup_pulse_ms_, [this]() { this->wakeup_pin_->digital_write(true); }); + this->set_timeout(WAKE_BUTTON_TIMEOUT_ID, this->wakeup_pulse_ms_, + [this]() { this->wakeup_pin_->digital_write(true); }); } void LD6002BComponent::set_number_value(NumberType type, float value) { diff --git a/esphome/components/ld6002b/ld6002b.h b/esphome/components/ld6002b/ld6002b.h index bea3804312..c52f770d6e 100644 --- a/esphome/components/ld6002b/ld6002b.h +++ b/esphome/components/ld6002b/ld6002b.h @@ -413,12 +413,12 @@ class LD6002BComponent : public Component, public uart::UARTDevice { // How long the module stays awake after any frame, and so still answers the next one. static constexpr uint32_t MODULE_AWAKE_MS = 10000; static constexpr uint8_t CMD_MAX_RETRIES = 3; - // Named so a repeated press replaces its own pending timeout instead of stacking + // Keyed so a repeated press replaces its own pending timeout instead of stacking // another, and so the command path can cancel it when it takes the pin over. - static constexpr const char *WAKE_BUTTON_TIMEOUT = "wake_button"; - // Named so a burst of writes collapses to one read once they settle, rather than + static constexpr uint32_t WAKE_BUTTON_TIMEOUT_ID = 0; + // Keyed so a burst of writes collapses to one read once they settle, rather than // one read per write. - static constexpr const char *AREA_REFRESH_TIMEOUT = "area_refresh"; + static constexpr uint32_t AREA_REFRESH_TIMEOUT_ID = 1; // A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms. static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000; diff --git a/esphome/components/ld6002b/number/__init__.py b/esphome/components/ld6002b/number/__init__.py index 236b049f53..de0ff57a64 100644 --- a/esphome/components/ld6002b/number/__init__.py +++ b/esphome/components/ld6002b/number/__init__.py @@ -1,9 +1,10 @@ import esphome.codegen as cg from esphome.components import number +from esphome.components.button import DOMAIN as BUTTON_DOMAIN +from esphome.components.select import DOMAIN as SELECT_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_AREA_ID, - CONF_BUTTON, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_DURATION, ENTITY_CATEGORY_CONFIG, @@ -114,7 +115,7 @@ def final_validate(config: ConfigType) -> None: has_apply_area = any( entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_APPLY_AREA) is not None - for entry in full_config.get(CONF_BUTTON, []) + for entry in full_config.get(BUTTON_DOMAIN, []) ) if not has_apply_area: raise cv.Invalid( @@ -124,7 +125,7 @@ def final_validate(config: ConfigType) -> None: has_area_id_select = any( entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_AREA_ID) is not None - for entry in full_config.get("select", []) + for entry in full_config.get(SELECT_DOMAIN, []) ) if not has_area_id_select: raise cv.Invalid( diff --git a/esphome/components/ld6002b/sensor.py b/esphome/components/ld6002b/sensor.py index 5653f8b944..d2285187f6 100644 --- a/esphome/components/ld6002b/sensor.py +++ b/esphome/components/ld6002b/sensor.py @@ -30,44 +30,32 @@ from .const import ( DEPENDENCIES = ["ld6002b"] -# The ld2450 defaults for a streamed value: hold the last reading for a second so a -# dropped frame does not read as absence, then rate-limit what reaches the frontend. -_VALUE_SENSOR_FILTERS = [ - { - "timeout": { - "timeout": cv.TimePeriod(milliseconds=1000), - "value": "last", - } - }, - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, -] - TARGET_SCHEMA = cv.Schema( { cv.Optional(CONF_X): sensor.sensor_schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, device_class=DEVICE_CLASS_DISTANCE, - filters=_VALUE_SENSOR_FILTERS, + filters=sensor.TIMEOUT_THROTTLE_FILTERS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_Y): sensor.sensor_schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, device_class=DEVICE_CLASS_DISTANCE, - filters=_VALUE_SENSOR_FILTERS, + filters=sensor.TIMEOUT_THROTTLE_FILTERS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_Z): sensor.sensor_schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, device_class=DEVICE_CLASS_DISTANCE, - filters=_VALUE_SENSOR_FILTERS, + filters=sensor.TIMEOUT_THROTTLE_FILTERS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DOPPLER_INDEX): sensor.sensor_schema( accuracy_decimals=0, - filters=_VALUE_SENSOR_FILTERS, + filters=sensor.TIMEOUT_THROTTLE_FILTERS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CLUSTER_ID): sensor.sensor_schema( diff --git a/esphome/components/ledc/__init__.py b/esphome/components/ledc/__init__.py index 71a87b6ae5..cc2c4d56aa 100644 --- a/esphome/components/ledc/__init__.py +++ b/esphome/components/ledc/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@OttoWinter"] +DOMAIN = "ledc" diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 5d630fec6c..61eafe5f86 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -57,6 +57,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["preferences"] +DOMAIN = "libretiny" IS_TARGET_PLATFORM = True # BLE 5.x BK SDK options to disable unused features. @@ -491,6 +492,9 @@ async def component_to_code(config): # Not enabled on RTL87xx/LN882x — costs more heap than it saves there. cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1") + # Static destructors never run, so skip registering them. See atexit_stubs.cpp. + cg.add_build_flag("-Wl,--wrap=__cxa_atexit") + # LN882x: a zero-size allocation must not trip the SDK's assert (see ln882x_zero_malloc.c). if config[CONF_COMPONENT_ID] == COMPONENT_LN882X: cg.add_build_flag("-Wl,--wrap=pvPortMalloc") diff --git a/esphome/components/libretiny/atexit_stubs.cpp b/esphome/components/libretiny/atexit_stubs.cpp new file mode 100644 index 0000000000..1d237e7f05 --- /dev/null +++ b/esphome/components/libretiny/atexit_stubs.cpp @@ -0,0 +1,20 @@ +/* + * Linker wrap stub for __cxa_atexit(). + * + * GCC registers the destructor of every global and function local static + * object with __cxa_atexit(). The firmware never returns from main or calls + * exit(), so these destructors never run, but the first registration links + * newlib's exit handler tables. + */ + +#include "esphome/core/defines.h" + +#ifdef USE_LIBRETINY + +namespace esphome::libretiny {} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; } +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/gpio_arduino.cpp b/esphome/components/libretiny/gpio_arduino.cpp index ff333aacef..9884c37b85 100644 --- a/esphome/components/libretiny/gpio_arduino.cpp +++ b/esphome/components/libretiny/gpio_arduino.cpp @@ -1,6 +1,7 @@ #ifdef USE_LIBRETINY #include "gpio_arduino.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome::libretiny { @@ -37,7 +38,7 @@ ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const { return ISRInternalGPIOPin((void *) arg); } -void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { +void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const { PinStatus arduino_mode = (PinStatus) 255; switch (type) { case gpio::INTERRUPT_RISING_EDGE: diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index da477fde36..b5ccf544ba 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -1,11 +1,11 @@ #pragma once #ifdef USE_LIBRETINY -#include "esphome/core/hal.h" +#include "esphome/core/gpio_pin.h" namespace esphome::libretiny { -class ArduinoInternalGPIOPin final : public InternalGPIOPin { +class ArduinoInternalGPIOPin final : public GPIOPin { public: void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } @@ -16,14 +16,18 @@ class ArduinoInternalGPIOPin final : public InternalGPIOPin { bool digital_read() override; void digital_write(bool value) override; size_t dump_summary(char *buffer, size_t len) const override; - void detach_interrupt() const override; - ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return pin_; } + void detach_interrupt() const; + template void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const { + this->attach_interrupt_(reinterpret_cast(func), arg, type); + } + ISRInternalGPIOPin to_isr() const; + uint8_t get_pin() const { return pin_; } gpio::Flags get_flags() const override { return flags_; } - bool is_inverted() const override { return inverted_; } + bool is_inverted() const { return inverted_; } + bool is_internal() override { return true; } protected: - void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const; uint8_t pin_; bool inverted_{}; diff --git a/esphome/components/libretiny/hal.h b/esphome/components/libretiny/hal.h index 48b94a5214..838536b866 100644 --- a/esphome/components/libretiny/hal.h +++ b/esphome/components/libretiny/hal.h @@ -8,6 +8,7 @@ #include #include +#include "esphome/core/main_task.h" #include "esphome/core/time_64.h" // IRAM_ATTR places a function in executable RAM so it is callable from an @@ -84,6 +85,10 @@ __attribute__((always_inline)) inline bool in_isr_context() { #endif } +/// Before setup() stores the handle every task counts as background; the wake is a no-op until then. +__attribute__((always_inline)) inline bool is_main_loop_thread() { + return xTaskGetCurrentTaskHandle() == esphome_main_task_handle; +} __attribute__((always_inline)) inline void yield() { ::yield(); } __attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } __attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } diff --git a/esphome/components/libretiny_pwm/__init__.py b/esphome/components/libretiny_pwm/__init__.py index 4db724f8ad..1825606769 100644 --- a/esphome/components/libretiny_pwm/__init__.py +++ b/esphome/components/libretiny_pwm/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@kuba2k2"] +DOMAIN = "libretiny_pwm" diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index b175620bbf..c490300b58 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -41,7 +41,7 @@ from esphome.core.entity_helpers import ( queue_entity_register, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass import esphome.final_validate as fv from esphome.types import ConfigType @@ -71,6 +71,7 @@ from .types import ( # noqa: F401 ChannelColors, ColorMode, GammaTable, + LightEffect, LightOutput, LightState, LightStateRTCState, @@ -519,6 +520,17 @@ def validate_color_temperature_channels(value): return value +def _effects_table(effects: list[MockObj]) -> MockObj: + """Emit the flash table of a light's effect pointers; each must be a static address.""" + return cg.shared_progmem_array( + "light_effects", + LightEffect.operator("ptr"), + cg.ArrayInitializer(*effects), + share=False, + constexpr=False, + ) + + @setup_entity("light") async def setup_light_core_(light_var, config, output_var): # All 8 legacy restore_mode values, and the restore_state key, are just different @@ -586,7 +598,7 @@ async def setup_light_core_(light_var, config, output_var): EFFECTS_REGISTRY, config.get(CONF_EFFECTS, []) ) if effects: - cg.add(light_var.add_effects(effects)) + cg.add(light_var.add_effects(_effects_table(effects), len(effects))) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 5ad9215eed..0970c513a2 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -147,7 +147,7 @@ void LightCall::perform() { // EFFECT StringRef effect_s; if (this->effect_ == 0u) { - effect_s = StringRef::from_lit("None"); + effect_s = EFFECT_NONE_REF; } else { effect_s = this->parent_->effects_[this->effect_ - 1]->get_name(); } diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index a89e3fec5a..3fabcf5d51 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -5,6 +5,8 @@ namespace esphome::light { +inline constexpr StringRef EFFECT_NONE_REF = StringRef::from_lit("None"); + class LightState; class LightEffect { diff --git a/esphome/components/light/light_effect_names.h b/esphome/components/light/light_effect_names.h new file mode 100644 index 0000000000..7ddf459b24 --- /dev/null +++ b/esphome/components/light/light_effect_names.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" +#include "light_effect.h" + +namespace esphome::light { + +/// Effect names as the native API lists them: "None" first, then each effect; empty without effects. +/// Reads the light's effect table directly, so listing entities allocates nothing. Holds a reference: +/// it only lives for one synchronous encode, and by value costs flash on every copy. +class LightEffectNames { + public: + explicit LightEffectNames(const ConstVector &effects) : effects_(effects) {} + + class Iterator { + public: + ESPHOME_ALWAYS_INLINE Iterator(const ConstVector &effects, size_t index) + : effects_(effects), index_(index) {} + ESPHOME_ALWAYS_INLINE StringRef operator*() const { + return this->index_ == 0 ? EFFECT_NONE_REF : this->effects_[this->index_ - 1]->get_name(); + } + ESPHOME_ALWAYS_INLINE Iterator &operator++() { + ++this->index_; + return *this; + } + ESPHOME_ALWAYS_INLINE bool operator!=(const Iterator &other) const { return this->index_ != other.index_; } + + protected: + const ConstVector &effects_; + size_t index_; + }; + + ESPHOME_ALWAYS_INLINE Iterator begin() const { return {this->effects_, 0}; } + ESPHOME_ALWAYS_INLINE Iterator end() const { + return {this->effects_, this->effects_.empty() ? 0 : this->effects_.size() + 1}; + } + ESPHOME_ALWAYS_INLINE bool empty() const { return this->effects_.empty(); } + + protected: + const ConstVector &effects_; +}; + +} // namespace esphome::light diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 9fe483a291..885440187f 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -9,8 +9,6 @@ #include "light_output.h" #include "transformers.h" -#include - namespace esphome::light { ESPHOME_LOG_TAG(TAG, "light"); @@ -196,8 +194,6 @@ void LightState::publish_state() { LightOutput *LightState::get_output() const { return this->output_; } -static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None"); - StringRef LightState::get_effect_name() { if (this->active_effect_index_ > 0) { return this->effects_[this->active_effect_index_ - 1]->get_name(); @@ -218,11 +214,6 @@ void LightState::add_target_state_reached_listener(LightTargetStateReachedListen this->target_state_reached_listeners_->push_back(listener); } -void LightState::add_effects(const std::initializer_list &effects) { - // Called once from Python codegen during setup with all effects from YAML config - this->effects_ = effects; -} - void LightState::current_values_as_brightness(float *brightness) { this->current_values.as_brightness(brightness); *brightness = this->gamma_correct_lut(*brightness); @@ -352,9 +343,6 @@ float LightState::gamma_uncorrect_lut(float value) const { #endif // USE_LIGHT_GAMMA_LUT void LightState::start_effect_(uint32_t effect_index) { - // An external add_effects() can exceed the codegen cap; ignore an index the uint16_t can't hold - if (effect_index > std::numeric_limits::max()) - return; this->stop_effect_(); if (effect_index == 0) return; diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index fcb2ca40c9..2a99943e1d 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -217,10 +217,13 @@ class LightState : public EntityBase, public Component { bool supports_effects() const { return !this->effects_.empty(); } /// Get all effects for this light state. - const FixedVector &get_effects() const { return this->effects_; } + const ConstVector &get_effects() const { return this->effects_; } - /// Add effects for this light state. - void add_effects(const std::initializer_list &effects); + /// Codegen only: point at the generated table of this light's effects, which must outlive it. + /// Codegen caps the count at 65535, so an effect index always fits the uint16_t active index. + void add_effects(LightEffect *const *effects, size_t count) { + this->effects_ = ConstVector(effects, count); + } /// Get the total number of effects available for this light. size_t get_effect_count() const { return this->effects_.size(); } @@ -354,7 +357,7 @@ class LightState : public EntityBase, public Component { /// The currently active transformer for this light (transition/flash). std::unique_ptr transformer_{nullptr}; /// List of effects for this light. - FixedVector effects_; + ConstVector effects_; /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index a1138c7de8..1a4234ceaf 100644 --- a/esphome/components/lightwaverf/__init__.py +++ b/esphome/components/lightwaverf/__init__.py @@ -15,6 +15,7 @@ from esphome.cpp_helpers import gpio_pin_expression from esphome.types import ConfigType CODEOWNERS = ["@max246"] +DOMAIN = "lightwaverf" lightwaverf_ns = cg.esphome_ns.namespace("lightwaverf") diff --git a/esphome/components/lilygo_t5_47/__init__.py b/esphome/components/lilygo_t5_47/__init__.py index 5499d096a9..cb3605c63e 100644 --- a/esphome/components/lilygo_t5_47/__init__.py +++ b/esphome/components/lilygo_t5_47/__init__.py @@ -1,3 +1,5 @@ import esphome.codegen as cg +DOMAIN = "lilygo_t5_47" + lilygo_t5_47_ns = cg.esphome_ns.namespace("lilygo_t5_47") diff --git a/esphome/components/lm75b/__init__.py b/esphome/components/lm75b/__init__.py index e69de29bb2..228b24febe 100644 --- a/esphome/components/lm75b/__init__.py +++ b/esphome/components/lm75b/__init__.py @@ -0,0 +1 @@ +DOMAIN = "lm75b" diff --git a/esphome/components/ln882h_ble/__init__.py b/esphome/components/ln882h_ble/__init__.py index aadc1f3b2f..7e382eda85 100644 --- a/esphome/components/ln882h_ble/__init__.py +++ b/esphome/components/ln882h_ble/__init__.py @@ -16,6 +16,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["ln882x"] CODEOWNERS = ["@Bl00d-B0b"] +DOMAIN = "ln882h_ble" ln882h_ble_ns = cg.esphome_ns.namespace("ln882h_ble") LN882HBLE = ln882h_ble_ns.class_("LN882HBLE", cg.Component) diff --git a/esphome/components/ln882h_ble_tracker/__init__.py b/esphome/components/ln882h_ble_tracker/__init__.py index 69a01ae1ac..0d2b3a5180 100644 --- a/esphome/components/ln882h_ble_tracker/__init__.py +++ b/esphome/components/ln882h_ble_tracker/__init__.py @@ -28,6 +28,7 @@ CONF_LN882H_BLE_ID = "ln882h_ble_id" DEPENDENCIES = ["ln882x"] AUTO_LOAD = ["ble_device_base", "ln882h_ble"] CODEOWNERS = ["@Bl00d-B0b"] +DOMAIN = "ln882h_ble_tracker" ble_device_base.register_hub_provider("ln882h_ble_tracker") diff --git a/esphome/components/ln882x/__init__.py b/esphome/components/ln882x/__init__.py index b4e179c4a9..d23702d799 100644 --- a/esphome/components/ln882x/__init__.py +++ b/esphome/components/ln882x/__init__.py @@ -35,6 +35,7 @@ from .boards import LN882X_BOARD_PINS, LN882X_BOARDS CODEOWNERS = ["@lamauny"] AUTO_LOAD = ["libretiny"] +DOMAIN = "ln882x" IS_TARGET_PLATFORM = True COMPONENT_DATA = LibreTinyComponent( diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index aa5bec98d5..cf723df9b9 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -22,6 +22,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@esphome/core"] +DOMAIN = "lock" IS_PLATFORM_COMPONENT = True lock_ns = cg.esphome_ns.namespace("lock") diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index a966ba67ad..67137214c1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -45,7 +45,6 @@ from esphome.const import ( CONF_HARDWARE_UART, CONF_ID, CONF_LEVEL, - CONF_LOGGER, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, @@ -365,7 +364,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config: ConfigType) -> None: baud_rate: int = config[CONF_BAUD_RATE] level = config[CONF_LEVEL] - CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level + CORE.data.setdefault(DOMAIN, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) # Determine task log buffer size. The buffer is a direct member of Logger diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index 00f67422f3..4bd979398c 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -1,13 +1,14 @@ import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv -from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_BUG +from esphome.const import CONF_LEVEL, ENTITY_CATEGORY_CONFIG, ICON_BUG from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented from esphome.types import ConfigType from .. import ( CONF_LOGGER_ID, + DOMAIN, LOG_LEVELS, Logger, logger_ns, @@ -31,7 +32,7 @@ async def to_code(config: ConfigType) -> None: request_logger_level_listeners() parent = await cg.get_variable(config[CONF_LOGGER_ID]) levels = list(LOG_LEVELS) - index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL]) + index = levels.index(CORE.data[DOMAIN][CONF_LEVEL]) levels = levels[: index + 1] var = await select.new_select(config, options=levels) await register_parented(var, parent) diff --git a/esphome/components/lps22/__init__.py b/esphome/components/lps22/__init__.py index e69de29bb2..b9936d7ab1 100644 --- a/esphome/components/lps22/__init__.py +++ b/esphome/components/lps22/__init__.py @@ -0,0 +1 @@ +DOMAIN = "lps22" diff --git a/esphome/components/lsm6ds/__init__.py b/esphome/components/lsm6ds/__init__.py index b1044276a1..311058249e 100644 --- a/esphome/components/lsm6ds/__init__.py +++ b/esphome/components/lsm6ds/__init__.py @@ -3,6 +3,7 @@ from esphome.components import i2c from esphome.components.motion import MotionComponent CODEOWNERS = ["@clydebarrow"] +DOMAIN = "lsm6ds" CONF_LSM6DS_ID = "lsm6ds_id" # C++ namespace / class diff --git a/esphome/components/ltr390/__init__.py b/esphome/components/ltr390/__init__.py index e69de29bb2..3bf5ae2563 100644 --- a/esphome/components/ltr390/__init__.py +++ b/esphome/components/ltr390/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ltr390" diff --git a/esphome/components/ltr501/__init__.py b/esphome/components/ltr501/__init__.py index dd06cfffea..450e38be0f 100644 --- a/esphome/components/ltr501/__init__.py +++ b/esphome/components/ltr501/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@latonita"] +DOMAIN = "ltr501" diff --git a/esphome/components/ltr_als_ps/__init__.py b/esphome/components/ltr_als_ps/__init__.py index dd06cfffea..3b6fa0d268 100644 --- a/esphome/components/ltr_als_ps/__init__.py +++ b/esphome/components/ltr_als_ps/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@latonita"] +DOMAIN = "ltr_als_ps" diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index e6527bbc9b..30480eacc7 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components.binary_sensor import BinarySensor +from esphome.components.rotary_encoder import DOMAIN as ROTARY_ENCODER_DOMAIN from esphome.components.rotary_encoder.sensor import RotaryEncoderSensor import esphome.config_validation as cv from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR @@ -27,7 +28,7 @@ ENCODERS_CONFIG = cv.ensure_list( cv.Required(CONF_SENSOR): cv.Any( cv.All( cv.use_id(RotaryEncoderSensor), - cv.requires_component("rotary_encoder"), + cv.requires_component(ROTARY_ENCODER_DOMAIN), ), cv.Schema( { diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 961fa4079d..d8a96de639 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -3,19 +3,17 @@ from typing import Any import esphome.codegen as cg from esphome.components import image -from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw +from esphome.components.color import ( + CONF_HEX, + DOMAIN as COLOR_DOMAIN, + ColorStruct, + from_rgbw, +) from esphome.components.const.css_colors import CSS_COLORS -from esphome.components.font import Font +from esphome.components.font import DOMAIN as FONT_DOMAIN, Font from esphome.components.image import Image_ import esphome.config_validation as cv -from esphome.const import ( - CONF_ARGS, - CONF_COLOR, - CONF_FORMAT, - CONF_ID, - CONF_TIME, - CONF_VALUE, -) +from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_TIME, CONF_VALUE from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj, StaticCastExpression, call_lambda from esphome.cpp_types import ESPTime, int32, uint32 @@ -81,7 +79,7 @@ def get_component_colors(value): if isinstance(value, int): return value >> 16, value >> 8 & 0xFF, value & 0xFF if isinstance(value, ID): - cval = [x for x in CORE.config[CONF_COLOR] if x[CONF_ID] == value][0] + cval = [x for x in CORE.config[COLOR_DOMAIN] if x[CONF_ID] == value][0] if CONF_HEX in cval: r, g, b = cval[CONF_HEX] else: @@ -437,7 +435,7 @@ class LvFont(LValidator): add_lv_use("font") fontval = cv.use_id(Font)(value) get_esphome_fonts_used().add(fontval) - return cv.requires_component("font")(fontval) + return cv.requires_component(FONT_DOMAIN)(fontval) # Use font::Font* as return type for lambdas returning ESPHome fonts # The inline overloads in lvgl_esphome.h handle conversion to lv_font_t* diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index 0d8d99ab21..b23e77df22 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -1,16 +1,10 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv -from esphome.const import CONF_ON_RELEASE, CONF_RESTORE_VALUE +from esphome.const import CONF_RESTORE_VALUE from esphome.cpp_generator import MockObj -from ..defines import ( - CONF_ANIMATED, - CONF_TRIGGER, - CONF_UPDATE_ON_RELEASE, - CONF_WIDGET, - LOGGER, -) +from ..defines import CONF_ANIMATED, CONF_TRIGGER, CONF_UPDATE_ON_RELEASE, CONF_WIDGET from ..lv_validation import animated from ..lvcode import ( EVENT_ARG, @@ -31,20 +25,18 @@ CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend( cv.Required(CONF_WIDGET): cv.use_id(LvNumber), **VALUE_TRIGGER_SCHEMA, cv.Optional(CONF_ANIMATED, default=True): animated, - cv.Optional(CONF_UPDATE_ON_RELEASE): cv.boolean, + # Removed in 2026.11.0 - kept to provide helpful error message + # Remove before 2027.5.0 + cv.Optional(CONF_UPDATE_ON_RELEASE): cv.invalid( + "The 'update_on_release' option has been removed in ESPHome 2026.11.0.\n" + "Use 'trigger: on_release' instead." + ), cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, } ) async def to_code(config): - trigger = config[CONF_TRIGGER] - if CONF_UPDATE_ON_RELEASE in config: - LOGGER.warning( - "Option 'update_on_release' is deprecated and will be removed in 2026.11.0 - use 'trigger: on_release' instead" - ) - if config[CONF_UPDATE_ON_RELEASE]: - trigger = CONF_ON_RELEASE widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] async with LambdaContext([], return_type=cg.float_) as value: @@ -70,6 +62,6 @@ async def to_code(config): lvgl_static.add_event_cb( widget.obj, await event.get_lambda(), - *TRIGGER_EVENT_MAP[trigger], + *TRIGGER_EVENT_MAP[config[CONF_TRIGGER]], ) ) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index cf766b96c8..924c17fb54 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -4,6 +4,7 @@ from typing import Any from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation +from esphome.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from esphome.components.mapping import mapping_class from esphome.components.time import RealTimeClock from esphome.config_validation import prepend_path @@ -148,7 +149,8 @@ PRESS_TIME = cv.All( ENCODER_SCHEMA = cv.Schema( { cv.GenerateID(): cv.All( - cv.declare_id(LVEncoderListener), cv.requires_component("binary_sensor") + cv.declare_id(LVEncoderListener), + cv.requires_component(BINARY_SENSOR_DOMAIN), ), cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t), cv.Optional(df.CONF_INITIAL_FOCUS): cv.All( diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index 0bb5715439..6b645da3aa 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -1,5 +1,9 @@ import esphome.codegen as cg -from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen +from esphome.components.touchscreen import ( + CONF_TOUCHSCREEN_ID, + DOMAIN as TOUCHSCREEN_DOMAIN, + Touchscreen, +) import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE @@ -17,7 +21,7 @@ CONF_TOUCHSCREEN = "touchscreen" TOUCHSCREENS_CONFIG = cv.maybe_simple_value( { cv.Required(CONF_TOUCHSCREEN_ID): cv.All( - cv.use_id(Touchscreen), cv.requires_component(CONF_TOUCHSCREEN) + cv.use_id(Touchscreen), cv.requires_component(TOUCHSCREEN_DOMAIN) ), cv.Optional(CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, cv.Optional(CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, @@ -29,7 +33,7 @@ TOUCHSCREENS_CONFIG = cv.maybe_simple_value( def touchscreen_schema(config): value = cv.ensure_list(TOUCHSCREENS_CONFIG)(config) - if value or CONF_TOUCHSCREEN not in CORE.loaded_integrations: + if value or TOUCHSCREEN_DOMAIN not in CORE.loaded_integrations: return value return [TOUCHSCREENS_CONFIG(config)] diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index ba32288128..fedc79383d 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components.image import get_image_metadata +from esphome.components.image import DOMAIN as IMAGE_DOMAIN, get_image_metadata import esphome.config_validation as cv from esphome.const import ( CONF_COLOR, @@ -214,7 +214,7 @@ INDICATOR_SCHEMA = cv.Schema( cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_image_t), } ), - cv.requires_component("image"), + cv.requires_component(IMAGE_DOMAIN), ), cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend( { diff --git a/esphome/components/m5stack_8angle/__init__.py b/esphome/components/m5stack_8angle/__init__.py index 6404bcf64c..96b9782e81 100644 --- a/esphome/components/m5stack_8angle/__init__.py +++ b/esphome/components/m5stack_8angle/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["i2c"] CODEOWNERS = ["@rnauber"] +DOMAIN = "m5stack_8angle" MULTI_CONF = True CONF_M5STACK_8ANGLE_ID = "m5stack_8angle_id" diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index f9b744daf0..8e9b21fe43 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -7,6 +7,7 @@ from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID from esphome.types import ConfigType CODEOWNERS = ["@ssieb"] +DOMAIN = "matrix_keypad" AUTO_LOAD = ["key_provider"] diff --git a/esphome/components/max17043/__init__.py b/esphome/components/max17043/__init__.py index 1db25ccdd6..ba609881f0 100644 --- a/esphome/components/max17043/__init__.py +++ b/esphome/components/max17043/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@blacknell"] +DOMAIN = "max17043" diff --git a/esphome/components/max31855/__init__.py b/esphome/components/max31855/__init__.py index e69de29bb2..b8a76cd500 100644 --- a/esphome/components/max31855/__init__.py +++ b/esphome/components/max31855/__init__.py @@ -0,0 +1 @@ +DOMAIN = "max31855" diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 3d681e4923..4bef41761c 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -7,6 +7,8 @@ namespace esphome::max31855 { ESPHOME_LOG_TAG(TAG, "max31855"); +static constexpr uint32_t READ_TIMEOUT_ID = 0; + void MAX31855Sensor::update() { this->enable(); delay(1); @@ -14,7 +16,7 @@ void MAX31855Sensor::update() { this->disable(); // Conversion time typ: 170ms, max: 220ms - this->set_timeout("value", 220, [this]() { this->read_data_(); }); + this->set_timeout(READ_TIMEOUT_ID, 220, [this]() { this->read_data_(); }); } void MAX31855Sensor::setup() { this->spi_setup(); } diff --git a/esphome/components/max31856/__init__.py b/esphome/components/max31856/__init__.py index e69de29bb2..3fde339873 100644 --- a/esphome/components/max31856/__init__.py +++ b/esphome/components/max31856/__init__.py @@ -0,0 +1 @@ +DOMAIN = "max31856" diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index 5612a4cbf7..bdb47216da 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -7,6 +7,8 @@ namespace esphome::max31856 { ESPHOME_LOG_TAG(TAG, "max31856"); +static constexpr uint32_t READ_TIMEOUT_ID = 0; + // Based on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31856 void MAX31856Sensor::setup() { @@ -42,7 +44,7 @@ void MAX31856Sensor::update() { this->one_shot_temperature_(); // Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz - this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, + this->set_timeout(READ_TIMEOUT_ID, filter_ == FILTER_60HZ ? 155 : 185, [this]() { this->read_thermocouple_temperature_(); }); } diff --git a/esphome/components/max31865/__init__.py b/esphome/components/max31865/__init__.py index e69de29bb2..42068dc04f 100644 --- a/esphome/components/max31865/__init__.py +++ b/esphome/components/max31865/__init__.py @@ -0,0 +1 @@ +DOMAIN = "max31865" diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index b637d6dbfc..94899b05ab 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -8,6 +8,8 @@ namespace esphome::max31865 { ESPHOME_LOG_TAG(TAG, "max31865"); +static constexpr uint32_t READ_TIMEOUT_ID = 0; + void MAX31865Sensor::update() { // Check new faults since last measurement if (!has_fault_) { @@ -59,7 +61,7 @@ void MAX31865Sensor::update() { this->write_config_(0b11100000, 0b10100000); // Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz - this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); }); + this->set_timeout(READ_TIMEOUT_ID, filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); }); } void MAX31865Sensor::setup() { diff --git a/esphome/components/max44009/__init__.py b/esphome/components/max44009/__init__.py index e69de29bb2..a7d755f6a6 100644 --- a/esphome/components/max44009/__init__.py +++ b/esphome/components/max44009/__init__.py @@ -0,0 +1 @@ +DOMAIN = "max44009" diff --git a/esphome/components/max6675/__init__.py b/esphome/components/max6675/__init__.py index e69de29bb2..d00df9770d 100644 --- a/esphome/components/max6675/__init__.py +++ b/esphome/components/max6675/__init__.py @@ -0,0 +1 @@ +DOMAIN = "max6675" diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index ad0c470e33..2fc8dbb73d 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -5,6 +5,8 @@ namespace esphome::max6675 { ESPHOME_LOG_TAG(TAG, "max6675"); +static constexpr uint32_t READ_TIMEOUT_ID = 0; + void MAX6675Sensor::update() { this->enable(); delay(1); @@ -12,7 +14,7 @@ void MAX6675Sensor::update() { this->disable(); // Conversion time typ: 170ms, max: 220ms - this->set_timeout("value", 250, [this]() { this->read_data_(); }); + this->set_timeout(READ_TIMEOUT_ID, 250, [this]() { this->read_data_(); }); } void MAX6675Sensor::setup() { this->spi_setup(); } diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index 05abaf886f..77e82fdec7 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -15,6 +15,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@looping40"] +DOMAIN = "max6956" DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/max7219/__init__.py b/esphome/components/max7219/__init__.py index e69de29bb2..2c1a783395 100644 --- a/esphome/components/max7219/__init__.py +++ b/esphome/components/max7219/__init__.py @@ -0,0 +1 @@ +DOMAIN = "max7219" diff --git a/esphome/components/max7219digit/__init__.py b/esphome/components/max7219digit/__init__.py index e69de29bb2..33d7ad3f80 100644 --- a/esphome/components/max7219digit/__init__.py +++ b/esphome/components/max7219digit/__init__.py @@ -0,0 +1 @@ +DOMAIN = "max7219digit" diff --git a/esphome/components/max9611/__init__.py b/esphome/components/max9611/__init__.py index 9b0ea14b57..a3fc6ca774 100644 --- a/esphome/components/max9611/__init__.py +++ b/esphome/components/max9611/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@mckaymatthew"] +DOMAIN = "max9611" diff --git a/esphome/components/mcp23008/__init__.py b/esphome/components/mcp23008/__init__.py index 3d1480a6f7..9f1e9c9f96 100644 --- a/esphome/components/mcp23008/__init__.py +++ b/esphome/components/mcp23008/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["mcp23x08_base"] CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["i2c"] +DOMAIN = "mcp23008" MULTI_CONF = True mcp23008_ns = cg.esphome_ns.namespace("mcp23008") diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index aa606507f0..7dddf4b042 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -16,6 +16,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] +DOMAIN = "mcp23016" MULTI_CONF = True mcp23016_ns = cg.esphome_ns.namespace("mcp23016") diff --git a/esphome/components/mcp23017/__init__.py b/esphome/components/mcp23017/__init__.py index 474d75f6ff..6733f19456 100644 --- a/esphome/components/mcp23017/__init__.py +++ b/esphome/components/mcp23017/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["mcp23x17_base"] CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["i2c"] +DOMAIN = "mcp23017" MULTI_CONF = True mcp23017_ns = cg.esphome_ns.namespace("mcp23017") diff --git a/esphome/components/mcp23s08/__init__.py b/esphome/components/mcp23s08/__init__.py index ffc51b8146..95a396865b 100644 --- a/esphome/components/mcp23s08/__init__.py +++ b/esphome/components/mcp23s08/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["mcp23x08_base"] CODEOWNERS = ["@SenexCrenshaw", "@jesserockz"] DEPENDENCIES = ["spi"] +DOMAIN = "mcp23s08" MULTI_CONF = True CONF_DEVICEADDRESS = "deviceaddress" diff --git a/esphome/components/mcp23s17/__init__.py b/esphome/components/mcp23s17/__init__.py index d693a64ce8..9745b74bc0 100644 --- a/esphome/components/mcp23s17/__init__.py +++ b/esphome/components/mcp23s17/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["mcp23x17_base"] CODEOWNERS = ["@SenexCrenshaw", "@jesserockz"] DEPENDENCIES = ["spi"] +DOMAIN = "mcp23s17" MULTI_CONF = True CONF_DEVICEADDRESS = "deviceaddress" diff --git a/esphome/components/mcp23x08_base/__init__.py b/esphome/components/mcp23x08_base/__init__.py index a3c12165f0..d283d57bd0 100644 --- a/esphome/components/mcp23x08_base/__init__.py +++ b/esphome/components/mcp23x08_base/__init__.py @@ -3,6 +3,7 @@ from esphome.components import mcp23xxx_base AUTO_LOAD = ["mcp23xxx_base"] CODEOWNERS = ["@jesserockz"] +DOMAIN = "mcp23x08_base" NUM_PINS = 8 diff --git a/esphome/components/mcp23x17_base/__init__.py b/esphome/components/mcp23x17_base/__init__.py index 1b93d16ff3..6160987cef 100644 --- a/esphome/components/mcp23x17_base/__init__.py +++ b/esphome/components/mcp23x17_base/__init__.py @@ -3,6 +3,7 @@ from esphome.components import mcp23xxx_base AUTO_LOAD = ["mcp23xxx_base"] CODEOWNERS = ["@jesserockz"] +DOMAIN = "mcp23x17_base" NUM_PINS = 16 diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index a626835518..f7449b0ab9 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -20,6 +20,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["gpio_expander"] CODEOWNERS = ["@jesserockz"] +DOMAIN = "mcp23xxx_base" mcp23xxx_base_ns = cg.esphome_ns.namespace("mcp23xxx_base") MCP23XXXBase = mcp23xxx_base_ns.class_("MCP23XXXBase", cg.Component) diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index b8032fcec8..8ad2f7521e 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -4,6 +4,8 @@ namespace esphome::mcp23xxx_base { +void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg) { arg->enable_loop_soon_any_context(); } + template void MCP23XXXGPIOPin::setup() { this->pin_mode(flags_); // When interrupt_pin is configured, pin_mode() already auto-enables CHANGE diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 1c45b0f4af..7414677175 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -8,6 +8,10 @@ namespace esphome::mcp23xxx_base { enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING }; +// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the +// ISR in flash. +void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg); + template class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander { public: virtual void pin_mode(uint8_t pin, gpio::Flags flags); @@ -35,7 +39,8 @@ template class MCP23XXXBase : public Component, public gpio_expander: void setup_interrupt_pin_() { if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->setup(); - this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->interrupt_pin_->attach_interrupt(&mcp23xxx_gpio_intr, static_cast(this), + gpio::INTERRUPT_FALLING_EDGE); this->set_invalidate_on_read_(false); } // Disable loop until an input pin is configured via pin_mode() @@ -43,7 +48,6 @@ template class MCP23XXXBase : public Component, public gpio_expander: // For polling mode, loop is re-enabled when pin_mode() registers an input pin this->disable_loop(); } - static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); } // read a given register virtual bool read_reg(uint8_t reg, uint8_t *value) = 0; diff --git a/esphome/components/mcp2515/__init__.py b/esphome/components/mcp2515/__init__.py index e69de29bb2..5b0e72b758 100644 --- a/esphome/components/mcp2515/__init__.py +++ b/esphome/components/mcp2515/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mcp2515" diff --git a/esphome/components/mcp3008/__init__.py b/esphome/components/mcp3008/__init__.py index 6d1bd5a970..f29050f9fe 100644 --- a/esphome/components/mcp3008/__init__.py +++ b/esphome/components/mcp3008/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["spi"] AUTO_LOAD = ["sensor"] +DOMAIN = "mcp3008" MULTI_CONF = True CONF_MCP3008 = "mcp3008" diff --git a/esphome/components/mcp3204/__init__.py b/esphome/components/mcp3204/__init__.py index 5757bdbfa9..7829a07d80 100644 --- a/esphome/components/mcp3204/__init__.py +++ b/esphome/components/mcp3204/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_REFERENCE_VOLTAGE from esphome.types import ConfigType DEPENDENCIES = ["spi"] +DOMAIN = "mcp3204" MULTI_CONF = True CODEOWNERS = ["@rsumner"] diff --git a/esphome/components/mcp3221/__init__.py b/esphome/components/mcp3221/__init__.py index 677bb78c35..8e5a7f134e 100644 --- a/esphome/components/mcp3221/__init__.py +++ b/esphome/components/mcp3221/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@philippderdiedas"] +DOMAIN = "mcp3221" diff --git a/esphome/components/mcp4461/__init__.py b/esphome/components/mcp4461/__init__.py index 60cece67d7..d5465098f2 100644 --- a/esphome/components/mcp4461/__init__.py +++ b/esphome/components/mcp4461/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@p1ngb4ck"] DEPENDENCIES = ["i2c"] +DOMAIN = "mcp4461" MULTI_CONF = True CONF_DISABLE_WIPER_0 = "disable_wiper_0" CONF_DISABLE_WIPER_1 = "disable_wiper_1" diff --git a/esphome/components/mcp4725/__init__.py b/esphome/components/mcp4725/__init__.py index e69de29bb2..d63ffaf7c3 100644 --- a/esphome/components/mcp4725/__init__.py +++ b/esphome/components/mcp4725/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mcp4725" diff --git a/esphome/components/mcp4728/__init__.py b/esphome/components/mcp4728/__init__.py index f48bdde681..7156b4aa7f 100644 --- a/esphome/components/mcp4728/__init__.py +++ b/esphome/components/mcp4728/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@berfenger"] DEPENDENCIES = ["i2c"] +DOMAIN = "mcp4728" MULTI_CONF = True CONF_STORE_IN_EEPROM = "store_in_eeprom" diff --git a/esphome/components/mcp47a1/__init__.py b/esphome/components/mcp47a1/__init__.py index e69de29bb2..274ca6fa01 100644 --- a/esphome/components/mcp47a1/__init__.py +++ b/esphome/components/mcp47a1/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mcp47a1" diff --git a/esphome/components/mcp9600/__init__.py b/esphome/components/mcp9600/__init__.py index e69de29bb2..3d85a4d937 100644 --- a/esphome/components/mcp9600/__init__.py +++ b/esphome/components/mcp9600/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mcp9600" diff --git a/esphome/components/mcp9808/__init__.py b/esphome/components/mcp9808/__init__.py index e69de29bb2..7a167ebf06 100644 --- a/esphome/components/mcp9808/__init__.py +++ b/esphome/components/mcp9808/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mcp9808" diff --git a/esphome/components/md5/__init__.py b/esphome/components/md5/__init__.py index 6a928d1682..0928766429 100644 --- a/esphome/components/md5/__init__.py +++ b/esphome/components/md5/__init__.py @@ -4,6 +4,7 @@ from esphome.helpers import IS_MACOS from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "md5" async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 0fb24fdf1d..e49a3897fe 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -5,7 +5,6 @@ import esphome.config_validation as cv from esphome.const import ( CONF_DISABLED, CONF_ID, - CONF_MDNS, CONF_OPENTHREAD, CONF_PORT, CONF_PROTOCOL, @@ -22,6 +21,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] +DOMAIN = "mdns" # Components that create mDNS services at runtime # IMPORTANT: If you add a new component here, you must also update the corresponding @@ -194,7 +194,7 @@ def request_service_enable_disable() -> bool: Public API for external components. Do not remove. """ - mdns_config = CORE.config.get(CONF_MDNS) + mdns_config = CORE.config.get(DOMAIN) if ( mdns_config is None or mdns_config[CONF_DISABLED] @@ -235,7 +235,7 @@ async def to_code(config: ConfigType) -> None: ethernet.request_ethernet_ip_state_listener() if CORE.is_esp32: - add_idf_component(name="espressif/mdns", ref="1.12.0") + add_idf_component(name="espressif/mdns", ref="1.14.0") # ESPHome only advertises; the browse APIs are unused add_idf_sdkconfig_option("CONFIG_MDNS_ENABLE_BROWSE", False) # The mdns console CLI is never used by ESPHome diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index d99e7877bf..d012773d8a 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -29,6 +29,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] +DOMAIN = "media_player" IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/media_source/__init__.py b/esphome/components/media_source/__init__.py index c9dab7e4d2..348ea01428 100644 --- a/esphome/components/media_source/__init__.py +++ b/esphome/components/media_source/__init__.py @@ -7,6 +7,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType CODEOWNERS = ["@kahrendt"] +DOMAIN = "media_source" AUTO_LOAD = ["audio"] diff --git a/esphome/components/mhz19/__init__.py b/esphome/components/mhz19/__init__.py index e69de29bb2..1b75339b45 100644 --- a/esphome/components/mhz19/__init__.py +++ b/esphome/components/mhz19/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mhz19" diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 9ad19f66d5..6d76e39986 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -471,14 +471,14 @@ void MicroWakeWord::loop() { // uses floating point operations. if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_, this->microphone_source_->get_audio_stream_info().get_sample_rate())) { - this->status_momentary_error("frontend_alloc", 1000); + this->status_momentary_error(1000); return; } if (!this->inference_task_.create(MicroWakeWord::inference_task, "mww", INFERENCE_TASK_STACK_SIZE, (void *) this, INFERENCE_TASK_PRIORITY, this->task_stack_in_psram_)) { FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state - this->status_momentary_error("task_start", 1000); + this->status_momentary_error(1000); } } break; diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index c604f64d77..a83827ef5d 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_ID, CONF_MICROPHONE, CONF_ON_DATA, - CONF_TRIGGER_ID, ) from esphome.core import CORE from esphome.coroutine import CoroPriority, coroutine_with_priority @@ -21,6 +20,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] +DOMAIN = "microphone" IS_PLATFORM_COMPONENT = True @@ -29,17 +29,12 @@ microphone_ns = cg.esphome_ns.namespace("microphone") Microphone = microphone_ns.class_("Microphone") MicrophoneSource = microphone_ns.class_("MicrophoneSource") -DataTrigger = microphone_ns.class_( - "DataTrigger", - automation.Trigger.template(cg.std_vector.template(cg.uint8).operator("ref")), -) - async def setup_microphone_core_(var: MockObj, config: ConfigType) -> None: for conf in config.get(CONF_ON_DATA, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_data_callback", [(cg.std_vector.template(cg.uint8).operator("ref").operator("const"), "x")], conf, ) @@ -53,11 +48,7 @@ async def register_microphone(var: MockObj, config: ConfigType) -> None: MICROPHONE_SCHEMA = cv.Schema.extend(audio.AUDIO_COMPONENT_SCHEMA).extend( { - cv.Optional(CONF_ON_DATA): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger), - } - ), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), } ) diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h deleted file mode 100644 index 0fac18d828..0000000000 --- a/esphome/components/microphone/automation.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "microphone.h" - -#include - -namespace esphome::microphone { - -class DataTrigger final : public Trigger &> { - public: - explicit DataTrigger(Microphone *mic) { - mic->add_data_callback([this](const std::vector &data) { this->trigger(data); }); - } -}; - -} // namespace esphome::microphone diff --git a/esphome/components/mics_4514/__init__.py b/esphome/components/mics_4514/__init__.py index e69de29bb2..9b985504b0 100644 --- a/esphome/components/mics_4514/__init__.py +++ b/esphome/components/mics_4514/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mics_4514" diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index d4794b6682..957cdf90ef 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -7,6 +7,8 @@ namespace esphome::mics_4514 { ESPHOME_LOG_TAG(TAG, "mics_4514"); +static constexpr uint32_t WARMUP_TIMEOUT_ID = 0; + static const uint8_t SENSOR_REGISTER = 0x04; static const uint8_t POWER_MODE_REGISTER = 0x0a; @@ -18,7 +20,7 @@ void MICS4514Component::setup() { power_mode = 0x01; this->write_register(POWER_MODE_REGISTER, &power_mode, 1); delay(100); // NOLINT - this->set_timeout("warmup", 3 * 60 * 1000, [this]() { this->warmed_up_ = true; }); + this->set_timeout(WARMUP_TIMEOUT_ID, 3 * 60 * 1000, [this]() { this->warmed_up_ = true; }); this->status_set_warning(); return; } diff --git a/esphome/components/midea/__init__.py b/esphome/components/midea/__init__.py index e69de29bb2..d9bb8492b3 100644 --- a/esphome/components/midea/__init__.py +++ b/esphome/components/midea/__init__.py @@ -0,0 +1 @@ +DOMAIN = "midea" diff --git a/esphome/components/midea_ac/__init__.py b/esphome/components/midea_ac/__init__.py index e69de29bb2..dbff1a1186 100644 --- a/esphome/components/midea_ac/__init__.py +++ b/esphome/components/midea_ac/__init__.py @@ -0,0 +1 @@ +DOMAIN = "midea_ac" diff --git a/esphome/components/midea_ir/__init__.py b/esphome/components/midea_ir/__init__.py index e69de29bb2..01f3c67f7c 100644 --- a/esphome/components/midea_ir/__init__.py +++ b/esphome/components/midea_ir/__init__.py @@ -0,0 +1 @@ +DOMAIN = "midea_ir" diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 526afa914b..b2351e23a4 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -35,6 +35,8 @@ from esphome.const import ( from esphome.core import CORE, TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +DOMAIN = "mipi" + LOGGER = cv.logging.getLogger(__name__) CONF_TRANSFORMS = "transforms" diff --git a/esphome/components/mipi_dsi/__init__.py b/esphome/components/mipi_dsi/__init__.py index 84dd66e2f4..ae17e00bdc 100644 --- a/esphome/components/mipi_dsi/__init__.py +++ b/esphome/components/mipi_dsi/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg CODEOWNERS = ["@clydebarrow"] +DOMAIN = "mipi_dsi" mipi_dsi_ns = cg.esphome_ns.namespace("mipi_dsi") diff --git a/esphome/components/mitsubishi/__init__.py b/esphome/components/mitsubishi/__init__.py index e69de29bb2..05ae0f0543 100644 --- a/esphome/components/mitsubishi/__init__.py +++ b/esphome/components/mitsubishi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mitsubishi" diff --git a/esphome/components/mixer/__init__.py b/esphome/components/mixer/__init__.py index e69de29bb2..5ea96da4d2 100644 --- a/esphome/components/mixer/__init__.py +++ b/esphome/components/mixer/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mixer" diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index dbe0799163..23867aef91 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -359,7 +359,7 @@ void MixerSpeaker::loop() { xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START); } else { ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); - this->status_momentary_error("failure", 1000); + this->status_momentary_error(1000); return; } } diff --git a/esphome/components/mk2pvrouter/__init__.py b/esphome/components/mk2pvrouter/__init__.py index d00b4ce8d0..7df1f7104f 100644 --- a/esphome/components/mk2pvrouter/__init__.py +++ b/esphome/components/mk2pvrouter/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@FredM67"] DEPENDENCIES = ["uart"] +DOMAIN = "mk2pvrouter" mk2pvrouter_ns = cg.esphome_ns.namespace("mk2pvrouter") Mk2PVRouter = mk2pvrouter_ns.class_("Mk2PVRouter", cg.Component, uart.UARTDevice) diff --git a/esphome/components/mlx90393/__init__.py b/esphome/components/mlx90393/__init__.py index fc92f02120..800d120558 100644 --- a/esphome/components/mlx90393/__init__.py +++ b/esphome/components/mlx90393/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@functionpointer"] +DOMAIN = "mlx90393" diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index 82d12a5789..e47b7d8c30 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -7,6 +7,8 @@ namespace esphome::mlx90393 { ESPHOME_LOG_TAG(TAG, "mlx90393"); +static constexpr uint32_t VERIFY_SETTINGS_TIMEOUT_ID = 0; + const LogString *settings_to_string(MLX90393Setting setting) { switch (setting) { case MLX90393_GAIN_SEL: @@ -114,7 +116,7 @@ void MLX90393Cls::setup() { } // start verify settings process - this->set_timeout("verify settings", 3000, [this]() { this->verify_settings_timeout_(MLX90393_GAIN_SEL); }); + this->set_timeout(VERIFY_SETTINGS_TIMEOUT_ID, 3000, [this]() { this->verify_settings_timeout_(MLX90393_GAIN_SEL); }); } void MLX90393Cls::dump_config() { @@ -268,7 +270,8 @@ void MLX90393Cls::verify_settings_timeout_(MLX90393Setting stage) { next_stage = static_cast(0); } - this->set_timeout("verify settings", 3000, [this, next_stage]() { this->verify_settings_timeout_(next_stage); }); + this->set_timeout(VERIFY_SETTINGS_TIMEOUT_ID, 3000, + [this, next_stage]() { this->verify_settings_timeout_(next_stage); }); } } // namespace esphome::mlx90393 diff --git a/esphome/components/mlx90614/__init__.py b/esphome/components/mlx90614/__init__.py index e69de29bb2..e1db3524a4 100644 --- a/esphome/components/mlx90614/__init__.py +++ b/esphome/components/mlx90614/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mlx90614" diff --git a/esphome/components/mmc5603/__init__.py b/esphome/components/mmc5603/__init__.py index cc88f26231..b7008ff157 100644 --- a/esphome/components/mmc5603/__init__.py +++ b/esphome/components/mmc5603/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@benhoff"] +DOMAIN = "mmc5603" diff --git a/esphome/components/mmc5983/__init__.py b/esphome/components/mmc5983/__init__.py index c8db8c4300..5e5287db5f 100644 --- a/esphome/components/mmc5983/__init__.py +++ b/esphome/components/mmc5983/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@agoode"] +DOMAIN = "mmc5983" diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index cad2781cc1..4e4d7fe160 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -18,6 +18,7 @@ from esphome.types import ConfigType, TemplateArgsType _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["uart"] +DOMAIN = "modbus" # Loading the hub makes the modbus_client.* actions available (they are registry entries only; no code is # generated unless a config uses one). AUTO_LOAD = ["modbus_client"] diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 7c56d2340f..80065546cf 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -11,6 +11,8 @@ namespace esphome::modbus { ESPHOME_LOG_TAG(TAG, "modbus"); +static constexpr uint32_t DEFERRED_SEND_TIMEOUT_ID = 0; + static constexpr size_t MODBUS_MAX_LOG_BYTES = 64; static constexpr uint32_t US_PER_SEC = 1000000; @@ -205,7 +207,7 @@ void ModbusServerHub::parse_modbus_frames() { while (!this->rx_buffer_.empty()) { if (this->deferred_payload_len_ != 0) { // Another frame arrived before the deferred reply went out, so the client has moved on. - this->cancel_timeout("deferred_send"); + this->cancel_timeout(DEFERRED_SEND_TIMEOUT_ID); ESP_LOGD(TAG, "Dropped deferred reply to %" PRIu8 ": a new frame arrived first", this->deferred_payload_[0]); this->deferred_payload_len_ = 0; } @@ -1216,7 +1218,7 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) { std::memcpy(this->deferred_payload_.data(), payload, len); this->deferred_payload_len_ = len; // set_timeout() takes milliseconds; round the microsecond delay up so we never fire early. - this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() { + this->set_timeout(DEFERRED_SEND_TIMEOUT_ID, (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() { ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1, this->deferred_payload_len_ - 1); this->deferred_payload_len_ = 0; diff --git a/esphome/components/modbus_client/__init__.py b/esphome/components/modbus_client/__init__.py index 015ebaa3f1..dee63f8ea9 100644 --- a/esphome/components/modbus_client/__init__.py +++ b/esphome/components/modbus_client/__init__.py @@ -18,6 +18,7 @@ from esphome.types import ConfigType, TemplateArgsType CODEOWNERS = ["@exciton"] DEPENDENCIES = ["modbus"] +DOMAIN = "modbus_client" MULTI_CONF = True # The modbus hub auto-loads this component to make the actions available. Without this, that auto-load # would try to create a device with no address. diff --git a/esphome/components/modbus_server/__init__.py b/esphome/components/modbus_server/__init__.py index 0de40a4cc1..bff23e364e 100644 --- a/esphome/components/modbus_server/__init__.py +++ b/esphome/components/modbus_server/__init__.py @@ -23,6 +23,7 @@ from .const import ( ) CODEOWNERS = ["@exciton"] +DOMAIN = "modbus_server" AUTO_LOAD = ["modbus"] diff --git a/esphome/components/monochromatic/__init__.py b/esphome/components/monochromatic/__init__.py index e69de29bb2..4f572e8557 100644 --- a/esphome/components/monochromatic/__init__.py +++ b/esphome/components/monochromatic/__init__.py @@ -0,0 +1 @@ +DOMAIN = "monochromatic" diff --git a/esphome/components/mopeka_ble/__init__.py b/esphome/components/mopeka_ble/__init__.py index ec7deb3067..968447dadf 100644 --- a/esphome/components/mopeka_ble/__init__.py +++ b/esphome/components/mopeka_ble/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@spbrogan", "@Fabian-Schmidt"] AUTO_LOAD = ["ble_device_base"] +DOMAIN = "mopeka_ble" CONF_SHOW_SENSORS_WITHOUT_SYNC = "show_sensors_without_sync" diff --git a/esphome/components/mopeka_pro_check/__init__.py b/esphome/components/mopeka_pro_check/__init__.py index c57f60f521..86bf451227 100644 --- a/esphome/components/mopeka_pro_check/__init__.py +++ b/esphome/components/mopeka_pro_check/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@spbrogan"] +DOMAIN = "mopeka_pro_check" diff --git a/esphome/components/mopeka_std_check/__init__.py b/esphome/components/mopeka_std_check/__init__.py index 88e344464f..be8f3d329b 100644 --- a/esphome/components/mopeka_std_check/__init__.py +++ b/esphome/components/mopeka_std_check/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Fabian-Schmidt"] +DOMAIN = "mopeka_std_check" diff --git a/esphome/components/mpl3115a2/__init__.py b/esphome/components/mpl3115a2/__init__.py index e69de29bb2..08956d32f6 100644 --- a/esphome/components/mpl3115a2/__init__.py +++ b/esphome/components/mpl3115a2/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mpl3115a2" diff --git a/esphome/components/mpr121/__init__.py b/esphome/components/mpr121/__init__.py index da56b4ff4b..1764ff9c05 100644 --- a/esphome/components/mpr121/__init__.py +++ b/esphome/components/mpr121/__init__.py @@ -25,6 +25,7 @@ CONF_MPR121 = "mpr121" CONF_MPR121_ID = "mpr121_id" DEPENDENCIES = ["i2c"] +DOMAIN = "mpr121" mpr121_ns = cg.esphome_ns.namespace("mpr121") MPR121Component = mpr121_ns.class_("MPR121Component", cg.Component, i2c.I2CDevice) diff --git a/esphome/components/mpu6050/__init__.py b/esphome/components/mpu6050/__init__.py index e69de29bb2..ee02560478 100644 --- a/esphome/components/mpu6050/__init__.py +++ b/esphome/components/mpu6050/__init__.py @@ -0,0 +1 @@ +DOMAIN = "mpu6050" diff --git a/esphome/components/mpu6886/__init__.py b/esphome/components/mpu6886/__init__.py index 933f71ccd7..af9cdd2f27 100644 --- a/esphome/components/mpu6886/__init__.py +++ b/esphome/components/mpu6886/__init__.py @@ -1 +1,3 @@ """Support for MPC-6886.""" + +DOMAIN = "mpu6886" diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 65a7cf409b..b21dc2e2f0 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -68,6 +68,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType DEPENDENCIES = ["network"] +DOMAIN = "mqtt" def AUTO_LOAD(): diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 906913723c..a04121e2a1 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() { this->button_->press(); } else { ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); - this->status_momentary_warning("state", 5000); + this->status_momentary_warning(5000); } }); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index b3d8ab80b1..6e5fad3efc 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -439,7 +439,7 @@ bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) { } else { delay(5); ESP_LOGV(TAG, "Subscribe failed for topic='%s'. Will retry", topic); - this->status_momentary_warning("subscribe", 1000); + this->status_momentary_warning(1000); } return ret != 0; } @@ -499,7 +499,7 @@ void MQTTClientComponent::unsubscribe(const std::string &topic) { } else { delay(5); ESP_LOGV(TAG, "Unsubscribe failed for topic='%s'.", topic.c_str()); - this->status_momentary_warning("unsubscribe", 1000); + this->status_momentary_warning(1000); } auto it = subscriptions_.begin(); @@ -553,7 +553,7 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t ESP_LOGVV(TAG, "Publish payload (len=%u): '%.*s'", payload_length, static_cast(payload_length), payload); } else { ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", topic, payload_length); - this->status_momentary_warning("publish", 1000); + this->status_momentary_warning(1000); } } return ret != 0; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index b4ae624404..141b5f12b3 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -273,6 +273,8 @@ class MQTTComponent : public Component { void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0); protected: + static constexpr uint32_t SEND_DEFER_ID = 0; + /// Helper method to get the discovery topic for this component into a buffer. StringRef get_discovery_topic_to_(std::span buf, const MQTTDiscoveryInfo &discovery_info) const; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 90f51cf6ce..7aa2f8e9d5 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -46,7 +46,7 @@ void MQTTFanComponent::setup() { case PARSE_NONE: default: ESP_LOGW(TAG, "Unknown state payload %s", payload.c_str()); - this->status_momentary_warning("state", 5000); + this->status_momentary_warning(5000); break; } }); @@ -71,7 +71,7 @@ void MQTTFanComponent::setup() { break; case PARSE_NONE: ESP_LOGW(TAG, "Unknown direction Payload %s", payload.c_str()); - this->status_momentary_warning("direction", 5000); + this->status_momentary_warning(5000); break; } }); @@ -95,7 +95,7 @@ void MQTTFanComponent::setup() { break; case PARSE_NONE: ESP_LOGW(TAG, "Unknown Oscillation Payload %s", payload.c_str()); - this->status_momentary_warning("oscillation", 5000); + this->status_momentary_warning(5000); break; } }); @@ -112,16 +112,16 @@ void MQTTFanComponent::setup() { this->state_->make_call().set_speed(speed_level).perform(); } else { ESP_LOGW(TAG, "Invalid speed level %d", speed_level); - this->status_momentary_warning("speed", 5000); + this->status_momentary_warning(5000); } } else { ESP_LOGW(TAG, "Invalid speed level %s (int expected)", payload.c_str()); - this->status_momentary_warning("speed", 5000); + this->status_momentary_warning(5000); } }); } - this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); + this->state_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); }); } void MQTTFanComponent::dump_config() { diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index e9058eb642..e1affae8a2 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -28,7 +28,7 @@ void MQTTJSONLightComponent::setup() { } void MQTTJSONLightComponent::on_light_remote_values_update() { - this->defer("send", [this]() { this->publish_state_(); }); + this->defer(SEND_DEFER_ID, [this]() { this->publish_state_(); }); } MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 8d8b4c38e8..8e9a628985 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -25,11 +25,11 @@ void MQTTLockComponent::setup() { this->lock_->open(); } else { ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); - this->status_momentary_warning("state", 5000); + this->status_momentary_warning(5000); } }); this->lock_->add_on_state_callback( - [this](LockState /*state*/) { this->defer("send", [this]() { this->publish_state(); }); }); + [this](LockState /*state*/) { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); }); } void MQTTLockComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", this->lock_->get_name().c_str()); diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index b786b4412e..44ce249772 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -29,12 +29,12 @@ void MQTTSwitchComponent::setup() { case PARSE_NONE: default: ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); - this->status_momentary_warning("state", 5000); + this->status_momentary_warning(5000); break; } }); this->switch_->add_on_state_callback( - [this](bool enabled) { this->defer("send", [this, enabled]() { this->publish_state(enabled); }); }); + [this](bool enabled) { this->defer(SEND_DEFER_ID, [this, enabled]() { this->publish_state(enabled); }); }); } void MQTTSwitchComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Switch '%s': ", this->switch_->get_name().c_str()); diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index a4565058ea..69b2f64771 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -20,11 +20,11 @@ void MQTTUpdateComponent::setup() { this->update_->perform(); } else { ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name_().c_str(), payload.c_str()); - this->status_momentary_warning("state", 5000); + this->status_momentary_warning(5000); } }); - this->update_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); + this->update_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); }); } bool MQTTUpdateComponent::publish_state() { diff --git a/esphome/components/mqtt_subscribe/__init__.py b/esphome/components/mqtt_subscribe/__init__.py index b17f321372..ee58f82fb6 100644 --- a/esphome/components/mqtt_subscribe/__init__.py +++ b/esphome/components/mqtt_subscribe/__init__.py @@ -1,3 +1,5 @@ import esphome.codegen as cg +DOMAIN = "mqtt_subscribe" + mqtt_subscribe_ns = cg.esphome_ns.namespace("mqtt_subscribe") diff --git a/esphome/components/ms5611/__init__.py b/esphome/components/ms5611/__init__.py index e69de29bb2..925c57d57b 100644 --- a/esphome/components/ms5611/__init__.py +++ b/esphome/components/ms5611/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ms5611" diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 8b275e53f2..8f66d704fe 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -6,6 +6,9 @@ namespace esphome::ms5611 { ESPHOME_LOG_TAG(TAG, "ms5611"); +static constexpr uint32_t TEMPERATURE_TIMEOUT_ID = 0; +static constexpr uint32_t PRESSURE_TIMEOUT_ID = 1; + static const uint8_t MS5611_ADDRESS = 0x77; static const uint8_t MS5611_CMD_ADC_READ = 0x00; static const uint8_t MS5611_CMD_RESET = 0x1E; @@ -44,7 +47,7 @@ void MS5611Component::update() { return; } - this->set_timeout("temperature", 10, [this]() { this->read_temperature_(); }); + this->set_timeout(TEMPERATURE_TIMEOUT_ID, 10, [this]() { this->read_temperature_(); }); } void MS5611Component::read_temperature_() { uint8_t bytes[3]; @@ -60,7 +63,7 @@ void MS5611Component::read_temperature_() { return; } - this->set_timeout("pressure", 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); }); + this->set_timeout(PRESSURE_TIMEOUT_ID, 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); }); } void MS5611Component::read_pressure_(uint32_t raw_temperature) { uint8_t bytes[3]; diff --git a/esphome/components/ms8607/__init__.py b/esphome/components/ms8607/__init__.py index e1cd49ec7b..9fc73f80a9 100644 --- a/esphome/components/ms8607/__init__.py +++ b/esphome/components/ms8607/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@e28eta"] +DOMAIN = "ms8607" diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index 832e7ab2d9..2e20c4e0ea 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -9,6 +9,12 @@ namespace esphome::ms8607 { /// TAG used for logging calls ESPHOME_LOG_TAG(TAG, "ms8607"); +static constexpr uint32_t RESET_TIMEOUT_ID = 0; +static constexpr uint32_t PROM_READ_TIMEOUT_ID = 1; +static constexpr uint32_t TEMPERATURE_TIMEOUT_ID = 2; +static constexpr uint32_t PRESSURE_TIMEOUT_ID = 3; +static constexpr uint32_t HUMIDITY_TIMEOUT_ID = 4; + /// Reset the Pressure/Temperature sensor static const uint8_t MS8607_PT_CMD_RESET = 0x1E; @@ -97,7 +103,7 @@ void MS8607Component::try_reset_() { if (--this->reset_attempts_remaining_ > 0) { uint32_t delay = this->reset_interval_; this->reset_interval_ *= 5; - this->set_timeout("reset", delay, [this]() { this->try_reset_(); }); + this->set_timeout(RESET_TIMEOUT_ID, delay, [this]() { this->try_reset_(); }); this->status_set_error(); } else { this->mark_failed(); @@ -110,7 +116,7 @@ void MS8607Component::try_reset_() { this->status_clear_error(); // 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library - this->set_timeout("prom-read", 15, [this]() { + this->set_timeout(PROM_READ_TIMEOUT_ID, 15, [this]() { if (this->read_calibration_values_from_prom_()) { this->setup_status_ = SetupStatus::SUCCESSFUL; this->status_clear_error(); @@ -251,7 +257,7 @@ void MS8607Component::request_read_temperature_() { } // datasheet says 17.2ms max conversion time at OSR 8192 - this->set_timeout("temperature", 20, [this]() { this->read_temperature_(); }); + this->set_timeout(TEMPERATURE_TIMEOUT_ID, 20, [this]() { this->read_temperature_(); }); } void MS8607Component::read_temperature_() { @@ -272,7 +278,8 @@ void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) { } // datasheet says 17.2ms max conversion time at OSR 8192 - this->set_timeout("pressure", 20, [this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); }); + this->set_timeout(PRESSURE_TIMEOUT_ID, 20, + [this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); }); } void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) { @@ -293,7 +300,7 @@ void MS8607Component::request_read_humidity_(float temperature_float) { } // datasheet says 15.89ms max conversion time at OSR 8192 - this->set_timeout("humidity", 20, [this, temperature_float]() { this->read_humidity_(temperature_float); }); + this->set_timeout(HUMIDITY_TIMEOUT_ID, 20, [this, temperature_float]() { this->read_humidity_(temperature_float); }); } void MS8607Component::read_humidity_(float temperature_float) { diff --git a/esphome/components/msa3xx/__init__.py b/esphome/components/msa3xx/__init__.py index 0beece6710..75260a1b0a 100644 --- a/esphome/components/msa3xx/__init__.py +++ b/esphome/components/msa3xx/__init__.py @@ -20,6 +20,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@latonita"] DEPENDENCIES = ["i2c"] +DOMAIN = "msa3xx" MULTI_CONF = True diff --git a/esphome/components/my9231/__init__.py b/esphome/components/my9231/__init__.py index e43454d6ed..6fcb09a347 100644 --- a/esphome/components/my9231/__init__.py +++ b/esphome/components/my9231/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( from esphome.types import ConfigType AUTO_LOAD = ["output"] +DOMAIN = "my9231" my9231_ns = cg.esphome_ns.namespace("my9231") MY9231OutputComponent = my9231_ns.class_("MY9231OutputComponent", cg.Component) diff --git a/esphome/components/nau7802/__init__.py b/esphome/components/nau7802/__init__.py index e69de29bb2..d86eed44b8 100644 --- a/esphome/components/nau7802/__init__.py +++ b/esphome/components/nau7802/__init__.py @@ -0,0 +1 @@ +DOMAIN = "nau7802" diff --git a/esphome/components/neopixelbus/__init__.py b/esphome/components/neopixelbus/__init__.py index e69de29bb2..0294a2a735 100644 --- a/esphome/components/neopixelbus/__init__.py +++ b/esphome/components/neopixelbus/__init__.py @@ -0,0 +1 @@ +DOMAIN = "neopixelbus" diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index decee27bcf..1c08202d41 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -21,6 +21,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] +DOMAIN = "network" _LOGGER = logging.getLogger(__name__) @@ -277,7 +278,7 @@ def get_priority_interfaces_from_full_config(full_config: ConfigType) -> set[str """ return { entry["interface"] - for entry in full_config.get("network", {}).get(CONF_PRIORITY, []) + for entry in full_config.get(DOMAIN, {}).get(CONF_PRIORITY, []) } diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index efb6c88d28..c1ab86427e 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -11,6 +11,7 @@ CONF_NEXTION_ID = "nextion_id" CONF_PUBLISH_STATE = "publish_state" CONF_SEND_TO_NEXTION = "send_to_nextion" +DOMAIN = "nextion" FILTER_SOURCE_FILES = filter_source_files_from_platform( { "nextion_upload_esp32.cpp": { diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 18f0a73928..3a70be21ca 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,5 +1,3 @@ -import logging - from esphome import automation import esphome.codegen as cg from esphome.components import display, esp32, uart @@ -43,8 +41,6 @@ from .base_component import ( CONF_WAKE_UP_PAGE, ) -_LOGGER = logging.getLogger(__name__) - CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] DEPENDENCIES = ["uart"] @@ -56,15 +52,6 @@ def AUTO_LOAD() -> list[str]: return base -def _deprecated_dump_device_info(value): - _LOGGER.warning( - "'dump_device_info' is deprecated and will be removed in ESPHome 2026.11.0. " - "Device info is now always logged at connection time. " - "Please remove this option from your configuration." - ) - return value - - def _validate_tft_upload(config): has_tft_url = CONF_TFT_URL in config for conf_key in ( @@ -91,9 +78,11 @@ CONFIG_SCHEMA = cv.All( cv.positive_time_period_milliseconds, cv.Range(max=TimePeriod(milliseconds=255)), ), - # Deprecated — device info is now always logged. Remove before 2026.11.0. - cv.Optional(CONF_DUMP_DEVICE_INFO): cv.All( - cv.boolean, _deprecated_dump_device_info + # Removed in 2026.11.0 - kept to provide helpful error message + # Remove before 2027.5.0 + cv.Optional(CONF_DUMP_DEVICE_INFO): cv.invalid( + "The 'dump_device_info' option has been removed in ESPHome 2026.11.0.\n" + "Device info is now always logged, so remove this option from your configuration." ), cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, cv.Optional(CONF_MAX_QUEUE_AGE, default="8000ms"): cv.All( diff --git a/esphome/components/nfc/__init__.py b/esphome/components/nfc/__init__.py index 6afc2eca09..a837edead5 100644 --- a/esphome/components/nfc/__init__.py +++ b/esphome/components/nfc/__init__.py @@ -2,6 +2,7 @@ from esphome import automation import esphome.codegen as cg CODEOWNERS = ["@jesserockz", "@kbx81"] +DOMAIN = "nfc" nfc_ns = cg.esphome_ns.namespace("nfc") diff --git a/esphome/components/noblex/__init__.py b/esphome/components/noblex/__init__.py index ceceff9df9..a7c86b064b 100644 --- a/esphome/components/noblex/__init__.py +++ b/esphome/components/noblex/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@AGalfra"] +DOMAIN = "noblex" diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index 66043236b1..1769c11018 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -10,6 +10,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "noise" # Keep in sync with platformio.ini and esphome/idf_component.yml. # LIBSODIUM_VERSION must match the version noise-c pins in its manifests. diff --git a/esphome/components/npi19/__init__.py b/esphome/components/npi19/__init__.py index e69de29bb2..190848e54d 100644 --- a/esphome/components/npi19/__init__.py +++ b/esphome/components/npi19/__init__.py @@ -0,0 +1 @@ +DOMAIN = "npi19" diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 86d485777c..7cd0c6cdc1 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -12,6 +12,7 @@ from esphome.build_helpers import pch from esphome.build_helpers.ccache import resolve_ccache_path import esphome.codegen as cg from esphome.components.zephyr import ( + Section, add_extra_script, copy_files as zephyr_copy_files, zephyr_add_overlay, @@ -86,6 +87,7 @@ from .gpio import nrf52_pin_to_code # noqa: F401 CODEOWNERS = ["@tomaszduda23"] AUTO_LOAD = ["zephyr", "preferences"] +DOMAIN = "nrf52" IS_TARGET_PLATFORM = True _LOGGER = logging.getLogger(__name__) @@ -125,11 +127,37 @@ def set_core_data(config: ConfigType) -> ConfigType: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: - zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]) + sections = BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]] + if CORE.testing_mode: + sections = _testing_mode_sections(sections) + zephyr_add_pm_static(sections) return config +# In testing mode, fake a larger flash to allow linking grouped component +# tests. The nRF52840 has 1 MB and a mcumgr OTA config halves the app slot, +# which an openthread config alone fills; CI images are never flashed. +NRF52840_FLASH_SIZE = 0x100000 +TESTING_FLASH_SIZE = 0x400000 + + +def _testing_mode_sections(sections: list[Section]) -> list[Section]: + """Move a bootloader pinned to the end of the real flash to the end of the + faked one, so the partition manager still sees a single gap for the app.""" + return [ + Section( + section.name, + section.address + TESTING_FLASH_SIZE - NRF52840_FLASH_SIZE, + section.size, + section.region, + ) + if section.end_address == NRF52840_FLASH_SIZE + else section + for section in sections + ] + + _TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.SDK_NRF) _resolve_toolchain = cv.resolve_toolchain("nRF52", _TOOLCHAINS, Toolchain.SDK_NRF) @@ -380,6 +408,15 @@ async def to_code(config: ConfigType) -> None: zephyr_setup_preferences() zephyr_to_code(config) + if CORE.testing_mode: + zephyr_add_overlay( + f""" + &flash0 {{ + reg = <0x0 {TESTING_FLASH_SIZE:#x}>; + }}; + """ + ) + if dfu_config := config.get(CONF_DFU): CORE.add_job(_dfu_to_code, dfu_config) framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] diff --git a/esphome/components/ntc/__init__.py b/esphome/components/ntc/__init__.py index e69de29bb2..6b126fe8d0 100644 --- a/esphome/components/ntc/__init__.py +++ b/esphome/components/ntc/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ntc" diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 8edbdecee5..5822c95c7e 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -154,6 +154,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_WIND_DIRECTION, DEVICE_CLASS_WIND_SPEED, ] +DOMAIN = "number" IS_PLATFORM_COMPONENT = True number_ns = cg.esphome_ns.namespace("number") diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py index 8d35aaa34e..69368a990a 100644 --- a/esphome/components/one_wire/__init__.py +++ b/esphome/components/one_wire/__init__.py @@ -5,6 +5,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@ssieb"] +DOMAIN = "one_wire" IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp index 65a9962349..e950472072 100644 --- a/esphome/components/one_wire/one_wire.cpp +++ b/esphome/components/one_wire/one_wire.cpp @@ -4,19 +4,6 @@ namespace esphome::one_wire { ESPHOME_LOG_TAG(TAG, "one_wire"); -const std::string &OneWireDevice::get_address_name() { - if (this->address_name_.empty()) { - char hex_buf[19]; // "0x" + 16 hex chars + null - this->address_name_ = format_hex_prefixed_to(hex_buf, this->address_); - } - return this->address_name_; -} - -void OneWireDevice::set_address(uint64_t address) { - this->address_ = address; - this->address_name_.clear(); -} - bool OneWireDevice::send_command_(uint8_t cmd) { if (!this->bus_->select(this->address_)) return false; diff --git a/esphome/components/one_wire/one_wire.h b/esphome/components/one_wire/one_wire.h index 4dbbe11792..e3abb95bb7 100644 --- a/esphome/components/one_wire/one_wire.h +++ b/esphome/components/one_wire/one_wire.h @@ -7,14 +7,17 @@ namespace esphome::one_wire { #define LOG_ONE_WIRE_DEVICE(this) \ - ESP_LOGCONFIG(TAG, " Address: %s (%s)", this->get_address_name().c_str(), \ - LOG_STR_ARG(this->bus_->get_model_str(this->address_ & 0xff))); + do { \ + char address_buf[format_hex_prefixed_size(sizeof(uint64_t))]; \ + ESP_LOGCONFIG(TAG, " Address: %s (%s)", format_hex_prefixed_to(address_buf, (this)->address_), \ + LOG_STR_ARG((this)->bus_->get_model_str((this)->address_ & 0xff))); \ + } while (0) class OneWireDevice { public: /// @brief store the address of the device /// @param address of the device - void set_address(uint64_t address); + void set_address(uint64_t address) { this->address_ = address; } void set_index(uint8_t index) { this->index_ = index; } @@ -22,16 +25,12 @@ class OneWireDevice { /// @param bus pointer to the OneWireBus object void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; } - /// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29". - const std::string &get_address_name(); - protected: static constexpr uint8_t INDEX_NOT_SET = 255; uint64_t address_{0}; uint8_t index_{INDEX_NOT_SET}; OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance - std::string address_name_; /// @brief find an address if necessary /// should be called from setup diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index 85632d0bf8..cfcdfe7783 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -5,12 +5,13 @@ import esphome.codegen as cg from esphome.components import sensor from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266 +from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 from esphome.core import CORE from . import const, generate, schema, validate CODEOWNERS = ["@olegtarasov"] +DOMAIN = "opentherm" MULTI_CONF = True CONF_IN_PIN = "in_pin" @@ -26,14 +27,16 @@ CONF_SYNC_MODE = "sync_mode" CONF_BEFORE_SEND = "before_send" CONF_BEFORE_PROCESS_RESPONSE = "before_process_response" -# Triggers -BeforeSendTrigger = generate.opentherm_ns.class_( - "BeforeSendTrigger", - automation.Trigger.template(generate.OpenthermData.operator("ref")), -) -BeforeProcessResponseTrigger = generate.opentherm_ns.class_( - "BeforeProcessResponseTrigger", - automation.Trigger.template(generate.OpenthermData.operator("ref")), +_DATA_ARGS = [(generate.OpenthermData.operator("ref"), "x")] +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_BEFORE_SEND, "add_on_before_send_callback", _DATA_ARGS + ), + automation.CallbackAutomation( + CONF_BEFORE_PROCESS_RESPONSE, + "add_on_before_process_response_callback", + _DATA_ARGS, + ), ) CONFIG_SCHEMA = cv.All( @@ -50,17 +53,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean, - cv.Optional(CONF_BEFORE_SEND): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger), - } - ), + cv.Optional(CONF_BEFORE_SEND): automation.validate_automation({}), cv.Optional(CONF_BEFORE_PROCESS_RESPONSE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - BeforeProcessResponseTrigger - ), - } + {} ), } ) @@ -131,14 +126,4 @@ async def to_code(config: dict[str, Any]) -> None: generate.define_setting_readers(const.SETTING, settings) generate.add_messages(var, settings, schema.SETTINGS) - for conf in config.get(CONF_BEFORE_SEND, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(generate.OpenthermData.operator("ref"), "x")], conf - ) - - for conf in config.get(CONF_BEFORE_PROCESS_RESPONSE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(generate.OpenthermData.operator("ref"), "x")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/opentherm/automation.h b/esphome/components/opentherm/automation.h deleted file mode 100644 index 365992b280..0000000000 --- a/esphome/components/opentherm/automation.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "hub.h" -#include "opentherm.h" - -namespace esphome::opentherm { - -class BeforeSendTrigger final : public Trigger { - public: - BeforeSendTrigger(OpenthermHub *hub) { - hub->add_on_before_send_callback([this](OpenthermData &x) { this->trigger(x); }); - } -}; - -class BeforeProcessResponseTrigger final : public Trigger { - public: - BeforeProcessResponseTrigger(OpenthermHub *hub) { - hub->add_on_before_process_response_callback([this](OpenthermData &x) { this->trigger(x); }); - } -}; - -} // namespace esphome::opentherm diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 80b28a2628..25c7a78beb 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -19,7 +19,7 @@ from esphome.components.esp32 import ( require_vfs_select, ) from esphome.components.mdns import MDNSComponent, enable_mdns_storage -from esphome.components.network import add_use_address +from esphome.components.network import DOMAIN as NETWORK_DOMAIN, add_use_address from esphome.components.nrf52.framework import include_west_project from esphome.components.zephyr import zephyr_add_prj_conf from esphome.config_helpers import filter_source_files_from_platform @@ -62,6 +62,7 @@ from .const import ( ) CODEOWNERS = ["@mrene"] +DOMAIN = "openthread" AUTO_LOAD = ["network"] @@ -260,7 +261,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(_: ConfigType) -> None: full_config = fv.full_config.get() - network_config = full_config.get("network", {}) + network_config = full_config.get(NETWORK_DOMAIN, {}) if not network_config.get(CONF_ENABLE_IPV6, False): raise cv.Invalid( "OpenThread requires IPv6 to be enabled in the network component. " diff --git a/esphome/components/openthread_info/__init__.py b/esphome/components/openthread_info/__init__.py index e69de29bb2..245d84a104 100644 --- a/esphome/components/openthread_info/__init__.py +++ b/esphome/components/openthread_info/__init__.py @@ -0,0 +1 @@ +DOMAIN = "openthread_info" diff --git a/esphome/components/opt3001/__init__.py b/esphome/components/opt3001/__init__.py index e69de29bb2..efd544b1e2 100644 --- a/esphome/components/opt3001/__init__.py +++ b/esphome/components/opt3001/__init__.py @@ -0,0 +1 @@ +DOMAIN = "opt3001" diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp index c6692d8dad..0101368ceb 100644 --- a/esphome/components/opt3001/opt3001.cpp +++ b/esphome/components/opt3001/opt3001.cpp @@ -5,6 +5,9 @@ namespace esphome::opt3001 { ESPHOME_LOG_TAG(TAG, "opt3001.sensor"); +static constexpr uint32_t RESULT_RETRY_TIMEOUT_ID = 0; +static constexpr uint32_t READ_TIMEOUT_ID = 1; + static const uint8_t OPT3001_REG_RESULT = 0x00; static const uint8_t OPT3001_REG_CONFIGURATION = 0x01; // See datasheet for full description of each bit. @@ -42,7 +45,7 @@ void OPT3001Sensor::read_result_(const std::function &f) { if ((raw_value & OPT3001_CONFIGURATION_CONVERSION_MODE_MASK) != OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN) { // not ready; wait 10ms and try again ESP_LOGW(TAG, "Data not ready; waiting 10ms"); - this->set_timeout("opt3001_wait", 10, [this, f]() { read_result_(f); }); + this->set_timeout(RESULT_RETRY_TIMEOUT_ID, 10, [this, f]() { read_result_(f); }); return; } @@ -70,7 +73,7 @@ void OPT3001Sensor::read_lx_(const std::function &f) { return; } - this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() { + this->set_timeout(READ_TIMEOUT_ID, OPT3001_CONVERSION_TIME_800, [this, f]() { if (this->write(&OPT3001_REG_CONFIGURATION, 1) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Starting configuration register read failed"); f(NAN); diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 6031a68489..f7e03a1699 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -22,6 +22,7 @@ from esphome.types import ConfigType OTA_STATE_LISTENER_KEY = "ota_state_listener" CODEOWNERS = ["@esphome/core"] +DOMAIN = "ota" def AUTO_LOAD() -> list[str]: diff --git a/esphome/components/output/button/output_button.cpp b/esphome/components/output/button/output_button.cpp index 6ebbae13d6..17ffc39d7f 100644 --- a/esphome/components/output/button/output_button.cpp +++ b/esphome/components/output/button/output_button.cpp @@ -5,6 +5,8 @@ namespace esphome::output { ESPHOME_LOG_TAG(TAG, "output.button"); +static constexpr uint32_t RESET_TIMEOUT_ID = 0; + void OutputButton::dump_config() { LOG_BUTTON("", "Output Button", this); ESP_LOGCONFIG(TAG, " Duration: %.1fs", this->duration_ / 1e3f); @@ -12,8 +14,8 @@ void OutputButton::dump_config() { void OutputButton::press_action() { this->output_->turn_on(); - // Use a named timeout so that it's automatically cancelled if button is pressed again before it's reset - this->set_timeout("reset", this->duration_, [this]() { this->output_->turn_off(); }); + // One timeout id, so a second press restarts the reset instead of stacking another + this->set_timeout(RESET_TIMEOUT_ID, this->duration_, [this]() { this->output_->turn_off(); }); } } // namespace esphome::output diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 703af8281d..9498d4b4da 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -7,6 +7,7 @@ from typing import Any from esphome import git, yaml_util from esphome.components.substitutions import ( + DOMAIN as SUBSTITUTIONS_DOMAIN, ContextVars, ErrList, push_context, @@ -38,7 +39,7 @@ from esphome.core import EsphomeError _LOGGER = logging.getLogger(__name__) -DOMAIN = CONF_PACKAGES +DOMAIN = "packages" # Guard against infinite include chains (e.g. A includes B includes A). MAX_INCLUDE_DEPTH = 20 @@ -353,7 +354,7 @@ def _walk_packages( packages = config[CONF_PACKAGES] packages_path = (path or []) + [CONF_PACKAGES] - with cv.prepend_path(CONF_PACKAGES): + with cv.prepend_path(DOMAIN): if isinstance(packages, yaml_util.IncludeFile): # If the packages key is an IncludeFile, resolve it first before processing. packages = resolve_include( @@ -599,7 +600,7 @@ def do_packages_pass( if CONF_PACKAGES not in config: return config - with cv.prepend_path(CONF_SUBSTITUTIONS): + with cv.prepend_path(SUBSTITUTIONS_DOMAIN): substitutions = UserDict( resolve_substitutions_block( config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions diff --git a/esphome/components/partition/__init__.py b/esphome/components/partition/__init__.py index e69de29bb2..7d911eb405 100644 --- a/esphome/components/partition/__init__.py +++ b/esphome/components/partition/__init__.py @@ -0,0 +1 @@ +DOMAIN = "partition" diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index 9756e9431a..69d8935977 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -18,6 +18,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@Mat931"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["gpio_expander"] +DOMAIN = "pca6416a" MULTI_CONF = True pca6416a_ns = cg.esphome_ns.namespace("pca6416a") diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index 68e6d163e8..cba5656e88 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -17,6 +17,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@hwstar", "@clydebarrow", "@bdraco"] AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] +DOMAIN = "pca9554" MULTI_CONF = True CONF_PIN_COUNT = "pin_count" pca9554_ns = cg.esphome_ns.namespace("pca9554") diff --git a/esphome/components/pca9685/__init__.py b/esphome/components/pca9685/__init__.py index 817cfd1c30..16103aee44 100644 --- a/esphome/components/pca9685/__init__.py +++ b/esphome/components/pca9685/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( from esphome.types import ConfigType DEPENDENCIES = ["i2c"] +DOMAIN = "pca9685" MULTI_CONF = True pca9685_ns = cg.esphome_ns.namespace("pca9685") diff --git a/esphome/components/pcd8544/__init__.py b/esphome/components/pcd8544/__init__.py index e69de29bb2..ff11812a30 100644 --- a/esphome/components/pcd8544/__init__.py +++ b/esphome/components/pcd8544/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pcd8544" diff --git a/esphome/components/pcf85063/__init__.py b/esphome/components/pcf85063/__init__.py index e69de29bb2..577cea5e1e 100644 --- a/esphome/components/pcf85063/__init__.py +++ b/esphome/components/pcf85063/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pcf85063" diff --git a/esphome/components/pcf8563/__init__.py b/esphome/components/pcf8563/__init__.py index e69de29bb2..624ec1be8c 100644 --- a/esphome/components/pcf8563/__init__.py +++ b/esphome/components/pcf8563/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pcf8563" diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index 4ee1c2af56..6c9ed3cd98 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -16,6 +16,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] +DOMAIN = "pcf8574" MULTI_CONF = True pcf8574_ns = cg.esphome_ns.namespace("pcf8574") diff --git a/esphome/components/pcm5122/__init__.py b/esphome/components/pcm5122/__init__.py index 81e00ca74b..f79d1eadfc 100644 --- a/esphome/components/pcm5122/__init__.py +++ b/esphome/components/pcm5122/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@remcom"] +DOMAIN = "pcm5122" diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py index 45d84b88ef..cd901cb448 100644 --- a/esphome/components/pi4ioe5v6408/__init__.py +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -20,6 +20,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["gpio_expander"] CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["i2c"] +DOMAIN = "pi4ioe5v6408" MULTI_CONF = True diff --git a/esphome/components/pid/__init__.py b/esphome/components/pid/__init__.py index 71a87b6ae5..b7b1c5c7af 100644 --- a/esphome/components/pid/__init__.py +++ b/esphome/components/pid/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@OttoWinter"] +DOMAIN = "pid" diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 67585f65e8..d33a7f7544 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -5,6 +5,8 @@ namespace esphome::pid { ESPHOME_LOG_TAG(TAG, "pid.climate"); +static constexpr uint32_t AUTOTUNE_PROGRESS_INTERVAL_ID = 0; + bool PIDClimate::set_deadband_thresholds(float threshold_low, float threshold_high) { if (threshold_low > threshold_high) { ESP_LOGW(TAG, "Deadband threshold low %.2f must not be greater than high %.2f", threshold_low, threshold_high); @@ -191,7 +193,7 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { "Until your sensor provides a reading, the autotuner may display \'nan\'", this->get_name().c_str()); - this->set_interval("autotune-progress", 10000, [this]() { + this->set_interval(AUTOTUNE_PROGRESS_INTERVAL_ID, 10000, [this]() { if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) this->autotuner_->dump_config(); }); diff --git a/esphome/components/pipsolar/__init__.py b/esphome/components/pipsolar/__init__.py index b404409145..54337b0b13 100644 --- a/esphome/components/pipsolar/__init__.py +++ b/esphome/components/pipsolar/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["uart"] CODEOWNERS = ["@andreashergert1984"] AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "switch", "output"] +DOMAIN = "pipsolar" MULTI_CONF = True CONF_PIPSOLAR_ID = "pipsolar_id" diff --git a/esphome/components/pixoo/__init__.py b/esphome/components/pixoo/__init__.py index b1de57df8f..a2c5172bd1 100644 --- a/esphome/components/pixoo/__init__.py +++ b/esphome/components/pixoo/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@jesserockz"] +DOMAIN = "pixoo" diff --git a/esphome/components/pm1006/__init__.py b/esphome/components/pm1006/__init__.py index e69de29bb2..e79bb3f2a4 100644 --- a/esphome/components/pm1006/__init__.py +++ b/esphome/components/pm1006/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pm1006" diff --git a/esphome/components/pm2005/__init__.py b/esphome/components/pm2005/__init__.py index 3716dd7b5e..bda634dc03 100644 --- a/esphome/components/pm2005/__init__.py +++ b/esphome/components/pm2005/__init__.py @@ -1 +1,3 @@ """PM2005/2105 component for ESPHome.""" + +DOMAIN = "pm2005" diff --git a/esphome/components/pmsa003i/__init__.py b/esphome/components/pmsa003i/__init__.py index e69de29bb2..e66395e3de 100644 --- a/esphome/components/pmsa003i/__init__.py +++ b/esphome/components/pmsa003i/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pmsa003i" diff --git a/esphome/components/pmsx003/__init__.py b/esphome/components/pmsx003/__init__.py index e69de29bb2..1c60a105dd 100644 --- a/esphome/components/pmsx003/__init__.py +++ b/esphome/components/pmsx003/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pmsx003" diff --git a/esphome/components/pmwcs3/__init__.py b/esphome/components/pmwcs3/__init__.py index b04c30a005..64229140cb 100644 --- a/esphome/components/pmwcs3/__init__.py +++ b/esphome/components/pmwcs3/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@SeByDocKy"] +DOMAIN = "pmwcs3" diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index c9118834c5..b8efeb213c 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -13,6 +13,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter", "@jesserockz"] AUTO_LOAD = ["binary_sensor", "nfc"] +DOMAIN = "pn532" MULTI_CONF = True CONF_PN532_ID = "pn532_id" diff --git a/esphome/components/pn532_i2c/__init__.py b/esphome/components/pn532_i2c/__init__.py index 16b877b1f1..bd2a9373d7 100644 --- a/esphome/components/pn532_i2c/__init__.py +++ b/esphome/components/pn532_i2c/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["pn532"] CODEOWNERS = ["@OttoWinter", "@jesserockz"] DEPENDENCIES = ["i2c"] +DOMAIN = "pn532_i2c" MULTI_CONF = True pn532_i2c_ns = cg.esphome_ns.namespace("pn532_i2c") diff --git a/esphome/components/pn532_spi/__init__.py b/esphome/components/pn532_spi/__init__.py index 734f794180..0e24425f76 100644 --- a/esphome/components/pn532_spi/__init__.py +++ b/esphome/components/pn532_spi/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["pn532"] CODEOWNERS = ["@OttoWinter", "@jesserockz"] DEPENDENCIES = ["spi"] +DOMAIN = "pn532_spi" MULTI_CONF = True pn532_spi_ns = cg.esphome_ns.namespace("pn532_spi") diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 14299ee310..4fbe07e2e3 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["pn71xx"] CODEOWNERS = ["@kbx81", "@jesserockz"] +DOMAIN = "pn7150" pn7150_ns = cg.esphome_ns.namespace("pn7150") PN7150 = pn7150_ns.class_("PN7150", pn71xx.PN71xx) diff --git a/esphome/components/pn7150_i2c/__init__.py b/esphome/components/pn7150_i2c/__init__.py index d15c0b431e..c0dfb4c2e4 100644 --- a/esphome/components/pn7150_i2c/__init__.py +++ b/esphome/components/pn7150_i2c/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["pn7150"] CODEOWNERS = ["@kbx81", "@jesserockz"] DEPENDENCIES = ["i2c"] +DOMAIN = "pn7150_i2c" pn7150_i2c_ns = cg.esphome_ns.namespace("pn7150_i2c") PN7150I2C = pn7150_i2c_ns.class_("PN7150I2C", pn7150.PN7150, i2c.I2CDevice) diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index 4cde36e9e7..f1bd6122ff 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["pn71xx"] CODEOWNERS = ["@kbx81", "@jesserockz"] +DOMAIN = "pn7160" CONF_DWL_REQ_PIN = "dwl_req_pin" CONF_WKUP_REQ_PIN = "wkup_req_pin" diff --git a/esphome/components/pn7160_i2c/__init__.py b/esphome/components/pn7160_i2c/__init__.py index d711c3e48f..f9add96bdf 100644 --- a/esphome/components/pn7160_i2c/__init__.py +++ b/esphome/components/pn7160_i2c/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["pn7160"] CODEOWNERS = ["@kbx81", "@jesserockz"] DEPENDENCIES = ["i2c"] +DOMAIN = "pn7160_i2c" pn7160_i2c_ns = cg.esphome_ns.namespace("pn7160_i2c") PN7160I2C = pn7160_i2c_ns.class_("PN7160I2C", pn7160.PN7160, i2c.I2CDevice) diff --git a/esphome/components/pn7160_spi/__init__.py b/esphome/components/pn7160_spi/__init__.py index 7705dfa6f6..32c1008f72 100644 --- a/esphome/components/pn7160_spi/__init__.py +++ b/esphome/components/pn7160_spi/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["pn7160"] CODEOWNERS = ["@kbx81", "@jesserockz"] DEPENDENCIES = ["spi"] +DOMAIN = "pn7160_spi" MULTI_CONF = True pn7160_spi_ns = cg.esphome_ns.namespace("pn7160_spi") diff --git a/esphome/components/pn71xx/__init__.py b/esphome/components/pn71xx/__init__.py index 8472d4192c..3b40eb1141 100644 --- a/esphome/components/pn71xx/__init__.py +++ b/esphome/components/pn71xx/__init__.py @@ -17,6 +17,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["binary_sensor", "nfc"] CODEOWNERS = ["@kbx81", "@jesserockz"] +DOMAIN = "pn71xx" CONF_EMULATION_MESSAGE = "emulation_message" CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py index b68118b352..4d90642b5f 100644 --- a/esphome/components/power_supply/__init__.py +++ b/esphome/components/power_supply/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "power_supply" power_supply_ns = cg.esphome_ns.namespace("power_supply") PowerSupply = power_supply_ns.class_("PowerSupply", cg.Component) MULTI_CONF = True diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index 7daee1663a..8fa1b92c25 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -5,9 +5,16 @@ namespace esphome::power_supply { ESPHOME_LOG_TAG(TAG, "power_supply"); +static constexpr uint32_t POWER_OFF_TIMEOUT_ID = 0; + void PowerSupply::setup() { this->pin_->setup(); - this->pin_->digital_write(false); + if (this->pin_->is_held()) { + // Rail stayed on across the reset; drop it unless something asks for it + this->schedule_off_if_idle_(); + } else { + this->pin_->digital_write(false); + } if (this->enable_on_boot_) this->request_high_power(); } @@ -31,7 +38,7 @@ bool PowerSupply::is_enabled() const { return this->active_requests_ != 0; } void PowerSupply::request_high_power() { if (this->active_requests_ == 0) { - this->cancel_timeout("power-supply-off"); + this->cancel_timeout(POWER_OFF_TIMEOUT_ID); ESP_LOGV(TAG, "Enabling"); this->pin_->digital_write(true); delay(this->enable_time_); @@ -45,14 +52,21 @@ void PowerSupply::unrequest_high_power() { return; } this->active_requests_--; + this->schedule_off_if_idle_(); +} + +void PowerSupply::schedule_off_if_idle_() { if (this->active_requests_ == 0) { - this->set_timeout("power-supply-off", this->keep_on_time_, [this]() { + this->set_timeout(POWER_OFF_TIMEOUT_ID, this->keep_on_time_, [this]() { ESP_LOGV(TAG, "Disabling"); this->pin_->digital_write(false); }); } } + void PowerSupply::on_powerdown() { + if (this->pin_->get_hold()) + return; this->active_requests_ = 0; this->pin_->digital_write(false); } diff --git a/esphome/components/power_supply/power_supply.h b/esphome/components/power_supply/power_supply.h index eaf77af32e..dc6b368ea8 100644 --- a/esphome/components/power_supply/power_supply.h +++ b/esphome/components/power_supply/power_supply.h @@ -39,6 +39,7 @@ class PowerSupply final : public Component { uint32_t keep_on_time_; int16_t active_requests_{0}; // use signed integer to make catching negative requests easier. bool enable_on_boot_{false}; + void schedule_off_if_idle_(); }; class PowerSupplyRequester { diff --git a/esphome/components/preferences/__init__.py b/esphome/components/preferences/__init__.py index abd050ec3b..142cfb32dc 100644 --- a/esphome/components/preferences/__init__.py +++ b/esphome/components/preferences/__init__.py @@ -7,6 +7,7 @@ from esphome.coroutine import CoroPriority from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "preferences" preferences_ns = cg.esphome_ns.namespace("preferences") IntervalSyncer = preferences_ns.class_("IntervalSyncer", cg.PollingComponent) diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index 3624fb2442..8bf0163fa7 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -7,6 +7,7 @@ from esphome.cpp_types import EntityBase from esphome.types import ConfigType AUTO_LOAD = ["web_server_base"] +DOMAIN = "prometheus" prometheus_ns = cg.esphome_ns.namespace("prometheus") PrometheusHandler = prometheus_ns.class_("PrometheusHandler", cg.Component) diff --git a/esphome/components/pulse_counter/__init__.py b/esphome/components/pulse_counter/__init__.py index e69de29bb2..9efceae6de 100644 --- a/esphome/components/pulse_counter/__init__.py +++ b/esphome/components/pulse_counter/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pulse_counter" diff --git a/esphome/components/pulse_meter/__init__.py b/esphome/components/pulse_meter/__init__.py index e69de29bb2..7c4fef139c 100644 --- a/esphome/components/pulse_meter/__init__.py +++ b/esphome/components/pulse_meter/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pulse_meter" diff --git a/esphome/components/pulse_width/__init__.py b/esphome/components/pulse_width/__init__.py index e69de29bb2..97cf2e1c39 100644 --- a/esphome/components/pulse_width/__init__.py +++ b/esphome/components/pulse_width/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pulse_width" diff --git a/esphome/components/pvvx_mithermometer/__init__.py b/esphome/components/pvvx_mithermometer/__init__.py index e69de29bb2..56767de804 100644 --- a/esphome/components/pvvx_mithermometer/__init__.py +++ b/esphome/components/pvvx_mithermometer/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pvvx_mithermometer" diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index 57876f8c07..995c4aff50 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -9,6 +9,8 @@ namespace esphome::pvvx_mithermometer { ESPHOME_LOG_TAG(TAG, "display.pvvx_mithermometer"); +static constexpr uint32_t DISCONNECT_TIMEOUT_ID = 0; + void PVVXDisplay::dump_config() { char service_buf[ble_device_base::UUID_STR_LEN]; char char_buf[ble_device_base::UUID_STR_LEN]; @@ -39,7 +41,7 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t case ESP_GATTC_DISCONNECT_EVT: ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str()); this->connection_established_ = false; - this->cancel_timeout("disconnect"); + this->cancel_timeout(DISCONNECT_TIMEOUT_ID); this->char_handle_ = 0; break; case ESP_GATTC_SEARCH_CMPL_EVT: { @@ -147,8 +149,8 @@ void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) { void PVVXDisplay::delayed_disconnect_() { if (this->disconnect_delay_ms_ == 0) return; - this->cancel_timeout("disconnect"); - this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); }); + this->cancel_timeout(DISCONNECT_TIMEOUT_ID); + this->set_timeout(DISCONNECT_TIMEOUT_ID, this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); }); } void PVVXDisplay::sync_time_and_display_() { diff --git a/esphome/components/pylontech/__init__.py b/esphome/components/pylontech/__init__.py index 242a613a6c..f9eef4c809 100644 --- a/esphome/components/pylontech/__init__.py +++ b/esphome/components/pylontech/__init__.py @@ -10,6 +10,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@functionpointer"] DEPENDENCIES = ["uart"] +DOMAIN = "pylontech" MULTI_CONF = True CONF_PYLONTECH_ID = "pylontech_id" diff --git a/esphome/components/pzem004t/__init__.py b/esphome/components/pzem004t/__init__.py index e69de29bb2..f8d5b7a0c1 100644 --- a/esphome/components/pzem004t/__init__.py +++ b/esphome/components/pzem004t/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pzem004t" diff --git a/esphome/components/pzem6l24/__init__.py b/esphome/components/pzem6l24/__init__.py index e69de29bb2..45ecb06188 100644 --- a/esphome/components/pzem6l24/__init__.py +++ b/esphome/components/pzem6l24/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pzem6l24" diff --git a/esphome/components/pzemac/__init__.py b/esphome/components/pzemac/__init__.py index e69de29bb2..600881c324 100644 --- a/esphome/components/pzemac/__init__.py +++ b/esphome/components/pzemac/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pzemac" diff --git a/esphome/components/pzemdc/__init__.py b/esphome/components/pzemdc/__init__.py index e69de29bb2..81e2bf067d 100644 --- a/esphome/components/pzemdc/__init__.py +++ b/esphome/components/pzemdc/__init__.py @@ -0,0 +1 @@ +DOMAIN = "pzemdc" diff --git a/esphome/components/qmc5883l/__init__.py b/esphome/components/qmc5883l/__init__.py index e69de29bb2..fb646a60c3 100644 --- a/esphome/components/qmc5883l/__init__.py +++ b/esphome/components/qmc5883l/__init__.py @@ -0,0 +1 @@ +DOMAIN = "qmc5883l" diff --git a/esphome/components/qmi8658/__init__.py b/esphome/components/qmi8658/__init__.py index 67838dbc3c..6c871dfc4e 100644 --- a/esphome/components/qmi8658/__init__.py +++ b/esphome/components/qmi8658/__init__.py @@ -4,6 +4,7 @@ from esphome.components.motion import MotionComponent CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["i2c", "motion"] +DOMAIN = "qmi8658" CONF_QMI8658_ID = "qmi8658_id" # C++ namespace / class diff --git a/esphome/components/qmp6988/__init__.py b/esphome/components/qmp6988/__init__.py index 09bcf51589..86498d192a 100644 --- a/esphome/components/qmp6988/__init__.py +++ b/esphome/components/qmp6988/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@andrewpc"] +DOMAIN = "qmp6988" diff --git a/esphome/components/qr_code/__init__.py b/esphome/components/qr_code/__init__.py index 8b773dcdf5..222041837b 100644 --- a/esphome/components/qr_code/__init__.py +++ b/esphome/components/qr_code/__init__.py @@ -7,6 +7,7 @@ CONF_SCALE = "scale" CONF_ECC = "ecc" CODEOWNERS = ["@wjtje"] +DOMAIN = "qr_code" DEPENDENCIES = ["display"] MULTI_CONF = True diff --git a/esphome/components/qspi_amoled/display.py b/esphome/components/qspi_amoled/display.py deleted file mode 100644 index 00773b516a..0000000000 --- a/esphome/components/qspi_amoled/display.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("The qspi_amoled component has been renamed to qspi_dbi") diff --git a/esphome/components/qspi_dbi/__init__.py b/esphome/components/qspi_dbi/__init__.py index aebbe2fcc8..abf64f675a 100644 --- a/esphome/components/qspi_dbi/__init__.py +++ b/esphome/components/qspi_dbi/__init__.py @@ -1,4 +1,5 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "qspi_dbi" CONF_DRAW_FROM_ORIGIN = "draw_from_origin" diff --git a/esphome/components/qwiic_pir/__init__.py b/esphome/components/qwiic_pir/__init__.py index e69de29bb2..2232f1e670 100644 --- a/esphome/components/qwiic_pir/__init__.py +++ b/esphome/components/qwiic_pir/__init__.py @@ -0,0 +1 @@ +DOMAIN = "qwiic_pir" diff --git a/esphome/components/radio_frequency/__init__.py b/esphome/components/radio_frequency/__init__.py index ebba97d1a3..cd680450d5 100644 --- a/esphome/components/radio_frequency/__init__.py +++ b/esphome/components/radio_frequency/__init__.py @@ -20,6 +20,7 @@ from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@kbx81"] AUTO_LOAD = ["ir_rf_base"] +DOMAIN = "radio_frequency" IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/radon_eye_ble/__init__.py b/esphome/components/radon_eye_ble/__init__.py index 0ca9f64326..e2af976399 100644 --- a/esphome/components/radon_eye_ble/__init__.py +++ b/esphome/components/radon_eye_ble/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["ble_device_base"] CODEOWNERS = ["@jeffeb3"] +DOMAIN = "radon_eye_ble" radon_eye_ble_ns = cg.esphome_ns.namespace("radon_eye_ble") RadonEyeListener = radon_eye_ble_ns.class_( diff --git a/esphome/components/radon_eye_rd200/__init__.py b/esphome/components/radon_eye_rd200/__init__.py index 0740f6967b..a2076ef945 100644 --- a/esphome/components/radon_eye_rd200/__init__.py +++ b/esphome/components/radon_eye_rd200/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@jeffeb3"] +DOMAIN = "radon_eye_rd200" diff --git a/esphome/components/rc522/__init__.py b/esphome/components/rc522/__init__.py index e9e8dd7b73..69cc713813 100644 --- a/esphome/components/rc522/__init__.py +++ b/esphome/components/rc522/__init__.py @@ -13,6 +13,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@glmnet"] AUTO_LOAD = ["binary_sensor"] +DOMAIN = "rc522" CONF_RC522_ID = "rc522_id" diff --git a/esphome/components/rc522_i2c/__init__.py b/esphome/components/rc522_i2c/__init__.py index c3fd368bbd..3fa9ef8a7b 100644 --- a/esphome/components/rc522_i2c/__init__.py +++ b/esphome/components/rc522_i2c/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@glmnet"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["rc522"] +DOMAIN = "rc522_i2c" MULTI_CONF = True rc522_i2c_ns = cg.esphome_ns.namespace("rc522_i2c") diff --git a/esphome/components/rc522_spi/__init__.py b/esphome/components/rc522_spi/__init__.py index f2820bbaee..6f517458bb 100644 --- a/esphome/components/rc522_spi/__init__.py +++ b/esphome/components/rc522_spi/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@glmnet"] DEPENDENCIES = ["spi"] AUTO_LOAD = ["rc522"] +DOMAIN = "rc522_spi" MULTI_CONF = True rc522_spi_ns = cg.esphome_ns.namespace("rc522_spi") diff --git a/esphome/components/rd03d/__init__.py b/esphome/components/rd03d/__init__.py index 4fff41e4f6..96bf4ffd74 100644 --- a/esphome/components/rd03d/__init__.py +++ b/esphome/components/rd03d/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@jasstrong"] DEPENDENCIES = ["uart"] +DOMAIN = "rd03d" MULTI_CONF = True CONF_RD03D_ID = "rd03d_id" diff --git a/esphome/components/rdm6300/__init__.py b/esphome/components/rdm6300/__init__.py index d3803bd3f7..86eb2cd738 100644 --- a/esphome/components/rdm6300/__init__.py +++ b/esphome/components/rdm6300/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor"] +DOMAIN = "rdm6300" MULTI_CONF = True rdm6300_ns = cg.esphome_ns.namespace("rdm6300") diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index e9708d0a9f..a880169fb2 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -51,6 +51,7 @@ from esphome.types import ConfigType, TemplateArgsType from esphome.util import Registry, SimpleRegistry AUTO_LOAD = ["binary_sensor"] +DOMAIN = "remote_base" CONF_RECEIVER_ID = "receiver_id" @@ -1018,6 +1019,43 @@ async def nec_action(var, config, args): cg.add(var.set_command_repeats(template_)) +# Onkyo RI +OnkyoRIData, OnkyoRIBinarySensor, OnkyoRITrigger, OnkyoRIAction, OnkyoRIDumper = ( + declare_protocol("OnkyoRI") +) +ONKYORI_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_int_range(0, 0xFFF)}) + + +@register_binary_sensor("onkyori", OnkyoRIBinarySensor, ONKYORI_SCHEMA) +def onkyori_binary_sensor(var: MockObj, config: ConfigType) -> None: + cg.add( + var.set_data( + cg.StructInitializer( + OnkyoRIData, + ("data", config[CONF_DATA]), + ) + ) + ) + + +@register_trigger("onkyori", OnkyoRITrigger, OnkyoRIData) +def onkyori_trigger(var: MockObj, config: ConfigType) -> None: + """The trigger takes no options beyond the automation.""" + + +@register_dumper("onkyori", OnkyoRIDumper) +def onkyori_dumper(var: MockObj, config: ConfigType) -> None: + """The dumper takes no options.""" + + +@register_action("onkyori", OnkyoRIAction, ONKYORI_SCHEMA) +async def onkyori_action( + var: MockObj, config: ConfigType, args: TemplateArgsType +) -> None: + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint16) + cg.add(var.set_data(template_)) + + # Pioneer ( PioneerData, @@ -2060,11 +2098,20 @@ def midea_dumper(var, config): pass +async def _code_bytes( + value: Any, args: TemplateArgsType, set_template: MockObj, set_static: MockObj +) -> None: + """Constant codes share one PROGMEM table pool across the remote protocols.""" + await automation.templatable_bytes( + value, args, set_template, set_static, "remote_base_code" + ) + + @register_action("midea", MideaAction, MIDEA_SCHEMA) async def midea_action(var, config, args): - vec_ = cg.std_vector.template(cg.uint8) - template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_) - cg.add(var.set_code(template_)) + await _code_bytes( + config[CONF_CODE], args, var.set_code_template, var.set_code_static + ) # AEHA @@ -2116,10 +2163,9 @@ def aeha_dumper(var, config): async def aeha_action(var, config, args): template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) cg.add(var.set_address(template_)) - template_ = await cg.templatable( - config[CONF_DATA], args, cg.std_vector.template(cg.uint8) + await _code_bytes( + config[CONF_DATA], args, var.set_data_template, var.set_data_static ) - cg.add(var.set_data(template_)) templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32) cg.add(var.set_carrier_frequency(templ)) @@ -2215,9 +2261,9 @@ def haier_dumper(var, config): @register_action("haier", HaierAction, HAIER_SCHEMA) async def haier_action(var, config, args): - vec_ = cg.std_vector.template(cg.uint8) - template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_) - cg.add(var.set_code(template_)) + await _code_bytes( + config[CONF_CODE], args, var.set_code_template, var.set_code_static + ) # ABBWelcome @@ -2358,9 +2404,9 @@ def mirage_dumper(var, config): @register_action("mirage", MirageAction, MIRAGE_SCHEMA) async def mirage_action(var, config, args): - vec_ = cg.std_vector.template(cg.uint8) - template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_) - cg.add(var.set_code(template_)) + await _code_bytes( + config[CONF_CODE], args, var.set_code_template, var.set_code_static + ) # Toto diff --git a/esphome/components/remote_base/aeha_protocol.h b/esphome/components/remote_base/aeha_protocol.h index 98a5501155..57706a7e4f 100644 --- a/esphome/components/remote_base/aeha_protocol.h +++ b/esphome/components/remote_base/aeha_protocol.h @@ -28,10 +28,9 @@ DECLARE_REMOTE_PROTOCOL(AEHA) template class AEHAAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint16_t, address) - TEMPLATABLE_VALUE(std::vector, data) + TEMPLATABLE_BYTES(data) TEMPLATABLE_VALUE(uint32_t, carrier_frequency); - void set_data(const std::vector &data) { data_ = data; } void encode(RemoteTransmitData *dst, Ts... x) override { AEHAData data{}; data.address = this->address_.value(x...); diff --git a/esphome/components/remote_base/haier_protocol.h b/esphome/components/remote_base/haier_protocol.h index e1fd60411f..90af88d2c7 100644 --- a/esphome/components/remote_base/haier_protocol.h +++ b/esphome/components/remote_base/haier_protocol.h @@ -25,7 +25,7 @@ DECLARE_REMOTE_PROTOCOL(Haier) template class HaierAction : public RemoteTransmitterActionBase { public: - TEMPLATABLE_VALUE(std::vector, code) + TEMPLATABLE_BYTES(code) void encode(RemoteTransmitData *dst, Ts... x) override { HaierData data{}; diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 85bbef1cb1..19521e8f6d 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -4,6 +4,7 @@ #include #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "remote_base.h" @@ -21,6 +22,9 @@ class MideaData { MideaData(const std::vector &data) { std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } + MideaData(const uint8_t *data, size_t len) { + progmem_memcpy(this->data_.data(), data, std::min(len, this->data_.size())); + } uint8_t *data() { return this->data_.data(); } const uint8_t *data() const { return this->data_.data(); } @@ -75,10 +79,12 @@ class MideaProtocol : public RemoteProtocol { DECLARE_REMOTE_PROTOCOL(Midea) template class MideaAction : public RemoteTransmitterActionBase { - TEMPLATABLE_VALUE(std::vector, code) + public: + TEMPLATABLE_BYTES(code) void encode(RemoteTransmitData *dst, Ts... x) override { - MideaData data(this->code_.value(x...)); + MideaData data = this->code_.is_static() ? MideaData(this->code_.data(), this->code_.size()) + : MideaData(this->code_.value(x...)); data.finalize(); MideaProtocol().encode(dst, data); } diff --git a/esphome/components/remote_base/mirage_protocol.h b/esphome/components/remote_base/mirage_protocol.h index a37fb93f4f..958127ba66 100644 --- a/esphome/components/remote_base/mirage_protocol.h +++ b/esphome/components/remote_base/mirage_protocol.h @@ -25,7 +25,7 @@ DECLARE_REMOTE_PROTOCOL(Mirage) template class MirageAction : public RemoteTransmitterActionBase { public: - TEMPLATABLE_VALUE(std::vector, code) + TEMPLATABLE_BYTES(code) void encode(RemoteTransmitData *dst, Ts... x) override { MirageData data{}; diff --git a/esphome/components/remote_base/onkyori_protocol.cpp b/esphome/components/remote_base/onkyori_protocol.cpp new file mode 100644 index 0000000000..6290e5bc50 --- /dev/null +++ b/esphome/components/remote_base/onkyori_protocol.cpp @@ -0,0 +1,59 @@ +#include "onkyori_protocol.h" +#include "esphome/core/log.h" + +namespace esphome::remote_base { + +ESPHOME_LOG_TAG(TAG, "remote.onkyori"); + +static constexpr uint8_t NBITS = 12; +static constexpr uint16_t MAX_DATA = (1U << NBITS) - 1; +static constexpr uint32_t HEADER_HIGH_US = 3000; +static constexpr uint32_t HEADER_LOW_US = 1000; +static constexpr uint32_t BIT_HIGH_US = 1000; +static constexpr uint32_t BIT_ONE_LOW_US = 2000; +static constexpr uint32_t BIT_ZERO_LOW_US = 1000; +static constexpr uint32_t TRAILER_HIGH_US = 1000; + +void OnkyoRIProtocol::encode(RemoteTransmitData *dst, const OnkyoRIData &data) { + if (data.data > MAX_DATA) { + ESP_LOGE(TAG, "Data 0x%04X does not fit in a 12-bit RI frame, not sending", data.data); + return; + } + + // RI is a baseband signal on a wire, so it has no carrier to modulate. + dst->set_carrier_frequency(0); + dst->reserve(2 + NBITS * 2u + 1); + + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + + for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { + dst->item(BIT_HIGH_US, (data.data & mask) != 0 ? BIT_ONE_LOW_US : BIT_ZERO_LOW_US); + } + + dst->mark(TRAILER_HIGH_US); +} + +optional OnkyoRIProtocol::decode(RemoteReceiveData src) { + OnkyoRIData out{.data = 0}; + + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + for (uint8_t i = 0; i < NBITS; i++) { + out.data <<= 1; + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + out.data |= 1; + } else if (!src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + return {}; + } + } + + if (!src.expect_mark(TRAILER_HIGH_US)) + return {}; + + return out; +} + +void OnkyoRIProtocol::dump(const OnkyoRIData &data) { ESP_LOGI(TAG, "Received OnkyoRI: data=0x%03X", data.data); } + +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/onkyori_protocol.h b/esphome/components/remote_base/onkyori_protocol.h new file mode 100644 index 0000000000..c6f0f1dd97 --- /dev/null +++ b/esphome/components/remote_base/onkyori_protocol.h @@ -0,0 +1,33 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome::remote_base { + +struct OnkyoRIData { + uint16_t data; + + bool operator==(const OnkyoRIData &rhs) const { return data == rhs.data; } +}; + +class OnkyoRIProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const OnkyoRIData &data); + optional decode(RemoteReceiveData src); + void dump(const OnkyoRIData &data); +}; + +DECLARE_REMOTE_PROTOCOL(OnkyoRI) + +template class OnkyoRIAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, data) + + void encode(RemoteTransmitData *dst, Ts... x) override { + OnkyoRIData data{}; + data.data = this->data_.value(x...); + OnkyoRIProtocol().encode(dst, data); + } +}; + +} // namespace esphome::remote_base diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 866e108131..481acaa06c 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -29,6 +29,7 @@ CONF_FILTER_SYMBOLS = "filter_symbols" CONF_RECEIVE_SYMBOLS = "receive_symbols" AUTO_LOAD = ["remote_base"] +DOMAIN = "remote_receiver" remote_receiver_ns = cg.esphome_ns.namespace("remote_receiver") remote_base_ns = cg.esphome_ns.namespace("remote_base") diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 68eac315d5..7553ed767b 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -26,6 +26,7 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["remote_base"] +DOMAIN = "remote_transmitter" CONF_EOT_LEVEL = "eot_level" CONF_NON_BLOCKING = "non_blocking" diff --git a/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp index f6550d107c..22a63ca4fd 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp @@ -11,6 +11,8 @@ namespace esphome::remote_transmitter { ESPHOME_LOG_TAG(TAG, "remote_transmitter"); +static constexpr uint32_t COMPLETE_TIMEOUT_ID = 0; + // Maximum RMT symbol duration (15-bit field) static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF; @@ -206,7 +208,7 @@ void RemoteTransmitterComponent::wait_for_rmt_() { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) void RemoteTransmitterComponent::flush_pending_completion() { // a frame still on the wire is waited out, and its completion reported, before the next one - if (this->non_blocking_ && this->cancel_timeout("complete")) { + if (this->non_blocking_ && this->cancel_timeout(COMPLETE_TIMEOUT_ID)) { this->wait_for_rmt_(); } } @@ -292,7 +294,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->status_clear_warning(); if (this->non_blocking_) { - this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); }); + this->set_timeout(COMPLETE_TIMEOUT_ID, total_duration / 1000, [this]() { this->wait_for_rmt_(); }); } else { this->wait_for_rmt_(); } diff --git a/esphome/components/resampler/__init__.py b/esphome/components/resampler/__init__.py index b9b070e015..e41b004b86 100644 --- a/esphome/components/resampler/__init__.py +++ b/esphome/components/resampler/__init__.py @@ -3,6 +3,8 @@ from typing import Any import esphome.codegen as cg import esphome.config_validation as cv +DOMAIN = "resampler" + resampler_ns = cg.esphome_ns.namespace("resampler") CONF_TAPS = "taps" diff --git a/esphome/components/resistance/__init__.py b/esphome/components/resistance/__init__.py index e69de29bb2..5bba63cc1b 100644 --- a/esphome/components/resistance/__init__.py +++ b/esphome/components/resistance/__init__.py @@ -0,0 +1 @@ +DOMAIN = "resistance" diff --git a/esphome/components/restart/__init__.py b/esphome/components/restart/__init__.py index f70ffa9520..6337520ebd 100644 --- a/esphome/components/restart/__init__.py +++ b/esphome/components/restart/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@esphome/core"] +DOMAIN = "restart" diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index fed8365e9e..e967335c18 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( DEPENDENCIES = ["uart"] CODEOWNERS = ["@jesserockz"] +DOMAIN = "rf_bridge" rf_bridge_ns = cg.esphome_ns.namespace("rf_bridge") RFBridgeComponent = rf_bridge_ns.class_( diff --git a/esphome/components/rgb/__init__.py b/esphome/components/rgb/__init__.py index e69de29bb2..11e05410be 100644 --- a/esphome/components/rgb/__init__.py +++ b/esphome/components/rgb/__init__.py @@ -0,0 +1 @@ +DOMAIN = "rgb" diff --git a/esphome/components/rgbct/__init__.py b/esphome/components/rgbct/__init__.py index e69de29bb2..b8a02914c5 100644 --- a/esphome/components/rgbct/__init__.py +++ b/esphome/components/rgbct/__init__.py @@ -0,0 +1 @@ +DOMAIN = "rgbct" diff --git a/esphome/components/rgbw/__init__.py b/esphome/components/rgbw/__init__.py index e69de29bb2..0635bc36c3 100644 --- a/esphome/components/rgbw/__init__.py +++ b/esphome/components/rgbw/__init__.py @@ -0,0 +1 @@ +DOMAIN = "rgbw" diff --git a/esphome/components/rgbww/__init__.py b/esphome/components/rgbww/__init__.py index e69de29bb2..ce95d11431 100644 --- a/esphome/components/rgbww/__init__.py +++ b/esphome/components/rgbww/__init__.py @@ -0,0 +1 @@ +DOMAIN = "rgbww" diff --git a/esphome/components/ring_buffer/__init__.py b/esphome/components/ring_buffer/__init__.py index b53476dcac..d053a7e409 100644 --- a/esphome/components/ring_buffer/__init__.py +++ b/esphome/components/ring_buffer/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg CODEOWNERS = ["@kahrendt"] DEPENDENCIES = ["esp32"] +DOMAIN = "ring_buffer" ring_buffer_ns = cg.esphome_ns.namespace("ring_buffer") RingBuffer = ring_buffer_ns.class_("RingBuffer") diff --git a/esphome/components/rotary_encoder/__init__.py b/esphome/components/rotary_encoder/__init__.py index e69de29bb2..5f448e531c 100644 --- a/esphome/components/rotary_encoder/__init__.py +++ b/esphome/components/rotary_encoder/__init__.py @@ -0,0 +1 @@ +DOMAIN = "rotary_encoder" diff --git a/esphome/components/router/__init__.py b/esphome/components/router/__init__.py index e69de29bb2..49a5eec426 100644 --- a/esphome/components/router/__init__.py +++ b/esphome/components/router/__init__.py @@ -0,0 +1 @@ +DOMAIN = "router" diff --git a/esphome/components/rp2/__init__.py b/esphome/components/rp2/__init__.py index 23346ca6f4..76dd240f7a 100644 --- a/esphome/components/rp2/__init__.py +++ b/esphome/components/rp2/__init__.py @@ -57,6 +57,7 @@ from .gpio import rp2_pin_to_code # noqa: F401 _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jesserockz"] AUTO_LOAD = ["preferences"] +DOMAIN = "rp2" IS_TARGET_PLATFORM = True # Legacy top-level YAML keys that route here. The framework diff --git a/esphome/components/rp2/gpio.cpp b/esphome/components/rp2/gpio.cpp index 93bdba1fca..2eeb460e95 100644 --- a/esphome/components/rp2/gpio.cpp +++ b/esphome/components/rp2/gpio.cpp @@ -1,6 +1,7 @@ #ifdef USE_RP2 #include "gpio.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { @@ -40,7 +41,7 @@ ISRInternalGPIOPin RP2GPIOPin::to_isr() const { return ISRInternalGPIOPin((void *) arg); } -void RP2GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { +void RP2GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const { PinStatus arduino_mode = LOW; switch (type) { case gpio::INTERRUPT_RISING_EDGE: diff --git a/esphome/components/rp2/gpio.h b/esphome/components/rp2/gpio.h index 538fef619a..b1d4c9a8f6 100644 --- a/esphome/components/rp2/gpio.h +++ b/esphome/components/rp2/gpio.h @@ -3,11 +3,11 @@ #ifdef USE_RP2 #include -#include "esphome/core/hal.h" +#include "esphome/core/gpio_pin.h" namespace esphome::rp2 { -class RP2GPIOPin final : public InternalGPIOPin { +class RP2GPIOPin final : public GPIOPin { public: void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } @@ -18,14 +18,18 @@ class RP2GPIOPin final : public InternalGPIOPin { bool digital_read() override; void digital_write(bool value) override; size_t dump_summary(char *buffer, size_t len) const override; - void detach_interrupt() const override; - ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return pin_; } + void detach_interrupt() const; + template void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const { + this->attach_interrupt_(reinterpret_cast(func), arg, type); + } + ISRInternalGPIOPin to_isr() const; + uint8_t get_pin() const { return pin_; } gpio::Flags get_flags() const override { return flags_; } - bool is_inverted() const override { return inverted_; } + bool is_inverted() const { return inverted_; } + bool is_internal() override { return true; } protected: - void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const; uint8_t pin_; bool inverted_{}; diff --git a/esphome/components/rp2/hal.h b/esphome/components/rp2/hal.h index ec46937bab..2a538f1571 100644 --- a/esphome/components/rp2/hal.h +++ b/esphome/components/rp2/hal.h @@ -4,6 +4,8 @@ #include +#include + #include "esphome/core/time_conversion.h" #define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) @@ -40,6 +42,9 @@ __attribute__((always_inline)) inline bool in_isr_context() { return ipsr != 0; } +/// arduino-pico runs setup() and loop() on core 0; ESPHome never uses core 1. +__attribute__((always_inline)) inline bool is_main_loop_thread() { return !in_isr_context() && get_core_num() == 0; } + __attribute__((always_inline)) inline void yield() { ::yield(); } __attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } __attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py index d2a08e9fc0..ad79551792 100644 --- a/esphome/components/rp2040_ble/__init__.py +++ b/esphome/components/rp2040_ble/__init__.py @@ -8,10 +8,10 @@ from esphome.types import ConfigType DEPENDENCIES = ["rp2"] CODEOWNERS = ["@bdraco"] +DOMAIN = "rp2040_ble" CONF_RP2040_BLE_ID = "rp2040_ble_id" -KEY_RP2040_BLE = "rp2040_ble" KEY_USED_CONNECTION_SLOTS = "used_connection_slots" # Hard platform cap on concurrent GATT connections: the BTstack pool overrides @@ -49,7 +49,7 @@ def consume_connection_slots( the total is checked against MAX_CONNECTIONS in final validation.""" def _consume_connection_slots(config: MutableMapping) -> MutableMapping: - data: dict = CORE.data.setdefault(KEY_RP2040_BLE, {}) + data: dict = CORE.data.setdefault(DOMAIN, {}) slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) slots.extend([consumer] * value) return config @@ -62,7 +62,7 @@ def validate_connection_slots() -> None: # Skip in testing mode to allow component grouping (esp32_ble parity). if CORE.testing_mode: return - used = CORE.data.get(KEY_RP2040_BLE, {}).get(KEY_USED_CONNECTION_SLOTS, []) + used = CORE.data.get(DOMAIN, {}).get(KEY_USED_CONNECTION_SLOTS, []) if len(used) > MAX_CONNECTIONS: raise cv.Invalid( f"BLE components require {len(used)} connection slots but the " diff --git a/esphome/components/rp2040_pio_led_strip/__init__.py b/esphome/components/rp2040_pio_led_strip/__init__.py index 4c9aa2d155..10b24efa09 100644 --- a/esphome/components/rp2040_pio_led_strip/__init__.py +++ b/esphome/components/rp2040_pio_led_strip/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Papa-DMan"] +DOMAIN = "rp2040_pio_led_strip" diff --git a/esphome/components/rp2040_pwm/__init__.py b/esphome/components/rp2040_pwm/__init__.py index e69de29bb2..3abe0b8c83 100644 --- a/esphome/components/rp2040_pwm/__init__.py +++ b/esphome/components/rp2040_pwm/__init__.py @@ -0,0 +1 @@ +DOMAIN = "rp2040_pwm" diff --git a/esphome/components/rp2_ble_tracker/__init__.py b/esphome/components/rp2_ble_tracker/__init__.py index 12857a556f..4a3d3f788b 100644 --- a/esphome/components/rp2_ble_tracker/__init__.py +++ b/esphome/components/rp2_ble_tracker/__init__.py @@ -33,6 +33,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["rp2"] AUTO_LOAD = ["ble_device_base", "rp2040_ble"] CODEOWNERS = ["@bdraco"] +DOMAIN = "rp2_ble_tracker" ble_device_base.register_hub_provider("rp2_ble_tracker") diff --git a/esphome/components/rp2_pio/__init__.py b/esphome/components/rp2_pio/__init__.py index 915ab73b87..1b2236ae8e 100644 --- a/esphome/components/rp2_pio/__init__.py +++ b/esphome/components/rp2_pio/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.types import ConfigType DEPENDENCIES = ["rp2"] +DOMAIN = "rp2_pio" PIOASM_REPO_VERSION = "1.5.0-b" diff --git a/esphome/components/rpi_dpi_rgb/__init__.py b/esphome/components/rpi_dpi_rgb/__init__.py index 8628f0e063..20629fd730 100644 --- a/esphome/components/rpi_dpi_rgb/__init__.py +++ b/esphome/components/rpi_dpi_rgb/__init__.py @@ -1,4 +1,5 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "rpi_dpi_rgb" DEPRECATED_COMPONENT = """ The 'rpi_dpi_rgb' component is deprecated and no new models will be added to it. diff --git a/esphome/components/rtl87xx/__init__.py b/esphome/components/rtl87xx/__init__.py index 88928b2bce..d3d5150819 100644 --- a/esphome/components/rtl87xx/__init__.py +++ b/esphome/components/rtl87xx/__init__.py @@ -35,6 +35,7 @@ from .boards import RTL87XX_BOARD_PINS, RTL87XX_BOARDS CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["libretiny"] +DOMAIN = "rtl87xx" IS_TARGET_PLATFORM = True COMPONENT_DATA = LibreTinyComponent( diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 4f4d17b441..47b8f26e3f 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -12,6 +12,7 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@glmnet", "@ximex"] +DOMAIN = "rtttl" CONF_RTTTL = "rtttl" CONF_ON_FINISHED_PLAYBACK = "on_finished_playback" diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index e27fcadbee..fcf5cb770c 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -17,6 +17,7 @@ from esphome.core import CORE AUTO_LOAD = ["image"] CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"] +DOMAIN = "runtime_image" CONF_PLACEHOLDER = "placeholder" CONF_TRANSPARENCY = "transparency" diff --git a/esphome/components/runtime_stats/__init__.py b/esphome/components/runtime_stats/__init__.py index 62f5f46cb4..2d1a6ac910 100644 --- a/esphome/components/runtime_stats/__init__.py +++ b/esphome/components/runtime_stats/__init__.py @@ -8,6 +8,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@bdraco"] +DOMAIN = "runtime_stats" CONF_LOG_INTERVAL = "log_interval" diff --git a/esphome/components/ruuvi_ble/__init__.py b/esphome/components/ruuvi_ble/__init__.py index ac4bafbb1a..ccfa8ecf10 100644 --- a/esphome/components/ruuvi_ble/__init__.py +++ b/esphome/components/ruuvi_ble/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType AUTO_LOAD = ["ble_device_base"] +DOMAIN = "ruuvi_ble" ruuvi_ble_ns = cg.esphome_ns.namespace("ruuvi_ble") RuuviListener = ruuvi_ble_ns.class_( diff --git a/esphome/components/ruuvitag/__init__.py b/esphome/components/ruuvitag/__init__.py index e69de29bb2..2a0e78efc8 100644 --- a/esphome/components/ruuvitag/__init__.py +++ b/esphome/components/ruuvitag/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ruuvitag" diff --git a/esphome/components/rx8025t/__init__.py b/esphome/components/rx8025t/__init__.py index e69de29bb2..e0b7464656 100644 --- a/esphome/components/rx8025t/__init__.py +++ b/esphome/components/rx8025t/__init__.py @@ -0,0 +1 @@ +DOMAIN = "rx8025t" diff --git a/esphome/components/rx8130/__init__.py b/esphome/components/rx8130/__init__.py index e69de29bb2..3b5225c024 100644 --- a/esphome/components/rx8130/__init__.py +++ b/esphome/components/rx8130/__init__.py @@ -0,0 +1 @@ +DOMAIN = "rx8130" diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index 5d03ed70a8..8d77ff363f 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_NUM_ATTEMPTS, CONF_REBOOT_TIMEOUT, - CONF_SAFE_MODE, CONF_STORAGE, KEY_PAST_SAFE_MODE, ) @@ -15,6 +14,7 @@ from esphome.cpp_generator import RawExpression from esphome.types import ConfigType CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] +DOMAIN = "safe_mode" CONF_BOOT_IS_GOOD_AFTER = "boot_is_good_after" CONF_BOOT_IS_GOOD_ON_SHUTDOWN = "boot_is_good_on_shutdown" @@ -92,5 +92,5 @@ async def to_code(config: ConfigType) -> None: ) cg.add(RawExpression(f"if ({condition}) return")) - CORE.data[CONF_SAFE_MODE] = {} - CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True + CORE.data[DOMAIN] = {} + CORE.data[DOMAIN][KEY_PAST_SAFE_MODE] = True diff --git a/esphome/components/scd30/__init__.py b/esphome/components/scd30/__init__.py index e69de29bb2..71e20c7e13 100644 --- a/esphome/components/scd30/__init__.py +++ b/esphome/components/scd30/__init__.py @@ -0,0 +1 @@ +DOMAIN = "scd30" diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 3e8794e65c..5802503627 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -10,6 +10,8 @@ namespace esphome::scd30 { ESPHOME_LOG_TAG(TAG, "scd30"); +static constexpr uint32_t STATUS_CHECK_INTERVAL_ID = 0; + static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100; static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102; @@ -107,7 +109,7 @@ void SCD30Component::setup() { } // check each 500ms if data is ready, and read it in that case - this->set_interval("status-check", 500, [this]() { + this->set_interval(STATUS_CHECK_INTERVAL_ID, 500, [this]() { if (this->is_data_ready_()) this->update(); }); diff --git a/esphome/components/scd4x/__init__.py b/esphome/components/scd4x/__init__.py index e69de29bb2..338bfdf59b 100644 --- a/esphome/components/scd4x/__init__.py +++ b/esphome/components/scd4x/__init__.py @@ -0,0 +1 @@ +DOMAIN = "scd4x" diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 2c79e9ed0a..48a208b7f7 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_MODE, CONF_PARAMETERS, CONF_RESTART from esphome.core import CORE, EsphomeError CODEOWNERS = ["@esphome/core"] +DOMAIN = "script" script_ns = cg.esphome_ns.namespace("script") Script = script_ns.class_("Script", automation.Trigger.template()) ScriptExecuteAction = script_ns.class_("ScriptExecuteAction", automation.Action) @@ -15,7 +16,6 @@ RestartScript = script_ns.class_("RestartScript", Script) QueueingScript = script_ns.class_("QueueingScript", Script, cg.Component) ParallelScript = script_ns.class_("ParallelScript", Script) -CONF_SCRIPT = "script" CONF_SINGLE = "single" CONF_QUEUED = "queued" CONF_PARALLEL = "parallel" @@ -35,7 +35,7 @@ PARAMETER_TYPE_TRANSLATIONS = { def get_script(script_id): - scripts = CORE.config.get(CONF_SCRIPT, {}) + scripts = CORE.config.get(DOMAIN, {}) for script in scripts: if script.get(CONF_ID, None) == script_id: return script diff --git a/esphome/components/sdl/__init__.py b/esphome/components/sdl/__init__.py index 872d831850..a8a2391a3c 100644 --- a/esphome/components/sdl/__init__.py +++ b/esphome/components/sdl/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg CODEOWNERS = ["@clydebarrow"] +DOMAIN = "sdl" SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode") diff --git a/esphome/components/sdm_meter/__init__.py b/esphome/components/sdm_meter/__init__.py index e69de29bb2..b13d6e18b6 100644 --- a/esphome/components/sdm_meter/__init__.py +++ b/esphome/components/sdm_meter/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sdm_meter" diff --git a/esphome/components/sdp3x/__init__.py b/esphome/components/sdp3x/__init__.py index e69de29bb2..50ffb263bf 100644 --- a/esphome/components/sdp3x/__init__.py +++ b/esphome/components/sdp3x/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sdp3x" diff --git a/esphome/components/sds011/__init__.py b/esphome/components/sds011/__init__.py index e69de29bb2..386bc61be5 100644 --- a/esphome/components/sds011/__init__.py +++ b/esphome/components/sds011/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sds011" diff --git a/esphome/components/seeed_mr24hpc1/__init__.py b/esphome/components/seeed_mr24hpc1/__init__.py index 56630f18f4..4ec01e46c6 100644 --- a/esphome/components/seeed_mr24hpc1/__init__.py +++ b/esphome/components/seeed_mr24hpc1/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["uart"] # is the code owner of the relevant code base CODEOWNERS = ["@limengdu"] +DOMAIN = "seeed_mr24hpc1" # The current component or platform can be configured or defined multiple times in the same configuration file. MULTI_CONF = True diff --git a/esphome/components/seeed_mr60bha2/__init__.py b/esphome/components/seeed_mr60bha2/__init__.py index 6bf8657af9..7a85567510 100644 --- a/esphome/components/seeed_mr60bha2/__init__.py +++ b/esphome/components/seeed_mr60bha2/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@limengdu"] DEPENDENCIES = ["uart"] +DOMAIN = "seeed_mr60bha2" MULTI_CONF = True mr60bha2_ns = cg.esphome_ns.namespace("seeed_mr60bha2") diff --git a/esphome/components/seeed_mr60fda2/__init__.py b/esphome/components/seeed_mr60fda2/__init__.py index 159a1ece9c..cc22741583 100644 --- a/esphome/components/seeed_mr60fda2/__init__.py +++ b/esphome/components/seeed_mr60fda2/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@limengdu"] DEPENDENCIES = ["uart"] +DOMAIN = "seeed_mr60fda2" MULTI_CONF = True mr60fda2_ns = cg.esphome_ns.namespace("seeed_mr60fda2") diff --git a/esphome/components/selec_meter/__init__.py b/esphome/components/selec_meter/__init__.py index e69de29bb2..469119b8ac 100644 --- a/esphome/components/selec_meter/__init__.py +++ b/esphome/components/selec_meter/__init__.py @@ -0,0 +1 @@ +DOMAIN = "selec_meter" diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index fa266fc81f..07f86c1330 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -30,6 +30,7 @@ from esphome.cpp_types import global_ns from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "select" IS_PLATFORM_COMPONENT = True select_ns = cg.esphome_ns.namespace("select") diff --git a/esphome/components/sen0321/__init__.py b/esphome/components/sen0321/__init__.py index 458ffa67f9..905618bfba 100644 --- a/esphome/components/sen0321/__init__.py +++ b/esphome/components/sen0321/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@notjj"] +DOMAIN = "sen0321" diff --git a/esphome/components/sen21231/__init__.py b/esphome/components/sen21231/__init__.py index e69de29bb2..c2802220f6 100644 --- a/esphome/components/sen21231/__init__.py +++ b/esphome/components/sen21231/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sen21231" diff --git a/esphome/components/sen5x/__init__.py b/esphome/components/sen5x/__init__.py index e69de29bb2..e58c474fae 100644 --- a/esphome/components/sen5x/__init__.py +++ b/esphome/components/sen5x/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sen5x" diff --git a/esphome/components/sen6x/__init__.py b/esphome/components/sen6x/__init__.py index e69de29bb2..abd5e7e50c 100644 --- a/esphome/components/sen6x/__init__.py +++ b/esphome/components/sen6x/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sen6x" diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 5e104fe58a..c557a7960d 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -5,7 +5,8 @@ from typing import Any from esphome import automation import esphome.codegen as cg from esphome.components import esp32, mdns, network, psram, socket, wifi -from esphome.components.const import CONF_MANUFACTURER +from esphome.components.const import CONF_ENABLED, CONF_MANUFACTURER +from esphome.components.mdns import DOMAIN as MDNS_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -13,7 +14,6 @@ from esphome.const import ( CONF_FORMAT, CONF_HEIGHT, CONF_ID, - CONF_MDNS, CONF_MODEL, CONF_NAME, CONF_PROJECT, @@ -175,6 +175,7 @@ class SendspinConfiguration: player_support: bool = False visualizer_support: bool = False pairing_code_display_support: bool = False + switch_types: set[str] = field(default_factory=set) artwork_preferences: list[ConfigType] = field(default_factory=list) player_config: ConfigType | None = None @@ -216,6 +217,11 @@ def request_pairing_code_display_support() -> None: _get_data().pairing_code_display_support = True +def request_switch(switch_type: str) -> None: + """Mark that a sendspin switch of this type drives the matching hub setting.""" + _get_data().switch_types.add(switch_type) + + def register_artwork_preference(config: ConfigType) -> int: """Register an artwork slot preference and return the slot it was given. @@ -275,6 +281,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_STATIC_PAIRING_CODE): cv.sensitive( _validate_static_pairing_code ), + # No default: the unpaired access switch rejects this key when it is set. cv.Optional(CONF_UNPAIRED_ACCESS): cv.boolean, cv.Optional(CONF_ON_OPEN_PAIRING_WINDOW): automation.validate_automation( {} @@ -322,8 +329,8 @@ def _final_validate(config: ConfigType) -> ConfigType: ) if not config.get(CONF_UNPAIRED_ACCESS, True) and not _has_pairing_method(config): _LOGGER.warning( - "'%s' is off but nothing lets a server pair (%s or a dynamic pairing code), so no " - "server can play on this device", + "'%s' is off but there is no pairing method (%s or a dynamic pairing code), so no " + "new server can pair with this device", CONF_UNPAIRED_ACCESS, CONF_STATIC_PAIRING_CODE, ) @@ -400,8 +407,12 @@ async def to_code(config: ConfigType) -> None: if (code := config.get(CONF_STATIC_PAIRING_CODE)) is not None: cg.add(var.set_static_pairing_code(code)) - if (unpaired_access := config.get(CONF_UNPAIRED_ACCESS)) is not None: - cg.add(var.set_default_unpaired_access(unpaired_access)) + # The client starts once both are set: here, or by the switch that drives the setting. + switch_types = _get_data().switch_types + if CONF_ENABLED not in switch_types: + cg.add(var.set_enabled(True)) + if CONF_UNPAIRED_ACCESS not in switch_types: + cg.add(var.set_unpaired_access_enabled(config.get(CONF_UNPAIRED_ACCESS, True))) if _offers_dynamic_pairing_code(config): cg.add(var.set_pairing_code_display_supported(True)) @@ -413,11 +424,11 @@ async def to_code(config: ConfigType) -> None: # esp_websocket_client links esp_tls even for ws:// connections. esp32.request_tls() - cg.add_define("USE_SENDSPIN", True) # for MDNS + cg.add_define("USE_SENDSPIN", True) # for MDNS and the native API # Service starts disabled and the hub enables it; always advertised where unsupported if mdns.request_service_enable_disable(): - mdns_var = await cg.get_variable(CORE.config[CONF_MDNS][CONF_ID]) + mdns_var = await cg.get_variable(CORE.config[MDNS_DOMAIN][CONF_ID]) cg.add(var.set_mdns(mdns_var)) data = _get_data() diff --git a/esphome/components/sendspin/image/sendspin_image.cpp b/esphome/components/sendspin/image/sendspin_image.cpp index 4787abc760..a93c2eb443 100644 --- a/esphome/components/sendspin/image/sendspin_image.cpp +++ b/esphome/components/sendspin/image/sendspin_image.cpp @@ -10,6 +10,8 @@ namespace esphome::sendspin_ { ESPHOME_LOG_TAG(TAG, "sendspin.image"); +static constexpr uint32_t TRANSITION_ACK_TIMEOUT_ID = 0; + // How long a displayed frame may wait for sendspin.image.transition_finished before a warning // names the missing ack. Generous next to a typical fade of a second or two. static constexpr uint32_t TRANSITION_ACK_WARNING_MS = 10000; @@ -163,7 +165,7 @@ void SendspinImageSlot::on_display_(uint32_t lateness_ms) { // that never reaches the action stalls the slot with nothing in the log. Name the cause after // a generous wait. Arming again replaces the previous timeout, so it cannot fire for a frame // that was already acked and superseded. - this->set_timeout("transition_ack", TRANSITION_ACK_WARNING_MS, [this]() { + this->set_timeout(TRANSITION_ACK_TIMEOUT_ID, TRANSITION_ACK_WARNING_MS, [this]() { if (this->transition_pending_) { ESP_LOGW(TAG, "Slot %u: displayed artwork was never acknowledged; no new artwork will arrive until " diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 33fdb78f10..8b5eaeeed2 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -113,6 +113,10 @@ static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"}; static const char *const IMAGE_FORMAT_NAMES[] = {"JPEG", "PNG"}; #endif +SendspinHub *global_sendspin_hub = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +SendspinHub::SendspinHub() { global_sendspin_hub = this; } + void SendspinHub::setup() { auto config = this->build_client_config_(); this->client_ = std::make_unique(std::move(config)); @@ -154,16 +158,15 @@ void SendspinHub::setup() { this->client_->add_player(this->player_config_).set_listener(this->player_listener_); #endif - this->client_->set_unpaired_access_enabled(this->default_unpaired_access_); - -#ifndef USE_SENDSPIN_SWITCH - this->enabled_ = true; -#endif + // Set before setup() by codegen; an unpaired access switch sets it later, from its own setup(). + if (this->unpaired_access_.has_value()) { + this->client_->set_unpaired_access_enabled(*this->unpaired_access_); + } } void SendspinHub::loop() { - if (this->enabled_.has_value() && this->enabled_.value() != this->client_->is_started() && - !this->status_has_error()) { + if (this->enabled_.has_value() && this->unpaired_access_.has_value() && + this->enabled_.value() != this->client_->is_started() && !this->status_has_error()) { if (!this->enabled_.value()) { this->client_->stop(); } else if (!this->client_->start()) { @@ -222,7 +225,7 @@ void SendspinHub::dump_config() { #endif } -// THREAD CONTEXT: Main loop (invoked from Sendspin components) +// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components) void SendspinHub::set_enabled(bool enabled) { if (this->status_has_error()) { ESP_LOGE(TAG, "Cannot %s: Sendspin failed to start, reboot to retry", @@ -232,6 +235,23 @@ void SendspinHub::set_enabled(bool enabled) { this->enabled_ = enabled; } +// THREAD CONTEXT: Main loop +std::optional SendspinHub::get_pairing_token() const { + if (this->client_ == nullptr) { + return std::nullopt; + } + return this->client_->pairing_token(); +} + +// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components) +void SendspinHub::set_unpaired_access_enabled(bool enabled) { + this->unpaired_access_ = enabled; + // Before setup() there is no client yet; setup() applies the stored value. + if (this->client_ != nullptr) { + this->client_->set_unpaired_access_enabled(enabled); + } +} + #ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE // THREAD CONTEXT: Main loop void SendspinHub::update_mdns_service_() { diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index d33269048e..cd8d0d0afa 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -62,6 +62,11 @@ inline constexpr float CHILD = HUB - 1.0f; /// Pairing-record slots, handed to the client as max_pairing_records. inline constexpr size_t SENDSPIN_RECORD_SLOTS = sendspin::SendspinClientConfig::DEFAULT_MAX_PAIRING_RECORDS; +/// Feature flags reported to API clients in DeviceCapabilitiesResponse. +enum SendspinFeature : uint32_t { + SENDSPIN_FEATURE_PAIRING_TOKEN = 1 << 0, +}; + /// @brief Thin adapter over sendspin::SendspinClient. /// /// The hub owns a SendspinClient instance and bridges its listener/provider interfaces to ESPHome's CallbackManager for @@ -91,6 +96,8 @@ class SendspinHub final : public Component, public sendspin::SendspinNetworkProvider, public sendspin::SendspinPersistenceProvider { public: + SendspinHub(); + float get_setup_priority() const override { return sendspin_priority::HUB; } void setup() override; void loop() override; @@ -168,16 +175,29 @@ class SendspinHub final : public Component, /// stop. /// /// Applied from the hub's loop(). Stopping blocks until the client is fully stopped; the roles' clear callbacks - /// fire from inside that call. With a sendspin switch configured the client stays stopped until the switch has - /// called this once. Must be called from the main loop thread. + /// fire from inside that call. The client stays stopped until this has been called once, by codegen or by the + /// enabled switch. Must be called from the main loop thread. void set_enabled(bool enabled); - /// Turns unpaired (Sentinel) access on or off from setup(); see SendspinClient::set_unpaired_access_enabled(). - void set_default_unpaired_access(bool enabled) { this->default_unpaired_access_ = enabled; } + /// Turns unpaired (Sentinel) access on or off. The client waits for this call, by codegen or by the unpaired + /// access switch, before its first start. May be called before setup(). Main loop only. + void set_unpaired_access_enabled(bool enabled); /// @brief Returns whether the Sendspin client is running. bool is_client_running() const { return this->client_ != nullptr && this->client_->is_started(); } + /// @brief Returns whether the client has been turned off with set_enabled(). + bool is_disabled() const { return this->enabled_.has_value() && !this->enabled_.value(); } + + /// @brief Returns the SendspinFeature flags reported to API clients. + uint32_t get_feature_flags() const { return SENDSPIN_FEATURE_PAIRING_TOKEN; } + + /// @brief Returns the pairing token a Sendspin server uses to pair with this device. + /// + /// The token is a long-lived secret, so this component never logs it. Returns std::nullopt until the client has + /// started once; it stays available after the client stops. Main loop only. + std::optional get_pairing_token() const; + /// @brief Sets the device information reported to the server in the `client/hello` message. /// /// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the @@ -382,14 +402,14 @@ class SendspinHub final : public Component, const char *static_pairing_code_{nullptr}; // Codegen string literal, or nullptr when not configured bool pairing_code_display_supported_{false}; - bool default_unpaired_access_{true}; bool task_stack_in_psram_{false}; #ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE bool mdns_advertised_{false}; // Last state requested from mdns #endif - // Requested client state, applied from loop(). Empty until the switch restores its state. + // Requested client state, applied from loop(). The client does not start until both are set. std::optional enabled_; + std::optional unpaired_access_; // Device information sent in the `client/hello` message. Defaults apply when neither the // sendspin configuration nor the project information supplies a value. @@ -423,6 +443,8 @@ class SendspinPollingChild : public PollingComponent, public Parented cv.Schema: + return ( + switch.switch_schema( + class_, + block_inverted=True, + default_restore_mode="RESTORE_DEFAULT_ON", + entity_category=ENTITY_CATEGORY_CONFIG, + ) + .extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)}) + .extend(cv.COMPONENT_SCHEMA) + ) + + +def _request_switch(config: ConfigType) -> ConfigType: + """Tell the hub to wait for this switch before the client's first start.""" + request_switch(config[CONF_TYPE]) + return config + CONFIG_SCHEMA = cv.All( cv.typed_schema( { - CONF_ENABLED: switch.switch_schema( - SendspinSwitch, - block_inverted=True, - default_restore_mode="RESTORE_DEFAULT_ON", - entity_category=ENTITY_CATEGORY_CONFIG, - ) - .extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)}) - .extend(cv.COMPONENT_SCHEMA), + CONF_ENABLED: _switch_schema(SendspinEnabledSwitch), + CONF_UNPAIRED_ACCESS: _switch_schema(SendspinUnpairedAccessSwitch), }, key=CONF_TYPE, ), cv.only_on_esp32, + _request_switch, ) +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + switch_type = config[CONF_TYPE] + same_type = [ + conf + for conf in full_config.get(SWITCH_DOMAIN, []) + if conf.get(CONF_PLATFORM) == DOMAIN and conf.get(CONF_TYPE) == switch_type + ] + # Two switches of one type would each drive the same hub setting. + if len(same_type) > 1: + raise cv.Invalid(f"Only one sendspin '{switch_type}' switch is allowed") + if switch_type != CONF_UNPAIRED_ACCESS: + return config + hub_config = full_config.get(DOMAIN, {}) + if CONF_UNPAIRED_ACCESS in hub_config: + raise cv.Invalid( + f"'{DOMAIN}: {CONF_UNPAIRED_ACCESS}' cannot be set together with an " + f"{CONF_UNPAIRED_ACCESS} switch; set the switch's restore_mode instead" + ) + if not _has_pairing_method(hub_config): + _LOGGER.warning( + "The %s switch has no pairing method (%s or a dynamic pairing code), so " + "while it is off no new server can pair with this device", + CONF_UNPAIRED_ACCESS, + CONF_STATIC_PAIRING_CODE, + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config: ConfigType) -> None: var = await switch.new_switch(config) await cg.register_component(var, config) diff --git a/esphome/components/sendspin/switch/sendspin_switch.cpp b/esphome/components/sendspin/switch/sendspin_switch.cpp index 343202e7f4..51c151980a 100644 --- a/esphome/components/sendspin/switch/sendspin_switch.cpp +++ b/esphome/components/sendspin/switch/sendspin_switch.cpp @@ -1,26 +1,49 @@ #include "sendspin_switch.h" -#ifdef USE_ESP_IDF +#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_SWITCH) #include "esphome/core/log.h" +#include "esphome/core/preferences.h" namespace esphome::sendspin_ { ESPHOME_LOG_TAG(TAG, "sendspin.switch"); -void SendspinSwitch::setup() { +// --- SendspinEnabledSwitch --- + +void SendspinEnabledSwitch::setup() { // The hub waits for this request, so a restore mode without a state still has to answer. this->control(this->get_initial_state_with_restore_mode().value_or(true)); } -void SendspinSwitch::dump_config() { LOG_SWITCH("", "Sendspin Switch", this); } +void SendspinEnabledSwitch::dump_config() { LOG_SWITCH("", "Sendspin Enabled Switch", this); } // THREAD CONTEXT: Main loop -void SendspinSwitch::write_state(bool state) { +void SendspinEnabledSwitch::write_state(bool state) { this->parent_->set_enabled(state); this->publish_state(state); } +// --- SendspinUnpairedAccessSwitch --- + +void SendspinUnpairedAccessSwitch::setup() { + // The hub holds the client's first start for this, so a restore mode without a state still has to answer. + this->control(this->get_initial_state_with_restore_mode().value_or(true)); +} + +void SendspinUnpairedAccessSwitch::dump_config() { LOG_SWITCH("", "Sendspin Unpaired Access Switch", this); } + +// THREAD CONTEXT: Main loop +void SendspinUnpairedAccessSwitch::write_state(bool state) { + const bool turning_off = !state && this->has_state() && this->state; + this->parent_->set_unpaired_access_enabled(state); + this->publish_state(state); + // Write the change to flash now: losing it in a power cut would reopen unpaired access. + if (turning_off) { + global_preferences->sync(); + } +} + } // namespace esphome::sendspin_ -#endif // USE_ESP_IDF +#endif diff --git a/esphome/components/sendspin/switch/sendspin_switch.h b/esphome/components/sendspin/switch/sendspin_switch.h index f9173c362d..4b04c95857 100644 --- a/esphome/components/sendspin/switch/sendspin_switch.h +++ b/esphome/components/sendspin/switch/sendspin_switch.h @@ -2,7 +2,7 @@ #include "esphome/core/defines.h" -#ifdef USE_ESP_IDF +#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_SWITCH) #include "esphome/components/sendspin/sendspin_hub.h" #include "esphome/components/switch/switch.h" @@ -10,7 +10,19 @@ namespace esphome::sendspin_ { /// @brief Switch that starts and stops the Sendspin client through the hub (see SendspinHub::set_enabled()). -class SendspinSwitch final : public switch_::Switch, public SendspinChild { +class SendspinEnabledSwitch final : public switch_::Switch, public SendspinChild { + public: + void setup() override; + void dump_config() override; + + protected: + void write_state(bool state) override; +}; + +/// @brief Switch that turns unpaired (Sentinel) access on and off through the hub (see +/// SendspinHub::set_unpaired_access_enabled()). The library does not persist the setting, so the switch's restore +/// mode does. +class SendspinUnpairedAccessSwitch final : public switch_::Switch, public SendspinChild { public: void setup() override; void dump_config() override; @@ -21,4 +33,4 @@ class SendspinSwitch final : public switch_::Switch, public SendspinChild { } // namespace esphome::sendspin_ -#endif // USE_ESP_IDF +#endif diff --git a/esphome/components/senseair/__init__.py b/esphome/components/senseair/__init__.py index e69de29bb2..5973c85b6a 100644 --- a/esphome/components/senseair/__init__.py +++ b/esphome/components/senseair/__init__.py @@ -0,0 +1 @@ +DOMAIN = "senseair" diff --git a/esphome/components/sensirion_common/__init__.py b/esphome/components/sensirion_common/__init__.py index 5d9424d1e7..e1fc793d6d 100644 --- a/esphome/components/sensirion_common/__init__.py +++ b/esphome/components/sensirion_common/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import i2c CODEOWNERS = ["@martgras"] +DOMAIN = "sensirion_common" sensirion_common_ns = cg.esphome_ns.namespace("sensirion_common") diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index f3293d4ed5..2b404442a4 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -1,5 +1,6 @@ import logging import math +from typing import Any from esphome import automation import esphome.codegen as cg @@ -43,6 +44,7 @@ from esphome.const import ( CONF_TIMEOUT, CONF_TO, CONF_TRIGGER_ID, + CONF_TYPE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE, CONF_WEB_SERVER, @@ -127,6 +129,7 @@ from esphome.types import ConfigType from esphome.util import Registry CODEOWNERS = ["@esphome/core"] +DOMAIN = "sensor" DEVICE_CLASSES = [ DEVICE_CLASS_ABSOLUTE_HUMIDITY, @@ -221,6 +224,13 @@ def validate_send_first_at(value): FILTER_REGISTRY = Registry() validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) +# Default for streamed readings (radar sensors): hold the last value for a second so a dropped +# frame doesn't read as absence, then rate limit. Codegen turns the pair into one TimeoutThrottleFilter. +TIMEOUT_THROTTLE_FILTERS = [ + {"timeout": {"timeout": cv.TimePeriod(milliseconds=1000), "value": "last"}}, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, +] + def validate_datapoint(value): if isinstance(value, dict): @@ -292,6 +302,7 @@ ThrottleWithPriorityNanFilter = sensor_ns.class_( TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component) TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) +TimeoutThrottleFilter = sensor_ns.class_("TimeoutThrottleFilter", TimeoutFilterLast) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -682,6 +693,12 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( ) +def _is_nan_only(values: Any) -> bool: + if not isinstance(values, list): + values = [values] + return bool(values) and all(isinstance(v, float) and math.isnan(v) for v in values) + + @FILTER_REGISTRY.register( "throttle_with_priority", ThrottleWithPriorityFilter, @@ -695,7 +712,7 @@ async def throttle_with_priority_filter_to_code(config, filter_id): # omits `value:`) to avoid the TemplatableFn array + NaN lambda the # generic ValueListFilter path requires. Behavior is identical: NaN sensor # readings always bypass the throttle. - if values and all(isinstance(v, float) and math.isnan(v) for v in values): + if _is_nan_only(values): filter_id = filter_id.copy() filter_id.type = ThrottleWithPriorityNanFilter return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) @@ -940,6 +957,43 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +def _timeout_throttle_period( + first: ConfigType, second: ConfigType +) -> cv.TimePeriod | None: + """Period of a `timeout` (value `last`) directly followed by a NaN only `throttle_with_priority`.""" + timeout = first.get("timeout") + throttle = second.get("throttle_with_priority") + if not isinstance(timeout, dict) or not isinstance(throttle, dict): + return None + if timeout[CONF_VALUE] != "last" or timeout[CONF_TIMEOUT] != throttle[CONF_TIMEOUT]: + return None + if not _is_nan_only(throttle[CONF_VALUE]): + return None + return timeout[CONF_TIMEOUT] + + +async def _build_chain_filters(config: list[ConfigType]) -> list: + """Like build_filters, but merges the radar `timeout` + `throttle_with_priority` pair into one filter.""" + filters = [] + i = 0 + while i < len(config): + if ( + i + 1 < len(config) + and (period := _timeout_throttle_period(config[i], config[i + 1])) + is not None + ): + filter_id = config[i][CONF_TYPE_ID].copy() + filter_id.type = TimeoutThrottleFilter + var = cg.new_Pvariable(filter_id, period) + await cg.register_component(var, {}) + filters.append(var) + i += 2 + continue + filters.append(await cg.build_registry_entry(FILTER_REGISTRY, config[i])) + i += 1 + return filters + + _CALLBACK_AUTOMATIONS = ( automation.CallbackAutomation( CONF_ON_VALUE, "add_on_state_callback", [(float, "x")] @@ -978,7 +1032,7 @@ async def setup_sensor_core_(var, config): cg.add(var.set_force_update(True)) if config.get(CONF_FILTERS): # must exist and not be empty cg.add_define("USE_SENSOR_FILTER") - filters = await build_filters(config[CONF_FILTERS]) + filters = await _build_chain_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) CORE.add_job(_build_sensor_automations, var, config) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 28f7e6100d..732db5b0b4 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -13,6 +13,17 @@ namespace esphome::sensor { ESPHOME_LOG_TAG(TAG, "sensor.filter"); +/// Shared pass check for throttle_with_priority (NaN only): passes and stamps `last_input` when the +/// period has elapsed, on the first value, or for NaN. +static bool throttle_nan_passes(uint32_t &last_input, uint32_t period, float value) { + const uint32_t now = App.get_loop_component_start_time(); + if (last_input == 0 || now - last_input >= period || std::isnan(value)) { + last_input = now; + return true; + } + return false; +} + // Filter void Filter::input(float value) { ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value); @@ -266,11 +277,8 @@ optional throttle_with_priority_new_value(Sensor *parent, float value, co ThrottleWithPriorityNanFilter::ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {} optional ThrottleWithPriorityNanFilter::new_value(float value) { - const uint32_t now = App.get_loop_component_start_time(); - if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || std::isnan(value)) { - this->last_input_ = now; + if (throttle_nan_passes(this->last_input_, this->min_time_between_inputs_, value)) return value; - } return {}; } @@ -355,6 +363,23 @@ optional TimeoutFilterConfigured::new_value(float value) { return value; } +// TimeoutThrottleFilter +optional TimeoutThrottleFilter::new_value(float value) { + TimeoutFilterLast::new_value(value); + if (throttle_nan_passes(this->last_input_, this->time_period_, value)) + return value; + return {}; +} + +void TimeoutThrottleFilter::loop() { + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->timeout_start_time_ >= this->time_period_) { + if (throttle_nan_passes(this->last_input_, this->time_period_, this->pending_value_)) + this->output(this->pending_value_); + this->disable_loop(); + } +} + // DebounceFilter optional DebounceFilter::new_value(float value) { App.scheduler.set_timeout(this, this->time_period_, [this, value]() { this->output(value); }); diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index bc086e3805..510ee94c6a 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -479,6 +479,19 @@ class TimeoutFilterConfigured : public TimeoutFilterBase { // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead }; +/// Codegen use only: `timeout` (value `last`) followed by `throttle_with_priority` (NaN only) with the +/// same period, as one object. Output matches the two filter chain exactly. +class TimeoutThrottleFilter : public TimeoutFilterLast { + public: + explicit TimeoutThrottleFilter(uint32_t time_period) : TimeoutFilterLast(time_period) {} + + optional new_value(float value) override; + void loop() override; + + protected: + uint32_t last_input_{0}; // When the throttle stage last let a value through +}; + class DebounceFilter : public Filter { public: explicit DebounceFilter(uint32_t time_period); diff --git a/esphome/components/servo/__init__.py b/esphome/components/servo/__init__.py index 74acaa3496..44ec294765 100644 --- a/esphome/components/servo/__init__.py +++ b/esphome/components/servo/__init__.py @@ -19,6 +19,7 @@ servo_ns = cg.esphome_ns.namespace("servo") Servo = servo_ns.class_("Servo", cg.Component) CONF_AUTO_DETACH_TIME = "auto_detach_time" +DOMAIN = "servo" MULTI_CONF = True CONFIG_SCHEMA = cv.Schema( { diff --git a/esphome/components/sfa30/__init__.py b/esphome/components/sfa30/__init__.py index 28b665906f..d608a07f79 100644 --- a/esphome/components/sfa30/__init__.py +++ b/esphome/components/sfa30/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ghsensdev"] +DOMAIN = "sfa30" diff --git a/esphome/components/sfa40/__init__.py b/esphome/components/sfa40/__init__.py index 79568d458a..077f0d2e9f 100644 --- a/esphome/components/sfa40/__init__.py +++ b/esphome/components/sfa40/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@NoQuarrel"] +DOMAIN = "sfa40" diff --git a/esphome/components/sgp30/__init__.py b/esphome/components/sgp30/__init__.py index e69de29bb2..4754f35d59 100644 --- a/esphome/components/sgp30/__init__.py +++ b/esphome/components/sgp30/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sgp30" diff --git a/esphome/components/sgp40/__init__.py b/esphome/components/sgp40/__init__.py index e69de29bb2..f18cc0d930 100644 --- a/esphome/components/sgp40/__init__.py +++ b/esphome/components/sgp40/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sgp40" diff --git a/esphome/components/sgp4x/__init__.py b/esphome/components/sgp4x/__init__.py index e69de29bb2..f8030fdae0 100644 --- a/esphome/components/sgp4x/__init__.py +++ b/esphome/components/sgp4x/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sgp4x" diff --git a/esphome/components/sha256/__init__.py b/esphome/components/sha256/__init__.py index 5db0e77b76..5856d230a2 100644 --- a/esphome/components/sha256/__init__.py +++ b/esphome/components/sha256/__init__.py @@ -5,6 +5,7 @@ from esphome.helpers import IS_MACOS from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "sha256" sha256_ns = cg.esphome_ns.namespace("sha256") diff --git a/esphome/components/shelly_dimmer/__init__.py b/esphome/components/shelly_dimmer/__init__.py index accefbbc34..72586d1637 100644 --- a/esphome/components/shelly_dimmer/__init__.py +++ b/esphome/components/shelly_dimmer/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@rnauber", "@edge90"] +DOMAIN = "shelly_dimmer" diff --git a/esphome/components/sht3xd/__init__.py b/esphome/components/sht3xd/__init__.py index e69de29bb2..f58b3fb150 100644 --- a/esphome/components/sht3xd/__init__.py +++ b/esphome/components/sht3xd/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sht3xd" diff --git a/esphome/components/sht4x/__init__.py b/esphome/components/sht4x/__init__.py index e69de29bb2..2e9124bdab 100644 --- a/esphome/components/sht4x/__init__.py +++ b/esphome/components/sht4x/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sht4x" diff --git a/esphome/components/shtcx/__init__.py b/esphome/components/shtcx/__init__.py index e69de29bb2..30c06bdaec 100644 --- a/esphome/components/shtcx/__init__.py +++ b/esphome/components/shtcx/__init__.py @@ -0,0 +1 @@ +DOMAIN = "shtcx" diff --git a/esphome/components/shutdown/__init__.py b/esphome/components/shutdown/__init__.py index 480a6f3e31..949ea69bb9 100644 --- a/esphome/components/shutdown/__init__.py +++ b/esphome/components/shutdown/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@esphome/core", "@jsuanet"] +DOMAIN = "shutdown" diff --git a/esphome/components/sigma_delta_output/__init__.py b/esphome/components/sigma_delta_output/__init__.py index 3356e61bb2..2af211995b 100644 --- a/esphome/components/sigma_delta_output/__init__.py +++ b/esphome/components/sigma_delta_output/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Cat-Ion"] +DOMAIN = "sigma_delta_output" diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index c472e01f83..e20c091b75 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_MESSAGE DEPENDENCIES = ["uart"] CODEOWNERS = ["@glmnet"] +DOMAIN = "sim800l" MULTI_CONF = True sim800l_ns = cg.esphome_ns.namespace("sim800l") diff --git a/esphome/components/slow_pwm/__init__.py b/esphome/components/slow_pwm/__init__.py index e69de29bb2..c46da34e38 100644 --- a/esphome/components/slow_pwm/__init__.py +++ b/esphome/components/slow_pwm/__init__.py @@ -0,0 +1 @@ +DOMAIN = "slow_pwm" diff --git a/esphome/components/sm10bit_base/__init__.py b/esphome/components/sm10bit_base/__init__.py index 2b555a59df..05ba98562d 100644 --- a/esphome/components/sm10bit_base/__init__.py +++ b/esphome/components/sm10bit_base/__init__.py @@ -6,6 +6,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@Cossid"] +DOMAIN = "sm10bit_base" MULTI_CONF = True CONF_MAX_POWER_COLOR_CHANNELS = "max_power_color_channels" diff --git a/esphome/components/sm16716/__init__.py b/esphome/components/sm16716/__init__.py index 3fb9ac3f06..ae3c54ab7c 100644 --- a/esphome/components/sm16716/__init__.py +++ b/esphome/components/sm16716/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( from esphome.types import ConfigType AUTO_LOAD = ["output"] +DOMAIN = "sm16716" sm16716_ns = cg.esphome_ns.namespace("sm16716") SM16716 = sm16716_ns.class_("SM16716", cg.Component) diff --git a/esphome/components/sm2135/__init__.py b/esphome/components/sm2135/__init__.py index 537db5bbe4..c42ce4c8b3 100644 --- a/esphome/components/sm2135/__init__.py +++ b/esphome/components/sm2135/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["output"] CODEOWNERS = ["@BoukeHaarsma23", "@matika77", "@dd32"] +DOMAIN = "sm2135" sm2135_ns = cg.esphome_ns.namespace("sm2135") SM2135 = sm2135_ns.class_("SM2135", cg.Component) diff --git a/esphome/components/sm2235/__init__.py b/esphome/components/sm2235/__init__.py index 95037fd275..803cd54be5 100644 --- a/esphome/components/sm2235/__init__.py +++ b/esphome/components/sm2235/__init__.py @@ -5,6 +5,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["sm10bit_base", "output"] CODEOWNERS = ["@Cossid"] +DOMAIN = "sm2235" MULTI_CONF = True sm2235_ns = cg.esphome_ns.namespace("sm2235") diff --git a/esphome/components/sm2335/__init__.py b/esphome/components/sm2335/__init__.py index 145c5a0117..a9fdfcef39 100644 --- a/esphome/components/sm2335/__init__.py +++ b/esphome/components/sm2335/__init__.py @@ -5,6 +5,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["sm10bit_base", "output"] CODEOWNERS = ["@Cossid"] +DOMAIN = "sm2335" MULTI_CONF = True sm2335_ns = cg.esphome_ns.namespace("sm2335") diff --git a/esphome/components/sm300d2/__init__.py b/esphome/components/sm300d2/__init__.py index e69de29bb2..da0a0637ed 100644 --- a/esphome/components/sm300d2/__init__.py +++ b/esphome/components/sm300d2/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sm300d2" diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index 07ca5bf444..35ca231f02 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -9,6 +9,7 @@ from esphome.const import CONF_ID, CONF_ON_DATA from esphome.types import ConfigType CODEOWNERS = ["@alengwenus"] +DOMAIN = "sml" DEPENDENCIES = ["uart"] diff --git a/esphome/components/smt100/__init__.py b/esphome/components/smt100/__init__.py index 44ed591400..572df68d99 100644 --- a/esphome/components/smt100/__init__.py +++ b/esphome/components/smt100/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@piechade"] +DOMAIN = "smt100" diff --git a/esphome/components/sn74hc165/__init__.py b/esphome/components/sn74hc165/__init__.py index 4f21312fec..af566cebda 100644 --- a/esphome/components/sn74hc165/__init__.py +++ b/esphome/components/sn74hc165/__init__.py @@ -15,6 +15,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] DEPENDENCIES = [] +DOMAIN = "sn74hc165" MULTI_CONF = True sn74hc165_ns = cg.esphome_ns.namespace("sn74hc165") diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index 367b65176b..78ed703ed9 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( from esphome.cpp_generator import MockObj from esphome.types import ConfigType +DOMAIN = "sn74hc595" MULTI_CONF = True sn74hc595_ns = cg.esphome_ns.namespace("sn74hc595") diff --git a/esphome/components/sntp/__init__.py b/esphome/components/sntp/__init__.py index e69de29bb2..481183e020 100644 --- a/esphome/components/sntp/__init__.py +++ b/esphome/components/sntp/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sntp" diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 9d849f9fe9..9365339473 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -2,13 +2,13 @@ import logging import esphome.codegen as cg from esphome.components import time as time_ +from esphome.components.time import DOMAIN as TIME_DOMAIN from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_PLATFORM, CONF_SERVERS, - CONF_TIME, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -20,11 +20,12 @@ from esphome.core import CORE import esphome.final_validate as fv from esphome.types import ConfigType +from . import DOMAIN + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["network"] -CONF_SNTP = "sntp" sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) @@ -35,13 +36,13 @@ DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] def _sntp_final_validate(config: ConfigType) -> None: """Merge multiple SNTP instances into one, similar to OTA merging behavior.""" full_conf = fv.full_config.get() - time_confs = full_conf.get(CONF_TIME, []) + time_confs = full_conf.get(TIME_DOMAIN, []) sntp_configs: list[ConfigType] = [] other_time_configs: list[ConfigType] = [] for time_conf in time_confs: - if time_conf.get(CONF_PLATFORM) == CONF_SNTP: + if time_conf.get(CONF_PLATFORM) == DOMAIN: sntp_configs.append(time_conf) else: other_time_configs.append(time_conf) @@ -81,7 +82,7 @@ def _sntp_final_validate(config: ConfigType) -> None: # Replace time configs with merged SNTP + other time platforms other_time_configs.append(merged) - full_conf[CONF_TIME] = other_time_configs + full_conf[TIME_DOMAIN] = other_time_configs fv.full_config.set(full_conf) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 3d2cd8ef50..cc5f672ad5 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -14,6 +14,7 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] +DOMAIN = "socket" socket_ns = cg.esphome_ns.namespace("socket") Ipv4AllowEntry = socket_ns.struct("Ipv4AllowEntry") diff --git a/esphome/components/sonoff_d1/__init__.py b/esphome/components/sonoff_d1/__init__.py index 18b4d30d18..40a6c103db 100644 --- a/esphome/components/sonoff_d1/__init__.py +++ b/esphome/components/sonoff_d1/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@anatoly-savchenkov"] +DOMAIN = "sonoff_d1" diff --git a/esphome/components/sound_level/__init__.py b/esphome/components/sound_level/__init__.py index e69de29bb2..93f0f4b2e3 100644 --- a/esphome/components/sound_level/__init__.py +++ b/esphome/components/sound_level/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sound_level" diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index c5eee3665c..1e51e45b7b 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -171,7 +171,7 @@ bool SoundLevelComponent::start_() { (stream_info.ms_to_bytes(RING_BUFFER_DURATION_MS) / bytes_per_frame) * bytes_per_frame; std::shared_ptr temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); if (temp_ring_buffer == nullptr) { - this->status_momentary_error("ring_buffer", 15000); + this->status_momentary_error(15000); return false; } @@ -180,7 +180,7 @@ bool SoundLevelComponent::start_() { this->audio_source_ = audio::RingBufferAudioSource::create( temp_ring_buffer, stream_info.ms_to_bytes(MAX_FILL_DURATION_MS), static_cast(bytes_per_frame)); if (this->audio_source_ == nullptr) { - this->status_momentary_error("audio_source", 15000); + this->status_momentary_error(15000); return false; } this->ring_buffer_ = temp_ring_buffer; diff --git a/esphome/components/spa06_base/__init__.py b/esphome/components/spa06_base/__init__.py index e084020a14..c08d92f9d8 100644 --- a/esphome/components/spa06_base/__init__.py +++ b/esphome/components/spa06_base/__init__.py @@ -19,6 +19,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@danielkent-net"] +DOMAIN = "spa06_base" spa06_ns = cg.esphome_ns.namespace("spa06_base") diff --git a/esphome/components/spa06_base/spa06_base.cpp b/esphome/components/spa06_base/spa06_base.cpp index 987a14c66a..02defd0773 100644 --- a/esphome/components/spa06_base/spa06_base.cpp +++ b/esphome/components/spa06_base/spa06_base.cpp @@ -8,6 +8,8 @@ namespace esphome::spa06_base { ESPHOME_LOG_TAG(TAG, "spa06"); +static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0; + // Sign extension function for <=16 bit types inline int16_t decode16(uint8_t msb, uint8_t lsb, size_t bits, size_t head = 0) { return static_cast(encode_uint16(msb, lsb) << head) >> (16 - bits); @@ -243,7 +245,7 @@ void SPA06Component::update() { } // Queue a background task for retrieving the measurement - this->set_timeout("measurement", this->conversion_time_, [this]() { + this->set_timeout(MEASUREMENT_TIMEOUT_ID, this->conversion_time_, [this]() { float raw_temperature; float temperature = 0.0; float pressure = 0.0; diff --git a/esphome/components/spa06_i2c/__init__.py b/esphome/components/spa06_i2c/__init__.py index e69de29bb2..928c6c157b 100644 --- a/esphome/components/spa06_i2c/__init__.py +++ b/esphome/components/spa06_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "spa06_i2c" diff --git a/esphome/components/spa06_spi/__init__.py b/esphome/components/spa06_spi/__init__.py index e69de29bb2..b473921999 100644 --- a/esphome/components/spa06_spi/__init__.py +++ b/esphome/components/spa06_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "spa06_spi" diff --git a/esphome/components/spd2010/__init__.py b/esphome/components/spd2010/__init__.py new file mode 100644 index 0000000000..67a78e5f28 --- /dev/null +++ b/esphome/components/spd2010/__init__.py @@ -0,0 +1,3 @@ +CODEOWNERS = ["@grischard"] +DEPENDENCIES = ["i2c"] +DOMAIN = "spd2010" diff --git a/esphome/components/spd2010/spd2010_touchscreen.cpp b/esphome/components/spd2010/spd2010_touchscreen.cpp new file mode 100644 index 0000000000..94fad87d7d --- /dev/null +++ b/esphome/components/spd2010/spd2010_touchscreen.cpp @@ -0,0 +1,211 @@ +// SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD +// SPDX-License-Identifier: Apache-2.0 +// Protocol adapted from esp_lcd_touch_spd2010 2.0.1 for ESPHome. +// https://github.com/espressif/esp-bsp/tree/master/components/lcd_touch/esp_lcd_touch_spd2010 + +#include "spd2010_touchscreen.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::spd2010 { + +ESPHOME_LOG_TAG(TAG, "spd2010.touchscreen"); + +static constexpr uint16_t REG_CLEAR_INTERRUPT = 0x0002; +static constexpr uint16_t REG_CPU_START = 0x0004; +static constexpr uint16_t REG_STATUS = 0x0020; +static constexpr uint16_t REG_VERSION = 0x0026; +static constexpr uint16_t REG_TOUCH_START = 0x0046; +static constexpr uint16_t REG_POINT_MODE = 0x0050; +static constexpr uint16_t REG_HDP_STATUS = 0x02FC; +static constexpr uint16_t REG_HDP = 0x0300; + +// Low status byte +static constexpr uint8_t STATUS_POINT = 0x01; +static constexpr uint8_t STATUS_GESTURE = 0x02; +static constexpr uint8_t STATUS_AUX = 0x08; +// High status byte +static constexpr uint8_t STATUS_BIOS = 0x40; +static constexpr uint8_t STATUS_CPU = 0x20; +static constexpr uint8_t STATUS_RUNNING = 0x08; + +static constexpr uint8_t HDP_STATUS_DONE = 0x82; +static constexpr uint8_t HDP_STATUS_MORE = 0x00; + +static constexpr uint8_t MAX_CONTACT_ID = 0x0A; +static constexpr uint8_t GESTURE_REPORT_ID = 0xF6; +static constexpr size_t REPORT_HEADER_LENGTH = 4; +static constexpr size_t CONTACT_LENGTH = 6; +static constexpr size_t MAX_REPORT_LENGTH = REPORT_HEADER_LENGTH + 10 * CONTACT_LENGTH; +static constexpr uint8_t MAX_DRAIN_READS = 8; + +static constexpr uint32_t RESET_PULSE_WIDTH = + 20; // longer than the datasheet says, but the controller seems to need it +static constexpr uint32_t RESET_DELAY = 20; + +bool SPD2010Touchscreen::read_register_(uint16_t reg, uint8_t *data, size_t length) const { + const uint8_t command[] = {static_cast(reg), static_cast(reg >> 8u)}; + if (this->write(command, sizeof(command)) != i2c::ERROR_OK) + return false; + // The controller needs a STOP and a delay before a commandless receive. + delay_microseconds_safe(200); + return this->read(data, length) == i2c::ERROR_OK; +} + +bool SPD2010Touchscreen::write_command_(uint16_t reg, uint16_t value) const { + const uint8_t command[]{ + static_cast(reg), + static_cast(reg >> 8u), + static_cast(value), + static_cast(value >> 8u), + }; + return this->write(command, sizeof(command)) == i2c::ERROR_OK; +} + +bool SPD2010Touchscreen::clear_interrupt_() const { return this->write_command_(REG_CLEAR_INTERRUPT, 1); } + +void SPD2010Touchscreen::setup() { + if (this->x_raw_max_ == this->x_raw_min_) + this->x_raw_max_ = this->display_->get_native_width() - 1; + if (this->y_raw_max_ == this->y_raw_min_) + this->y_raw_max_ = this->display_->get_native_height() - 1; + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + this->set_timeout(RESET_PULSE_WIDTH, [this] { + this->reset_pin_->digital_write(true); + this->set_timeout(RESET_DELAY, [this] { this->initialize_(); }); + }); + } else { + this->initialize_(); + } +} + +void SPD2010Touchscreen::initialize_() { + uint8_t version[18]; + if (!this->read_register_(REG_VERSION, version, sizeof(version))) { + this->mark_failed(LOG_STR("Failed to read firmware version")); + return; + } + ESP_LOGD(TAG, "Firmware version: 0x%04X", encode_uint16(version[5], version[4])); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + this->ready_ = true; +} + +bool SPD2010Touchscreen::finish_report_() { + // A corrupt status must not cause an unbounded drain loop or buffer overflow. + for (uint8_t attempt = 0; attempt != MAX_DRAIN_READS; attempt++) { + uint8_t status[8]; + if (!this->read_register_(REG_HDP_STATUS, status, sizeof(status))) + return false; + if (status[5] == HDP_STATUS_DONE) + return this->clear_interrupt_(); + const uint16_t length = encode_uint16(status[3], status[2]); + if (status[5] != HDP_STATUS_MORE || length == 0 || length > MAX_REPORT_LENGTH) + break; + uint8_t remaining[MAX_REPORT_LENGTH]; + if (!this->read_register_(REG_HDP, remaining, length)) + return false; + } + ESP_LOGW(TAG, "Invalid or unfinished report"); + (void) this->clear_interrupt_(); + return false; +} + +// Returns true only when touch data was read correctly and should be published. +bool SPD2010Touchscreen::read_data_() { + uint8_t status[4]; + if (!this->read_register_(REG_STATUS, status, sizeof(status))) { + this->status_set_warning(LOG_STR("Failed to read status")); + return false; + } + const uint8_t status_low = status[0]; + const uint8_t status_high = status[1]; + const uint16_t length = encode_uint16(status[3], status[2]); + + if (status_high & STATUS_BIOS) { + this->status_set_warning(LOG_STR("BIOS status; start CPU")); + (void) (this->clear_interrupt_() && this->write_command_(REG_CPU_START, 1)); + return false; + } + if (status_high & STATUS_CPU) { + this->status_set_warning(LOG_STR("CPU status; stop point mode, start touch")); + (void) (this->write_command_(REG_POINT_MODE, 0) && this->write_command_(REG_TOUCH_START, 0) && + this->clear_interrupt_()); + return false; + } + if ((status_high & STATUS_RUNNING) && length == 0) { + if (!this->clear_interrupt_()) { + this->status_set_warning(LOG_STR("Failed to clear interrupt")); + return false; + } + this->status_clear_warning(); + return true; + } + if (!(status_low & (STATUS_POINT | STATUS_GESTURE))) { + if ((status_high & STATUS_RUNNING) && (status_low & STATUS_AUX) && !this->clear_interrupt_()) { + this->status_set_warning(LOG_STR("Failed to clear interrupt")); + return false; + } + this->status_clear_warning(); + return false; + } + if (length < REPORT_HEADER_LENGTH || length > MAX_REPORT_LENGTH || + (length - REPORT_HEADER_LENGTH) % CONTACT_LENGTH != 0) { + ESP_LOGW(TAG, "Invalid report length: %u", length); + this->status_set_warning(LOG_STR("Invalid report length")); + (void) this->clear_interrupt_(); + return false; + } + uint8_t report[MAX_REPORT_LENGTH]; + if (!this->read_register_(REG_HDP, report, length) || !this->finish_report_()) { + this->status_set_warning(LOG_STR("Failed to read report")); + return false; + } + // Gesture reports are not used. + if (!(status_low & STATUS_POINT) || + (length > REPORT_HEADER_LENGTH && report[REPORT_HEADER_LENGTH] == GESTURE_REPORT_ID)) { + this->status_clear_warning(); + return false; + } + // Check every ID before adding any point, so a bad report leaves the stored touches unchanged. + for (size_t offset = REPORT_HEADER_LENGTH; offset < length; offset += CONTACT_LENGTH) { + if (report[offset] > MAX_CONTACT_ID) { + this->status_set_warning(LOG_STR("Invalid contact ID")); + return false; + } + } + for (size_t offset = REPORT_HEADER_LENGTH; offset < length; offset += CONTACT_LENGTH) { + const uint8_t *contact = report + offset; + // Zero strength marks a released contact. + if (contact[4] == 0) + continue; + const uint16_t x = ((contact[3] & 0xF0u) << 4u) | contact[1]; + const uint16_t y = ((contact[3] & 0x0Fu) << 8u) | contact[2]; + this->add_raw_touch_position_(contact[0], x, y, contact[4]); + } + this->status_clear_warning(); + return true; +} + +void SPD2010Touchscreen::update_touches() { + if (!this->ready_ || !this->read_data_()) + this->skip_update_ = true; +} + +void SPD2010Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, + "SPD2010 Touchscreen:\n" + " Raw X range: %d-%d\n" + " Raw Y range: %d-%d", + this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +} // namespace esphome::spd2010 diff --git a/esphome/components/spd2010/spd2010_touchscreen.h b/esphome/components/spd2010/spd2010_touchscreen.h new file mode 100644 index 0000000000..8817396872 --- /dev/null +++ b/esphome/components/spd2010/spd2010_touchscreen.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/hal.h" + +namespace esphome::spd2010 { + +class SPD2010Touchscreen final : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void initialize_(); + void update_touches() override; + bool read_data_(); + bool read_register_(uint16_t reg, uint8_t *data, size_t length) const; + bool write_command_(uint16_t reg, uint16_t value) const; + bool clear_interrupt_() const; + bool finish_report_(); + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + bool ready_{false}; +}; + +} // namespace esphome::spd2010 diff --git a/esphome/components/spd2010/touchscreen.py b/esphome/components/spd2010/touchscreen.py new file mode 100644 index 0000000000..7ead6a3418 --- /dev/null +++ b/esphome/components/spd2010/touchscreen.py @@ -0,0 +1,33 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c, touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN +from esphome.types import ConfigType + +spd2010_ns = cg.esphome_ns.namespace("spd2010") +SPD2010Touchscreen = spd2010_ns.class_( + "SPD2010Touchscreen", touchscreen.Touchscreen, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + touchscreen.touchscreen_schema("50ms") + .extend( + { + cv.GenerateID(): cv.declare_id(SPD2010Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x53)) +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index d0a743543e..d34cfc32c0 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -8,6 +8,7 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] +DOMAIN = "speaker" IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index e4aa78d10d..8ab4128e92 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -52,6 +52,11 @@ static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1; ESPHOME_LOG_TAG(TAG, "speaker_media_player"); +static constexpr uint32_t UNPAUSE_MEDIA_INTERVAL_ID = 0; +static constexpr uint32_t NEXT_ANNOUNCEMENT_TIMEOUT_ID = 1; +static constexpr uint32_t NEXT_MEDIA_TIMEOUT_ID = 2; +static constexpr uint32_t UNPAUSE_ANNOUNCEMENT_INTERVAL_ID = 3; + void SpeakerMediaPlayer::setup() { #ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF state = media_player::MEDIA_PLAYER_STATE_OFF; @@ -112,13 +117,13 @@ void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type, void SpeakerMediaPlayer::stop_and_unpause_media_() { this->media_pipeline_->stop(); this->unpause_media_remaining_ = 3; - this->set_interval("unpause_med", 50, [this]() { + this->set_interval(UNPAUSE_MEDIA_INTERVAL_ID, 50, [this]() { if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { - this->cancel_interval("unpause_med"); + this->cancel_interval(UNPAUSE_MEDIA_INTERVAL_ID); this->media_pipeline_->set_pause_state(false); this->is_paused_ = false; } else if (--this->unpause_media_remaining_ == 0) { - this->cancel_interval("unpause_med"); + this->cancel_interval(UNPAUSE_MEDIA_INTERVAL_ID); } }); } @@ -152,7 +157,7 @@ void SpeakerMediaPlayer::watch_media_commands_() { if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { if (!enqueue) { // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause - this->cancel_timeout("next_ann"); + this->cancel_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID); this->announcement_playlist_.clear(); this->announcement_item_failed_ = false; if (media_command.file.has_value()) { @@ -166,7 +171,7 @@ void SpeakerMediaPlayer::watch_media_commands_() { } else { if (!enqueue) { // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause - this->cancel_timeout("next_media"); + this->cancel_timeout(NEXT_MEDIA_TIMEOUT_ID); this->media_playlist_.clear(); this->media_item_failed_ = false; if (this->is_paused_) { @@ -236,23 +241,23 @@ void SpeakerMediaPlayer::watch_media_commands_() { if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { #endif if (this->announcement_pipeline_ != nullptr) { - this->cancel_timeout("next_ann"); + this->cancel_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID); this->announcement_playlist_.clear(); this->announcement_item_failed_ = false; this->announcement_pipeline_->stop(); this->unpause_announcement_remaining_ = 3; - this->set_interval("unpause_ann", 50, [this]() { + this->set_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID, 50, [this]() { if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) { - this->cancel_interval("unpause_ann"); + this->cancel_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID); this->announcement_pipeline_->set_pause_state(false); } else if (--this->unpause_announcement_remaining_ == 0) { - this->cancel_interval("unpause_ann"); + this->cancel_interval(UNPAUSE_ANNOUNCEMENT_INTERVAL_ID); } }); } } else { if (this->media_pipeline_ != nullptr) { - this->cancel_timeout("next_media"); + this->cancel_timeout(NEXT_MEDIA_TIMEOUT_ID); this->media_playlist_.clear(); this->media_item_failed_ = false; this->stop_and_unpause_media_(); @@ -408,7 +413,8 @@ void SpeakerMediaPlayer::loop() { this->announcement_pipeline_->set_pause_state(true); // Internally unpause the pipeline after the delay between playlist items. Announcements do not follow the // media player's pause state. - this->set_timeout("next_ann", timeout_ms, [this]() { this->announcement_pipeline_->set_pause_state(false); }); + this->set_timeout(NEXT_ANNOUNCEMENT_TIMEOUT_ID, timeout_ms, + [this]() { this->announcement_pipeline_->set_pause_state(false); }); } } } else { @@ -454,7 +460,7 @@ void SpeakerMediaPlayer::loop() { this->media_pipeline_->set_pause_state(true); // Internally unpause the pipeline after the delay between playlist items, if the media player state is // not paused. - this->set_timeout("next_media", timeout_ms, + this->set_timeout(NEXT_MEDIA_TIMEOUT_ID, timeout_ms, [this]() { this->media_pipeline_->set_pause_state(this->is_paused_); }); } } diff --git a/esphome/components/speaker_source/__init__.py b/esphome/components/speaker_source/__init__.py index e69de29bb2..ebe19a4ead 100644 --- a/esphome/components/speaker_source/__init__.py +++ b/esphome/components/speaker_source/__init__.py @@ -0,0 +1 @@ +DOMAIN = "speaker_source" diff --git a/esphome/components/speed/__init__.py b/esphome/components/speed/__init__.py index b75374863b..9f048ef37d 100644 --- a/esphome/components/speed/__init__.py +++ b/esphome/components/speed/__init__.py @@ -1,3 +1,5 @@ import esphome.codegen as cg +DOMAIN = "speed" + speed_ns = cg.esphome_ns.namespace("speed") diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index dd2dce01c5..d6949cc590 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -18,6 +18,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S31, only_on_variant, ) +from esphome.components.psram import DOMAIN as PSRAM_DOMAIN from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -44,6 +45,7 @@ import esphome.final_validate as fv from esphome.types import ConfigType CODEOWNERS = ["@esphome/core", "@clydebarrow"] +DOMAIN = "spi" spi_ns = cg.esphome_ns.namespace("spi") SPIComponent = spi_ns.class_("SPIComponent", cg.Component) QuadSPIComponent = spi_ns.class_("QuadSPIComponent", cg.Component) @@ -157,7 +159,7 @@ def _validate_psram_dma(value: Any) -> bool: msg_prefix="PSRAM DMA", ), cv.require_framework_version(esp_idf=cv.Version(5, 5, 3)), - cv.requires_component("psram"), + cv.requires_component(PSRAM_DOMAIN), )(value) diff --git a/esphome/components/spi_device/__init__.py b/esphome/components/spi_device/__init__.py index bd2bfe6452..3f64dfc3d1 100644 --- a/esphome/components/spi_device/__init__.py +++ b/esphome/components/spi_device/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["spi"] CODEOWNERS = ["@clydebarrow"] +DOMAIN = "spi_device" MULTI_CONF = True spi_device_ns = cg.esphome_ns.namespace("spi_device") diff --git a/esphome/components/spi_led_strip/__init__.py b/esphome/components/spi_led_strip/__init__.py index 850a1f6e02..2d856e1b10 100644 --- a/esphome/components/spi_led_strip/__init__.py +++ b/esphome/components/spi_led_strip/__init__.py @@ -1,2 +1,3 @@ CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["spi"] +DOMAIN = "spi_led_strip" diff --git a/esphome/components/split_buffer/__init__.py b/esphome/components/split_buffer/__init__.py index be7472936f..e66c0c6efa 100644 --- a/esphome/components/split_buffer/__init__.py +++ b/esphome/components/split_buffer/__init__.py @@ -1,4 +1,5 @@ CODEOWNERS = ["@jesserockz"] +DOMAIN = "split_buffer" # Allows split_buffer to be configured in yaml, to allow use of the C++ api. diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 00144adacd..fe096514d0 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -23,6 +23,7 @@ from esphome.helpers import docs_url AUTO_LOAD = ["number", "switch"] CODEOWNERS = ["@kbx81"] +DOMAIN = "sprinkler" CONF_AUTO_ADVANCE_SWITCH = "auto_advance_switch" CONF_DIVIDER = "divider" diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index a3c264da58..eb9e61958f 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -322,13 +322,11 @@ optional SprinklerValveRunRequest::valve_as_opt() { SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; } Sprinkler::Sprinkler() : Sprinkler("") {} -Sprinkler::Sprinkler(const char *name) : name_(name) { - // The `name` is stored for dump_config logging - this->timer_.init(2); - // Timer names only need to be unique within this component instance - this->timer_.push_back({"sm", false, 0, 0, [this]() { this->sm_timer_callback_(); }}); - this->timer_.push_back({"vs", false, 0, 0, [this]() { this->valve_selection_callback_(); }}); -} +// The `name` is stored for dump_config logging; the scheduler keys each timer by its SprinklerTimerIndex +Sprinkler::Sprinkler(const char *name) + : name_(name), + timer_{{{false, 0, 0, [this]() { this->sm_timer_callback_(); }}, + {false, 0, 0, [this]() { this->valve_selection_callback_(); }}}} {} void Sprinkler::setup() { this->all_valves_off_(true); @@ -1510,7 +1508,7 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) { void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { if (this->timer_duration_(timer_index) > 0) { - this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), + this->set_timeout(static_cast(timer_index), this->timer_duration_(timer_index), this->timer_cbf_(timer_index)); this->timer_[timer_index].start_time = millis(); this->timer_[timer_index].active = true; @@ -1521,7 +1519,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) { this->timer_[timer_index].active = false; - return this->cancel_timeout(this->timer_[timer_index].name); + return this->cancel_timeout(static_cast(timer_index)); } bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; } diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 2499a0a591..c51824d883 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -7,6 +7,7 @@ #include "esphome/components/number/number.h" #include "esphome/components/switch/switch.h" +#include #include namespace esphome::sprinkler { @@ -49,7 +50,6 @@ struct SprinklerQueueItem { }; struct SprinklerTimer { - const char *name; bool active; uint32_t time; uint32_t start_time; @@ -564,8 +564,8 @@ class Sprinkler final : public Component { /// Sprinkler valve operator objects std::vector valve_op_{2}; - /// Valve control timers - FixedVector enforces that this can never grow beyond init() size - FixedVector timer_; + /// Valve control timers, indexed by SprinklerTimerIndex (also used as the scheduler id) + std::array timer_; /// Other Sprinkler instances we should be aware of (used to check if pumps are in use) std::vector other_controllers_; diff --git a/esphome/components/sps30/__init__.py b/esphome/components/sps30/__init__.py index e69de29bb2..4b06a323ab 100644 --- a/esphome/components/sps30/__init__.py +++ b/esphome/components/sps30/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sps30" diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 26eda3b4ea..9577f9d4f7 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -18,6 +18,8 @@ from esphome.const import ( from esphome.cpp_generator import MockObj from esphome.types import ConfigType +DOMAIN = "ssd1306_base" + ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base") SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer) SSD1306Model = ssd1306_base_ns.enum("SSD1306Model") diff --git a/esphome/components/ssd1306_i2c/__init__.py b/esphome/components/ssd1306_i2c/__init__.py index e69de29bb2..9389c74a44 100644 --- a/esphome/components/ssd1306_i2c/__init__.py +++ b/esphome/components/ssd1306_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ssd1306_i2c" diff --git a/esphome/components/ssd1306_spi/__init__.py b/esphome/components/ssd1306_spi/__init__.py index e69de29bb2..fc62e74a43 100644 --- a/esphome/components/ssd1306_spi/__init__.py +++ b/esphome/components/ssd1306_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ssd1306_spi" diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py index daa0345857..d66ae8d5c9 100644 --- a/esphome/components/ssd1322_base/__init__.py +++ b/esphome/components/ssd1322_base/__init__.py @@ -13,6 +13,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] +DOMAIN = "ssd1322_base" ssd1322_base_ns = cg.esphome_ns.namespace("ssd1322_base") SSD1322 = ssd1322_base_ns.class_("SSD1322", cg.PollingComponent, display.DisplayBuffer) diff --git a/esphome/components/ssd1322_spi/__init__.py b/esphome/components/ssd1322_spi/__init__.py index e69de29bb2..52b9de59b2 100644 --- a/esphome/components/ssd1322_spi/__init__.py +++ b/esphome/components/ssd1322_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ssd1322_spi" diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index e48a821825..fbbf329d53 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -13,6 +13,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] +DOMAIN = "ssd1325_base" ssd1325_base_ns = cg.esphome_ns.namespace("ssd1325_base") SSD1325 = ssd1325_base_ns.class_("SSD1325", cg.PollingComponent, display.DisplayBuffer) diff --git a/esphome/components/ssd1325_spi/__init__.py b/esphome/components/ssd1325_spi/__init__.py index e69de29bb2..218baca080 100644 --- a/esphome/components/ssd1325_spi/__init__.py +++ b/esphome/components/ssd1325_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ssd1325_spi" diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py index 462f197696..2c9c0faf3b 100644 --- a/esphome/components/ssd1327_base/__init__.py +++ b/esphome/components/ssd1327_base/__init__.py @@ -7,6 +7,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] +DOMAIN = "ssd1327_base" ssd1327_base_ns = cg.esphome_ns.namespace("ssd1327_base") SSD1327 = ssd1327_base_ns.class_("SSD1327", cg.PollingComponent, display.DisplayBuffer) diff --git a/esphome/components/ssd1327_i2c/__init__.py b/esphome/components/ssd1327_i2c/__init__.py index e69de29bb2..b093087fa1 100644 --- a/esphome/components/ssd1327_i2c/__init__.py +++ b/esphome/components/ssd1327_i2c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ssd1327_i2c" diff --git a/esphome/components/ssd1327_spi/__init__.py b/esphome/components/ssd1327_spi/__init__.py index e69de29bb2..f95fe10579 100644 --- a/esphome/components/ssd1327_spi/__init__.py +++ b/esphome/components/ssd1327_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ssd1327_spi" diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py index d1c0b585f8..2345acdf65 100644 --- a/esphome/components/ssd1331_base/__init__.py +++ b/esphome/components/ssd1331_base/__init__.py @@ -7,6 +7,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] +DOMAIN = "ssd1331_base" ssd1331_base_ns = cg.esphome_ns.namespace("ssd1331_base") SSD1331 = ssd1331_base_ns.class_("SSD1331", cg.PollingComponent, display.DisplayBuffer) diff --git a/esphome/components/ssd1331_spi/__init__.py b/esphome/components/ssd1331_spi/__init__.py index e69de29bb2..4e37ffcaba 100644 --- a/esphome/components/ssd1331_spi/__init__.py +++ b/esphome/components/ssd1331_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ssd1331_spi" diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py index a516b4d951..f693004d63 100644 --- a/esphome/components/ssd1351_base/__init__.py +++ b/esphome/components/ssd1351_base/__init__.py @@ -7,6 +7,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] +DOMAIN = "ssd1351_base" ssd1351_base_ns = cg.esphome_ns.namespace("ssd1351_base") SSD1351 = ssd1351_base_ns.class_("SSD1351", cg.PollingComponent, display.DisplayBuffer) diff --git a/esphome/components/ssd1351_spi/__init__.py b/esphome/components/ssd1351_spi/__init__.py index e69de29bb2..0cb07c8b5e 100644 --- a/esphome/components/ssd1351_spi/__init__.py +++ b/esphome/components/ssd1351_spi/__init__.py @@ -0,0 +1 @@ +DOMAIN = "ssd1351_spi" diff --git a/esphome/components/st7123/__init__.py b/esphome/components/st7123/__init__.py index 335bc238be..6e3a763eb4 100644 --- a/esphome/components/st7123/__init__.py +++ b/esphome/components/st7123/__init__.py @@ -2,5 +2,6 @@ import esphome.codegen as cg CODEOWNERS = ["@miniskipper"] DEPENDENCIES = ["i2c"] +DOMAIN = "st7123" st7123_ns = cg.esphome_ns.namespace("st7123") diff --git a/esphome/components/st7567_base/__init__.py b/esphome/components/st7567_base/__init__.py index 51225c5023..8fdc0a7f7a 100644 --- a/esphome/components/st7567_base/__init__.py +++ b/esphome/components/st7567_base/__init__.py @@ -14,6 +14,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@latonita"] +DOMAIN = "st7567_base" st7567_base_ns = cg.esphome_ns.namespace("st7567_base") ST7567 = st7567_base_ns.class_("ST7567", cg.PollingComponent, display.DisplayBuffer) diff --git a/esphome/components/st7567_i2c/__init__.py b/esphome/components/st7567_i2c/__init__.py index dd06cfffea..3c54c70741 100644 --- a/esphome/components/st7567_i2c/__init__.py +++ b/esphome/components/st7567_i2c/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@latonita"] +DOMAIN = "st7567_i2c" diff --git a/esphome/components/st7567_spi/__init__.py b/esphome/components/st7567_spi/__init__.py index dd06cfffea..ed60104857 100644 --- a/esphome/components/st7567_spi/__init__.py +++ b/esphome/components/st7567_spi/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@latonita"] +DOMAIN = "st7567_spi" diff --git a/esphome/components/st7701s/__init__.py b/esphome/components/st7701s/__init__.py index c58ce8a01e..812c6caa8f 100644 --- a/esphome/components/st7701s/__init__.py +++ b/esphome/components/st7701s/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "st7701s" diff --git a/esphome/components/st7735/__init__.py b/esphome/components/st7735/__init__.py index 62dfa6f413..176eb233e1 100644 --- a/esphome/components/st7735/__init__.py +++ b/esphome/components/st7735/__init__.py @@ -1,5 +1,7 @@ import esphome.codegen as cg +DOMAIN = "st7735" + st7735_ns = cg.esphome_ns.namespace("st7735") DEPRECATED_COMPONENT = """ diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 7eb70c5c23..94b94d17a9 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -218,7 +218,7 @@ static constexpr uint8_t PROGMEM 100 }; // 100 ms delay // clang-format on -static const char *const TAG = "st7735"; +ESPHOME_LOG_TAG(TAG, "st7735"); ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr, bool invert_colors) diff --git a/esphome/components/st7789v/__init__.py b/esphome/components/st7789v/__init__.py index 7915cf119c..e67f480912 100644 --- a/esphome/components/st7789v/__init__.py +++ b/esphome/components/st7789v/__init__.py @@ -1,5 +1,7 @@ import esphome.codegen as cg +DOMAIN = "st7789v" + st7789v_ns = cg.esphome_ns.namespace("st7789v") DEPRECATED_COMPONENT = """ diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 2e07e24522..343d196b50 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -4,7 +4,7 @@ namespace esphome::st7789v { -static const char *const TAG = "st7789v"; +ESPHOME_LOG_TAG(TAG, "st7789v"); #ifdef USE_ESP32 static constexpr size_t TEMP_BUFFER_SIZE = 1024; #else diff --git a/esphome/components/st7920/__init__.py b/esphome/components/st7920/__init__.py index e69de29bb2..a1512e4e57 100644 --- a/esphome/components/st7920/__init__.py +++ b/esphome/components/st7920/__init__.py @@ -0,0 +1 @@ +DOMAIN = "st7920" diff --git a/esphome/components/statsd/__init__.py b/esphome/components/statsd/__init__.py index 58d4be97fa..16df864082 100644 --- a/esphome/components/statsd/__init__.py +++ b/esphome/components/statsd/__init__.py @@ -14,6 +14,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["socket"] CODEOWNERS = ["@Links2004"] DEPENDENCIES = ["network"] +DOMAIN = "statsd" CONF_PREFIX = "prefix" diff --git a/esphome/components/status/__init__.py b/esphome/components/status/__init__.py index e69de29bb2..dee924f6cf 100644 --- a/esphome/components/status/__init__.py +++ b/esphome/components/status/__init__.py @@ -0,0 +1 @@ +DOMAIN = "status" diff --git a/esphome/components/status_led/__init__.py b/esphome/components/status_led/__init__.py index 700296c0df..8bd98c0ef8 100644 --- a/esphome/components/status_led/__init__.py +++ b/esphome/components/status_led/__init__.py @@ -5,6 +5,8 @@ from esphome.const import CONF_ID, CONF_PIN from esphome.core import CoroPriority, coroutine_with_priority from esphome.types import ConfigType +DOMAIN = "status_led" + status_led_ns = cg.esphome_ns.namespace("status_led") StatusLED = status_led_ns.class_("StatusLED", cg.Component) diff --git a/esphome/components/stcc4/__init__.py b/esphome/components/stcc4/__init__.py index e69de29bb2..d0f16805b8 100644 --- a/esphome/components/stcc4/__init__.py +++ b/esphome/components/stcc4/__init__.py @@ -0,0 +1 @@ +DOMAIN = "stcc4" diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 017fd95cd8..f5c1a30433 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import SafeExpType +DOMAIN = "stepper" IS_PLATFORM_COMPONENT = True stepper_ns = cg.esphome_ns.namespace("stepper") diff --git a/esphome/components/sts3x/__init__.py b/esphome/components/sts3x/__init__.py index e69de29bb2..d7d6491925 100644 --- a/esphome/components/sts3x/__init__.py +++ b/esphome/components/sts3x/__init__.py @@ -0,0 +1 @@ +DOMAIN = "sts3x" diff --git a/esphome/components/stts22h/__init__.py b/esphome/components/stts22h/__init__.py index a33c0b554b..15a864ef0c 100644 --- a/esphome/components/stts22h/__init__.py +++ b/esphome/components/stts22h/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@B48D81EFCC"] +DOMAIN = "stts22h" diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index d59f39b9c2..93cf588e02 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -25,6 +25,7 @@ from esphome.yaml_util import ( from .jinja import Jinja, JinjaError, Missing, Resolver, UndefinedError, has_jinja CODEOWNERS = ["@esphome/core"] +DOMAIN = "substitutions" _LOGGER = logging.getLogger(__name__) ContextVars = ChainMap[str, Any] @@ -517,7 +518,7 @@ def resolve_substitutions_block( """Resolve a deferred ``substitutions: !include file.yaml`` and validate the shape. The caller is responsible for wrapping the call in - ``cv.prepend_path(CONF_SUBSTITUTIONS)`` for error reporting. + ``cv.prepend_path(DOMAIN)`` for error reporting. ``command_line_substitutions`` seeds the filename context so ``substitutions: !include ${var}.yaml`` can reference CLI-provided vars. """ @@ -552,7 +553,7 @@ def do_substitution_pass( # Extract substitutions from config, overriding with substitutions coming from command line: # Use merge_dicts_ordered to preserve OrderedDict type for move_to_end() substitutions = config.pop(CONF_SUBSTITUTIONS, {}) - with cv.prepend_path(CONF_SUBSTITUTIONS): + with cv.prepend_path(DOMAIN): substitutions = resolve_substitutions_block( substitutions, command_line_substitutions ) diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index 73ddefe972..71441e9782 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter"] +DOMAIN = "sun" sun_ns = cg.esphome_ns.namespace("sun") Sun = sun_ns.class_("Sun") diff --git a/esphome/components/sun_gtil2/__init__.py b/esphome/components/sun_gtil2/__init__.py index 0f5ae27753..61643cf488 100644 --- a/esphome/components/sun_gtil2/__init__.py +++ b/esphome/components/sun_gtil2/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@Mat931"] +DOMAIN = "sun_gtil2" MULTI_CONF = True DEPENDENCIES = ["uart"] diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index c34a73a36d..e965b552f4 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -32,6 +32,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] +DOMAIN = "switch" IS_PLATFORM_COMPONENT = True DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 63c3711b10..c8b920d6cb 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -10,6 +10,7 @@ from esphome.core import ID, TimePeriod from esphome.cpp_generator import MockObj from esphome.types import ConfigType, TemplateArgsType +DOMAIN = "sx126x" MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] DEPENDENCIES = ["spi"] diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 68c56cdff4..ea7fe38ba3 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -10,6 +10,7 @@ from esphome.core import ID from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType +DOMAIN = "sx127x" MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] DEPENDENCIES = ["spi"] diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index 942911c7fe..f96479498d 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -29,6 +29,7 @@ CONF_SX1509_ID = "sx1509_id" AUTO_LOAD = ["key_provider", "gpio_expander"] DEPENDENCIES = ["i2c"] +DOMAIN = "sx1509" MULTI_CONF = True sx1509_ns = cg.esphome_ns.namespace("sx1509") diff --git a/esphome/components/sy6970/__init__.py b/esphome/components/sy6970/__init__.py index cb9d64aee7..90bd9f4691 100644 --- a/esphome/components/sy6970/__init__.py +++ b/esphome/components/sy6970/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@linkedupbits"] DEPENDENCIES = ["i2c"] +DOMAIN = "sy6970" MULTI_CONF = True CONF_SY6970_ID = "sy6970_id" diff --git a/esphome/components/syslog/__init__.py b/esphome/components/syslog/__init__.py index e25567c783..b48192ddd2 100644 --- a/esphome/components/syslog/__init__.py +++ b/esphome/components/syslog/__init__.py @@ -9,6 +9,7 @@ from esphome.cpp_types import Component, Parented from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] +DOMAIN = "syslog" DEPENDENCIES = ["udp", "logger", "time"] diff --git a/esphome/components/systa_bus/__init__.py b/esphome/components/systa_bus/__init__.py index 0695b43d41..49ac214d40 100644 --- a/esphome/components/systa_bus/__init__.py +++ b/esphome/components/systa_bus/__init__.py @@ -6,6 +6,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@Mat931"] +DOMAIN = "systa_bus" DEPENDENCIES = ["uart"] diff --git a/esphome/components/t6615/__init__.py b/esphome/components/t6615/__init__.py index e69de29bb2..bbbca72b13 100644 --- a/esphome/components/t6615/__init__.py +++ b/esphome/components/t6615/__init__.py @@ -0,0 +1 @@ +DOMAIN = "t6615" diff --git a/esphome/components/tas2780/__init__.py b/esphome/components/tas2780/__init__.py index 81e00ca74b..f1fed4b698 100644 --- a/esphome/components/tas2780/__init__.py +++ b/esphome/components/tas2780/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@remcom"] +DOMAIN = "tas2780" diff --git a/esphome/components/tas58xx/__init__.py b/esphome/components/tas58xx/__init__.py index 0931d7ba1f..56700aeafd 100644 --- a/esphome/components/tas58xx/__init__.py +++ b/esphome/components/tas58xx/__init__.py @@ -1,6 +1,7 @@ from esphome.config_helpers import filter_source_files_from_defines CODEOWNERS = ["@mrtoy-me", "@remcom"] +DOMAIN = "tas58xx" FILTER_SOURCE_FILES = filter_source_files_from_defines( { diff --git a/esphome/components/tc74/__init__.py b/esphome/components/tc74/__init__.py index c1407c7cad..1021a425b8 100644 --- a/esphome/components/tc74/__init__.py +++ b/esphome/components/tc74/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@sethgirvan"] +DOMAIN = "tc74" diff --git a/esphome/components/tca9548a/__init__.py b/esphome/components/tca9548a/__init__.py index 381b84cad6..a1945e97c5 100644 --- a/esphome/components/tca9548a/__init__.py +++ b/esphome/components/tca9548a/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@andreashergert1984"] +DOMAIN = "tca9548a" DEPENDENCIES = ["i2c"] diff --git a/esphome/components/tca9555/__init__.py b/esphome/components/tca9555/__init__.py index 6ae1afc883..a039872e16 100644 --- a/esphome/components/tca9555/__init__.py +++ b/esphome/components/tca9555/__init__.py @@ -15,6 +15,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@mobrembski"] +DOMAIN = "tca9555" AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] diff --git a/esphome/components/tcl112/__init__.py b/esphome/components/tcl112/__init__.py index e69de29bb2..03f3908817 100644 --- a/esphome/components/tcl112/__init__.py +++ b/esphome/components/tcl112/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tcl112" diff --git a/esphome/components/tcp_uart/__init__.py b/esphome/components/tcp_uart/__init__.py index 796a9fc48d..35326d77d5 100644 --- a/esphome/components/tcp_uart/__init__.py +++ b/esphome/components/tcp_uart/__init__.py @@ -24,6 +24,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@Bascht74"] DEPENDENCIES = ["network"] AUTO_LOAD = ["uart", "binary_sensor", "sensor", "socket"] +DOMAIN = "tcp_uart" MULTI_CONF = True CONF_DISCONNECTS = "disconnects" diff --git a/esphome/components/tcs34725/__init__.py b/esphome/components/tcs34725/__init__.py index e69de29bb2..cf99348a86 100644 --- a/esphome/components/tcs34725/__init__.py +++ b/esphome/components/tcs34725/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tcs34725" diff --git a/esphome/components/tee501/__init__.py b/esphome/components/tee501/__init__.py index e69de29bb2..d3790674e2 100644 --- a/esphome/components/tee501/__init__.py +++ b/esphome/components/tee501/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tee501" diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index 67aad11d0f..8caf020212 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@0hax"] +DOMAIN = "teleinfo" MULTI_CONF = True teleinfo_ns = cg.esphome_ns.namespace("teleinfo") diff --git a/esphome/components/tem3200/__init__.py b/esphome/components/tem3200/__init__.py index e69de29bb2..728db67d54 100644 --- a/esphome/components/tem3200/__init__.py +++ b/esphome/components/tem3200/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tem3200" diff --git a/esphome/components/template/__init__.py b/esphome/components/template/__init__.py index 6253af9090..542e78809c 100644 --- a/esphome/components/template/__init__.py +++ b/esphome/components/template/__init__.py @@ -1,3 +1,5 @@ import esphome.codegen as cg +DOMAIN = "template" + template_ns = cg.esphome_ns.namespace("template_") diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 59624a5f53..a708c83d09 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -117,8 +117,13 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = await alarm_control_panel.new_alarm_control_panel(config) await cg.register_component(var, config) - if CONF_CODES in config: - cg.add(var.set_codes(config[CONF_CODES])) + # Shared flash table ended by nullptr, so the panel stores only a pointer. + # requires_code_to_arm needs codes (validate_config) and defaults to false. + if codes := config.get(CONF_CODES): + table = cg.shared_progmem_array( + "alarm_codes", cg.const_char_ptr, [*codes, cg.nullptr] + ) + cg.add(var.set_codes(table)) if CONF_REQUIRES_CODE_TO_ARM in config: cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM])) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 293f025527..12ff0b5a4b 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -36,6 +36,11 @@ static const LogString *sensor_type_to_string(AlarmSensorType type) { #endif void TemplateAlarmControlPanel::dump_config() { + size_t num_codes = 0; + if (this->codes_ != nullptr) { + while (this->codes_[num_codes] != nullptr) + num_codes++; + } ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:\n" " Current State: %s\n" @@ -47,8 +52,8 @@ void TemplateAlarmControlPanel::dump_config() { " Pending Time: %" PRIu32 "s\n" " Trigger Time: %" PRIu32 "s\n" " Supported Features: %" PRIu32, - LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size(), - YESNO(!this->codes_.empty() && this->requires_code_to_arm_), (this->arming_away_time_ / 1000), + LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), num_codes, + YESNO(this->codes_ != nullptr && this->requires_code_to_arm_), (this->arming_away_time_ / 1000), (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR @@ -194,13 +199,12 @@ void TemplateAlarmControlPanel::loop() { } bool TemplateAlarmControlPanel::is_code_valid_(optional code) { - if (!this->codes_.empty()) { + if (this->codes_ != nullptr) { if (code.has_value()) { ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str()); - // Use strcmp for const char* comparison const char *code_cstr = code.value().c_str(); - for (const char *stored_code : this->codes_) { - if (strcmp(stored_code, code_cstr) == 0) + for (const char *const *stored = this->codes_; *stored != nullptr; stored++) { + if (strcmp(*stored, code_cstr) == 0) return true; } return false; diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 5888ce5e29..c977da10a9 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -61,7 +61,7 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl void setup() override; void loop() override; uint32_t get_supported_features() const override; - bool get_requires_code() const override { return !this->codes_.empty(); } + bool get_requires_code() const override { return this->codes_ != nullptr; } bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } @@ -83,14 +83,11 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); #endif - /** Set the codes (from initializer list). + /** Set the codes. * - * @param codes The list of valid codes + * @param codes nullptr-terminated table of valid codes; must outlive the panel */ - void set_codes(std::initializer_list codes) { this->codes_ = codes; } - - // Deleted overload to catch incorrect std::string usage at compile time - void set_codes(std::initializer_list codes) = delete; + void set_codes(const char *const *codes) { this->codes_ = codes; } /** set requires a code to arm * @@ -150,8 +147,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl uint32_t pending_time_; // the time in trigger uint32_t trigger_time_; - // a list of codes (const char* pointers to string literals in flash) - FixedVector codes_; + // nullptr-terminated table of codes, shared in flash + const char *const *codes_{nullptr}; // requires a code to arm bool requires_code_to_arm_ = false; bool supports_arm_home_ = false; diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 2f82f19ff5..fbbc09bd2d 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -23,6 +23,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType CODEOWNERS = ["@mauritskorse"] +DOMAIN = "text" IS_PLATFORM_COMPONENT = True text_ns = cg.esphome_ns.namespace("text") diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 08870db64a..e9c70f7a2e 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -39,6 +39,7 @@ DEVICE_CLASSES = [ ] +DOMAIN = "text_sensor" IS_PLATFORM_COMPONENT = True text_sensor_ns = cg.esphome_ns.namespace("text_sensor") diff --git a/esphome/components/tfluna/__init__.py b/esphome/components/tfluna/__init__.py index 6ba821b1ab..6914894ba7 100644 --- a/esphome/components/tfluna/__init__.py +++ b/esphome/components/tfluna/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@candrews"] DEPENDENCIES = ["i2c"] +DOMAIN = "tfluna" MULTI_CONF = True tfluna_ns = cg.esphome_ns.namespace("tfluna") diff --git a/esphome/components/tfluna/tfluna.cpp b/esphome/components/tfluna/tfluna.cpp index 0936a07f49..e3d7045d8e 100644 --- a/esphome/components/tfluna/tfluna.cpp +++ b/esphome/components/tfluna/tfluna.cpp @@ -17,7 +17,9 @@ static constexpr uint8_t TRIGGER_ONESHOT_REGISTER = 0x24; static constexpr uint8_t MAX_READ_ATTEMPTS = 5; static constexpr uint8_t READ_RETRY_MS = 5; ESPHOME_LOG_TAG(TAG, "tfluna"); -static const char *const READ_RETRY_TIMEOUT = "read_retry"; + +static constexpr uint32_t SETUP_TIMEOUT_ID = 0; +static constexpr uint32_t READ_RETRY_TIMEOUT_ID = 1; void TFLuna::dump_config() { ESP_LOGCONFIG(TAG, "TF-Luna (i2c):"); @@ -121,7 +123,7 @@ void TFLuna::read_data_timeout_() { } else { if (this->attempt_ < MAX_READ_ATTEMPTS) { this->attempt_++; - this->set_timeout(READ_RETRY_TIMEOUT, READ_RETRY_MS, [this]() { this->read_data_timeout_(); }); + this->set_timeout(READ_RETRY_TIMEOUT_ID, READ_RETRY_MS, [this]() { this->read_data_timeout_(); }); } else { this->status_set_warning("Hung device, restarting..."); this->restart(); @@ -130,7 +132,7 @@ void TFLuna::read_data_timeout_() { } void TFLuna::update() { - this->cancel_timeout(READ_RETRY_TIMEOUT); + this->cancel_timeout(READ_RETRY_TIMEOUT_ID); this->attempt_ = 0; if (!this->write_byte(TRIGGER_ONESHOT_REGISTER, 0x01)) { this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); @@ -145,7 +147,7 @@ void TFLuna::factory_reset() { return; } this->status_set_warning("Factory reset issued; waiting for device to become ready"); - this->set_timeout("_setup", 100, [this]() { this->setup(); }); + this->set_timeout(SETUP_TIMEOUT_ID, 100, [this]() { this->setup(); }); } void TFLuna::restart() { diff --git a/esphome/components/thermopro_ble/__init__.py b/esphome/components/thermopro_ble/__init__.py index e69de29bb2..f2f59ccbc2 100644 --- a/esphome/components/thermopro_ble/__init__.py +++ b/esphome/components/thermopro_ble/__init__.py @@ -0,0 +1 @@ +DOMAIN = "thermopro_ble" diff --git a/esphome/components/thermostat/__init__.py b/esphome/components/thermostat/__init__.py index e69de29bb2..7bed41dc56 100644 --- a/esphome/components/thermostat/__init__.py +++ b/esphome/components/thermostat/__init__.py @@ -0,0 +1 @@ +DOMAIN = "thermostat" diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index cedb9b3c9b..a017109e7d 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -46,7 +46,6 @@ DOMAIN = "time" time_ns = cg.esphome_ns.namespace("time") RealTimeClock = time_ns.class_("RealTimeClock", cg.PollingComponent) CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Component) -SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component) # C++ types for pre-parsed timezone struct generation DSTRuleType_cpp = time_ns.enum("DSTRuleType", is_class=True) @@ -357,11 +356,7 @@ TIME_SCHEMA = cv.Schema( }, validate_cron_keys, ), - cv.Optional(CONF_ON_TIME_SYNC): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncTrigger), - } - ), + cv.Optional(CONF_ON_TIME_SYNC): automation.validate_automation({}), } ).extend( # ``visibility=ADVANCED`` flags the inherited ``update_interval`` @@ -424,8 +419,7 @@ async def setup_time_core_(time_var, config): _emit_parsed_timezone_fields(parsed) on_time = config.get(CONF_ON_TIME, []) - on_time_sync = config.get(CONF_ON_TIME_SYNC, []) - if on_time or on_time_sync: + if on_time: cg.add_define("USE_TIME_TRIGGERS") for conf in on_time: @@ -447,11 +441,10 @@ async def setup_time_core_(time_var, config): await cg.register_component(trigger, conf) await automation.build_automation(trigger, [], conf) - for conf in on_time_sync: - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) - - await cg.register_component(trigger, conf) - await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TIME_SYNC, []): + await automation.build_callback_automation( + time_var, "add_on_time_sync_callback", [], conf + ) async def register_time(time_var, config): @@ -478,8 +471,8 @@ automation.register_apply_condition( # posix_tz.cpp is fully #ifdef'd on USE_TIME_TIMEZONE, set only when a -# timezone is configured or detected; automation.cpp holds the on_time and -# on_time_sync triggers and is #ifdef'd on USE_TIME_TRIGGERS. +# timezone is configured or detected; automation.cpp holds the on_time +# trigger and is #ifdef'd on USE_TIME_TRIGGERS. FILTER_SOURCE_FILES = filter_source_files_from_defines( { "posix_tz.cpp": "USE_TIME_TIMEZONE", diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 26956703e7..1c3d462566 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -94,10 +94,6 @@ void CronTrigger::add_days_of_week(const std::vector &days_of_week) { } float CronTrigger::get_setup_priority() const { return setup_priority::HARDWARE; } -SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) { - rtc->add_on_time_sync_callback([this]() { this->trigger(); }); -} - } // namespace esphome::time #endif // USE_TIME_TRIGGERS diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index 5f980690a4..5e70000ac3 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -43,14 +43,6 @@ class CronTrigger final : public Trigger<>, public Component { RealTimeClock *rtc_; optional last_check_; }; - -class SyncTrigger final : public Trigger<>, public Component { - public: - explicit SyncTrigger(RealTimeClock *rtc); - - protected: - RealTimeClock *rtc_; -}; } // namespace esphome::time #endif // USE_TIME_TRIGGERS diff --git a/esphome/components/time_based/__init__.py b/esphome/components/time_based/__init__.py index ce2f453bda..39ec2d9a8d 100644 --- a/esphome/components/time_based/__init__.py +++ b/esphome/components/time_based/__init__.py @@ -1,3 +1,5 @@ import esphome.codegen as cg +DOMAIN = "time_based" + time_based_ns = cg.esphome_ns.namespace("time_based") diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index 1b819088ca..586b69d603 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -10,12 +10,14 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, ) +from esphome.components.logger import DOMAIN as LOGGER_DOMAIN import esphome.config_validation as cv from esphome.const import CONF_HARDWARE_UART, CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] CONFLICTS_WITH = ["usb_host"] +DOMAIN = "tinyusb" CONF_ON_MOUNT = "on_mount" CONF_ON_UNMOUNT = "on_unmount" @@ -95,7 +97,7 @@ def _final_validate(config: ConfigType) -> None: # tinyusb owns the USB OTG peripheral. The logger's USB_CDC backend routes # the ROM console through that same peripheral, so the two cannot coexist. # (USB_SERIAL_JTAG is a separate peripheral and is fine alongside tinyusb.) - logger_config = full_config.get("logger") + logger_config = full_config.get(LOGGER_DOMAIN) if logger_config and logger_config.get(CONF_HARDWARE_UART) == "USB_CDC": raise cv.Invalid( "'tinyusb' cannot be used with 'logger.hardware_uart: USB_CDC' " diff --git a/esphome/components/tlc59208f/__init__.py b/esphome/components/tlc59208f/__init__.py index 1af9f66b95..5d719e40ba 100644 --- a/esphome/components/tlc59208f/__init__.py +++ b/esphome/components/tlc59208f/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType DEPENDENCIES = ["i2c"] +DOMAIN = "tlc59208f" MULTI_CONF = True tlc59208f_ns = cg.esphome_ns.namespace("tlc59208f") diff --git a/esphome/components/tlc5947/__init__.py b/esphome/components/tlc5947/__init__.py index 05e2ef037d..71247dd13e 100644 --- a/esphome/components/tlc5947/__init__.py +++ b/esphome/components/tlc5947/__init__.py @@ -16,6 +16,7 @@ from esphome.types import ConfigType CONF_LAT_PIN = "lat_pin" CODEOWNERS = ["@rnauber"] +DOMAIN = "tlc5947" tlc5947_ns = cg.esphome_ns.namespace("tlc5947") TLC5947 = tlc5947_ns.class_("TLC5947", cg.Component) diff --git a/esphome/components/tlc5971/__init__.py b/esphome/components/tlc5971/__init__.py index 5b9027e61a..26beec280e 100644 --- a/esphome/components/tlc5971/__init__.py +++ b/esphome/components/tlc5971/__init__.py @@ -8,6 +8,7 @@ from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID, CONF_NUM_CHIPS from esphome.types import ConfigType CODEOWNERS = ["@IJIJI"] +DOMAIN = "tlc5971" tlc5971_ns = cg.esphome_ns.namespace("tlc5971") TLC5971 = tlc5971_ns.class_("TLC5971", cg.Component) diff --git a/esphome/components/tm1621/__init__.py b/esphome/components/tm1621/__init__.py index 2e88d4f366..1b78f353ca 100644 --- a/esphome/components/tm1621/__init__.py +++ b/esphome/components/tm1621/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@Philippe12"] +DOMAIN = "tm1621" diff --git a/esphome/components/tm1637/__init__.py b/esphome/components/tm1637/__init__.py index e69de29bb2..c8b08abf03 100644 --- a/esphome/components/tm1637/__init__.py +++ b/esphome/components/tm1637/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tm1637" diff --git a/esphome/components/tm1638/__init__.py b/esphome/components/tm1638/__init__.py index e69de29bb2..4500a10ff4 100644 --- a/esphome/components/tm1638/__init__.py +++ b/esphome/components/tm1638/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tm1638" diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index dfe475f7f2..14aa1c8842 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( from esphome.types import ConfigType CODEOWNERS = ["@mrtoy-me"] +DOMAIN = "tm1651" CONF_LEVEL_PERCENT = "level_percent" diff --git a/esphome/components/tmp102/__init__.py b/esphome/components/tmp102/__init__.py index 3e32a230f2..8510f18769 100644 --- a/esphome/components/tmp102/__init__.py +++ b/esphome/components/tmp102/__init__.py @@ -7,3 +7,5 @@ reading temperatures to a resolution of 0.0625°C. https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf """ + +DOMAIN = "tmp102" diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index 4f10555324..d851bd925f 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -6,6 +6,8 @@ namespace esphome::tmp102 { ESPHOME_LOG_TAG(TAG, "tmp102"); +static constexpr uint32_t READ_TEMP_TIMEOUT_ID = 0; + static const uint8_t TMP102_ADDRESS = 0x48; static const uint8_t TMP102_REGISTER_TEMPERATURE = 0x00; static const uint8_t TMP102_REGISTER_CONFIGURATION = 0x01; @@ -29,7 +31,7 @@ void TMP102Component::update() { this->status_set_warning(); return; } - this->set_timeout("read_temp", 50, [this]() { + this->set_timeout(READ_TEMP_TIMEOUT_ID, 50, [this]() { int16_t raw_temperature; if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { this->status_set_warning(); diff --git a/esphome/components/tmp1075/__init__.py b/esphome/components/tmp1075/__init__.py index ddd04ad11a..81c0e50033 100644 --- a/esphome/components/tmp1075/__init__.py +++ b/esphome/components/tmp1075/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@sybrenstuvel"] +DOMAIN = "tmp1075" diff --git a/esphome/components/tmp117/__init__.py b/esphome/components/tmp117/__init__.py index e69de29bb2..e7429bd52e 100644 --- a/esphome/components/tmp117/__init__.py +++ b/esphome/components/tmp117/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tmp117" diff --git a/esphome/components/tof10120/__init__.py b/esphome/components/tof10120/__init__.py index e69de29bb2..89b7cffed4 100644 --- a/esphome/components/tof10120/__init__.py +++ b/esphome/components/tof10120/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tof10120" diff --git a/esphome/components/tormatic/__init__.py b/esphome/components/tormatic/__init__.py index 7f3f05a3cd..e26792c1d3 100644 --- a/esphome/components/tormatic/__init__.py +++ b/esphome/components/tormatic/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ti-mo"] +DOMAIN = "tormatic" diff --git a/esphome/components/toshiba/__init__.py b/esphome/components/toshiba/__init__.py index e69de29bb2..1e5ec1fc27 100644 --- a/esphome/components/toshiba/__init__.py +++ b/esphome/components/toshiba/__init__.py @@ -0,0 +1 @@ +DOMAIN = "toshiba" diff --git a/esphome/components/total_daily_energy/__init__.py b/esphome/components/total_daily_energy/__init__.py index e69de29bb2..d3880a5084 100644 --- a/esphome/components/total_daily_energy/__init__.py +++ b/esphome/components/total_daily_energy/__init__.py @@ -0,0 +1 @@ +DOMAIN = "total_daily_energy" diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index c8b918007b..7a23d14069 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -23,6 +23,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@jesserockz", "@nielsnl68"] DEPENDENCIES = ["display"] +DOMAIN = "touchscreen" IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/tsl2561/__init__.py b/esphome/components/tsl2561/__init__.py index e69de29bb2..9986b7fd3e 100644 --- a/esphome/components/tsl2561/__init__.py +++ b/esphome/components/tsl2561/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tsl2561" diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index 989a15f054..d1d1d66a26 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -5,6 +5,8 @@ namespace esphome::tsl2561 { ESPHOME_LOG_TAG(TAG, "tsl2561"); +static constexpr uint32_t READ_TIMEOUT_ID = 0; + static const uint8_t TSL2561_COMMAND_BIT = 0x80; static const uint8_t TSL2561_WORD_BIT = 0x20; static const uint8_t TSL2561_REGISTER_CONTROL = 0x00; @@ -60,7 +62,7 @@ void TSL2561Sensor::update() { // Make sure the data is there when we will read it. auto timeout = static_cast(this->get_integration_time_ms_() + 20); - this->set_timeout("illuminance", timeout, [this]() { this->read_data_(); }); + this->set_timeout(READ_TIMEOUT_ID, timeout, [this]() { this->read_data_(); }); } float TSL2561Sensor::calculate_lx_(uint16_t ch0, uint16_t ch1) { diff --git a/esphome/components/tsl2591/__init__.py b/esphome/components/tsl2591/__init__.py index 63331641c5..c611ad0ebd 100644 --- a/esphome/components/tsl2591/__init__.py +++ b/esphome/components/tsl2591/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@wjcarpenter"] +DOMAIN = "tsl2591" diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index dfc0dfedf4..f1f9ae9cfd 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -6,6 +6,8 @@ namespace esphome::tsl2591 { ESPHOME_LOG_TAG(TAG, "tsl2591.sensor"); +static constexpr uint32_t ADC_WAIT_INTERVAL_ID = 0; + // Various constants used in TSL2591 register manipulation #define TSL2591_COMMAND_BIT (0xA0) // 1010 0000: bits 7 and 5 for 'command, normal' #define TSL2591_ENABLE_POWERON (0x01) // Flag for ENABLE register, to enable @@ -163,19 +165,17 @@ void TSL2591Component::process_update_() { this->status_clear_warning(); } -#define interval_name "tsl2591_interval_for_update" - void TSL2591Component::interval_function_for_update_() { if (!this->is_adc_valid()) { uint64_t now = millis(); ESP_LOGD(TAG, "Elapsed %3llu ms; still waiting for valid ADC", (now - this->interval_start_)); if (now > this->interval_timeout_) { ESP_LOGW(TAG, "Interval timeout for '%s' expired before ADCs became valid", this->name_); - this->cancel_interval(interval_name); + this->cancel_interval(ADC_WAIT_INTERVAL_ID); } return; } - this->cancel_interval(interval_name); + this->cancel_interval(ADC_WAIT_INTERVAL_ID); this->process_update_(); } @@ -191,7 +191,7 @@ void TSL2591Component::update() { } else { this->interval_start_ = millis(); this->interval_timeout_ = this->interval_start_ + 620; - this->set_interval(interval_name, 100, [this] { this->interval_function_for_update_(); }); + this->set_interval(ADC_WAIT_INTERVAL_ID, 100, [this] { this->interval_function_for_update_(); }); } } } diff --git a/esphome/components/tt21100/__init__.py b/esphome/components/tt21100/__init__.py index a309d34beb..a8f0ec0dd3 100644 --- a/esphome/components/tt21100/__init__.py +++ b/esphome/components/tt21100/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg CODEOWNERS = ["@kroimon"] +DOMAIN = "tt21100" tt21100_ns = cg.esphome_ns.namespace("tt21100") diff --git a/esphome/components/ttp229_bsf/__init__.py b/esphome/components/ttp229_bsf/__init__.py index 9e2a4ad114..1b10940570 100644 --- a/esphome/components/ttp229_bsf/__init__.py +++ b/esphome/components/ttp229_bsf/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_SCL_PIN, CONF_SDO_PIN from esphome.types import ConfigType AUTO_LOAD = ["binary_sensor"] +DOMAIN = "ttp229_bsf" CONF_TTP229_ID = "ttp229_id" ttp229_bsf_ns = cg.esphome_ns.namespace("ttp229_bsf") diff --git a/esphome/components/ttp229_lsf/__init__.py b/esphome/components/ttp229_lsf/__init__.py index d32f72fcce..e89b177a3e 100644 --- a/esphome/components/ttp229_lsf/__init__.py +++ b/esphome/components/ttp229_lsf/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType DEPENDENCIES = ["i2c"] AUTO_LOAD = ["binary_sensor"] +DOMAIN = "ttp229_lsf" CONF_TTP229_ID = "ttp229_id" ttp229_lsf_ns = cg.esphome_ns.namespace("ttp229_lsf") diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 2cc60e225c..bf392c7826 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -2,9 +2,11 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import time, uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID +from esphome.cpp_generator import MockObj DEPENDENCIES = ["uart"] +DOMAIN = "tuya" CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints" @@ -16,6 +18,8 @@ tuya_ns = cg.esphome_ns.namespace("tuya") TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True) Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) +TuyaDatapoint = tuya_ns.struct("TuyaDatapoint") + DPTYPE_ANY = "any" DPTYPE_RAW = "raw" DPTYPE_BOOL = "bool" @@ -25,60 +29,18 @@ DPTYPE_STRING = "string" DPTYPE_ENUM = "enum" DPTYPE_BITMASK = "bitmask" +# Automation argument type, plus the expected TuyaDatapointType and the field forwarded DATAPOINT_TYPES = { - DPTYPE_ANY: tuya_ns.struct("TuyaDatapoint"), - DPTYPE_RAW: cg.std_vector.template(cg.uint8), - DPTYPE_BOOL: cg.bool_, - DPTYPE_INT: cg.int_, - DPTYPE_UINT: cg.uint32, - DPTYPE_STRING: cg.std_string, - DPTYPE_ENUM: cg.uint8, - DPTYPE_BITMASK: cg.uint32, + DPTYPE_ANY: (TuyaDatapoint, None, None), + DPTYPE_RAW: (cg.std_vector.template(cg.uint8), TuyaDatapointType.RAW, "value_raw"), + DPTYPE_BOOL: (cg.bool_, TuyaDatapointType.BOOLEAN, "value_bool"), + DPTYPE_INT: (cg.int_, TuyaDatapointType.INTEGER, "value_int"), + DPTYPE_UINT: (cg.uint32, TuyaDatapointType.INTEGER, "value_uint"), + DPTYPE_STRING: (cg.std_string, TuyaDatapointType.STRING, "value_string"), + DPTYPE_ENUM: (cg.uint8, TuyaDatapointType.ENUM, "value_enum"), + DPTYPE_BITMASK: (cg.uint32, TuyaDatapointType.BITMASK, "value_bitmask"), } -DATAPOINT_TRIGGERS = { - DPTYPE_ANY: tuya_ns.class_( - "TuyaDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ANY]), - ), - DPTYPE_RAW: tuya_ns.class_( - "TuyaRawDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_RAW]), - ), - DPTYPE_BOOL: tuya_ns.class_( - "TuyaBoolDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BOOL]), - ), - DPTYPE_INT: tuya_ns.class_( - "TuyaIntDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_INT]), - ), - DPTYPE_UINT: tuya_ns.class_( - "TuyaUIntDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_UINT]), - ), - DPTYPE_STRING: tuya_ns.class_( - "TuyaStringDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_STRING]), - ), - DPTYPE_ENUM: tuya_ns.class_( - "TuyaEnumDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ENUM]), - ), - DPTYPE_BITMASK: tuya_ns.class_( - "TuyaBitmaskDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BITMASK]), - ), -} - - -def assign_declare_id(value): - value = value.copy() - value[CONF_TRIGGER_ID] = cv.declare_id( - DATAPOINT_TRIGGERS[value[CONF_DATAPOINT_TYPE]] - )(value[CONF_TRIGGER_ID].id) - return value - CONF_TUYA_ID = "tuya_id" CONFIG_SCHEMA = ( @@ -92,15 +54,11 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_STATUS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DATAPOINT_TRIGGERS[DPTYPE_ANY] - ), cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, cv.Optional(CONF_DATAPOINT_TYPE, default=DPTYPE_ANY): cv.one_of( - *DATAPOINT_TRIGGERS, lower=True + *DATAPOINT_TYPES, lower=True ), - }, - extra_validators=assign_declare_id, + } ), } ) @@ -123,9 +81,11 @@ async def to_code(config): for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]: cg.add(var.add_ignore_mcu_update_on_datapoints(dp)) for conf in config.get(CONF_ON_DATAPOINT_UPDATE, []): - trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], var, conf[CONF_SENSOR_DATAPOINT] - ) - await automation.build_automation( - trigger, [(DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]], "x")], conf + type_, expected, field = DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]] + forward = None + if expected is not None: + forward = [getattr(MockObj("x", ".").expect_type(expected), field)] + callback = await automation.build_trigger_callback( + [(type_, "x")], conf, params=[(TuyaDatapoint, "x")], forward=forward ) + cg.add(var.register_listener(conf[CONF_SENSOR_DATAPOINT], callback)) diff --git a/esphome/components/tuya/automation.cpp b/esphome/components/tuya/automation.cpp deleted file mode 100644 index a7eacf8e9a..0000000000 --- a/esphome/components/tuya/automation.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "esphome/core/log.h" - -#include "automation.h" - -ESPHOME_LOG_TAG(TAG, "tuya.automation"); - -namespace esphome::tuya { - -void check_expected_datapoint(const TuyaDatapoint &dp, TuyaDatapointType expected) { - if (dp.type != expected) { - ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", dp.id, - static_cast(expected), static_cast(dp.type)); - } -} - -TuyaRawDatapointUpdateTrigger::TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::RAW); - this->trigger(dp.value_raw); - }); -} - -TuyaBoolDatapointUpdateTrigger::TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::BOOLEAN); - this->trigger(dp.value_bool); - }); -} - -TuyaIntDatapointUpdateTrigger::TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::INTEGER); - this->trigger(dp.value_int); - }); -} - -TuyaUIntDatapointUpdateTrigger::TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::INTEGER); - this->trigger(dp.value_uint); - }); -} - -TuyaStringDatapointUpdateTrigger::TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::STRING); - this->trigger(dp.value_string); - }); -} - -TuyaEnumDatapointUpdateTrigger::TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::ENUM); - this->trigger(dp.value_enum); - }); -} - -TuyaBitmaskDatapointUpdateTrigger::TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::BITMASK); - this->trigger(dp.value_bitmask); - }); -} - -} // namespace esphome::tuya diff --git a/esphome/components/tuya/automation.h b/esphome/components/tuya/automation.h deleted file mode 100644 index 0cd63a76be..0000000000 --- a/esphome/components/tuya/automation.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "tuya.h" - -#include - -namespace esphome::tuya { - -class TuyaDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { this->trigger(dp); }); - } -}; - -class TuyaRawDatapointUpdateTrigger final : public Trigger> { - public: - explicit TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaBoolDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaIntDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaUIntDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaStringDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaEnumDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaBitmaskDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -} // namespace esphome::tuya diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 992dde1533..b23dcc946b 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -19,6 +19,10 @@ namespace esphome::tuya { ESPHOME_LOG_TAG(TAG, "tuya"); + +static constexpr uint32_t HEARTBEAT_INTERVAL_ID = 0; +static constexpr uint32_t WIFI_STATUS_INTERVAL_ID = 1; +static constexpr uint32_t DATAPOINT_DUMP_TIMEOUT_ID = 2; static const int COMMAND_DELAY = 10; static const int RECEIVE_TIMEOUT = 300; static const int MAX_RETRIES = 5; @@ -34,7 +38,7 @@ static bool network_is_connected() { } void Tuya::setup() { - this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); + this->set_interval(HEARTBEAT_INTERVAL_ID, 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); if (this->status_pin_ != nullptr) { this->status_pin_->digital_write(false); } @@ -223,7 +227,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->status_pin_reported_); } ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_->get_pin()); - this->set_interval("wifi", 1000, [this] { this->set_status_pin_(); }); + this->set_interval(WIFI_STATUS_INTERVAL_ID, 1000, [this] { this->set_status_pin_(); }); } else { ESP_LOGW(TAG, "MCU reported status_pin %i but no status_pin was configured; running in limited mode.", this->status_pin_reported_); @@ -231,7 +235,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff } else { this->init_state_ = TuyaInitState::INIT_WIFI; ESP_LOGV(TAG, "Configured WIFI_STATE periodic send"); - this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); }); + this->set_interval(WIFI_STATUS_INTERVAL_ID, 1000, [this] { this->send_wifi_status_(); }); } } break; @@ -279,7 +283,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff case TuyaCommandType::DATAPOINT_REPORT_SYNC: if (this->init_state_ == TuyaInitState::INIT_DATAPOINT) { this->init_state_ = TuyaInitState::INIT_DONE; - this->set_timeout("datapoint_dump", 1000, [this] { this->dump_config(); }); + this->set_timeout(DATAPOINT_DUMP_TIMEOUT_ID, 1000, [this] { this->dump_config(); }); this->initialized_callback_.call(); } this->handle_datapoints_(buffer, len); @@ -478,7 +482,7 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { // Run through listeners for (auto &listener : this->listeners_) { if (listener.datapoint_id == datapoint.id) - listener.on_datapoint(datapoint); + listener.on_datapoint.call(datapoint); } } } @@ -792,20 +796,27 @@ void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datap this->send_command_(TuyaCommand{.cmd = TuyaCommandType::DATAPOINT_DELIVER, .payload = buffer}); } -void Tuya::register_listener(uint8_t datapoint_id, const std::function &func) { - auto listener = TuyaDatapointListener{ +void Tuya::register_listener_(uint8_t datapoint_id, Callback func) { + this->listeners_.push_back(TuyaDatapointListener{ .datapoint_id = datapoint_id, .on_datapoint = func, - }; - this->listeners_.push_back(listener); + }); // Run through existing datapoints for (auto &datapoint : this->datapoints_) { if (datapoint.id == datapoint_id) - func(datapoint); + func.call(datapoint); } } +const TuyaDatapoint &TuyaDatapoint::expect_type(TuyaDatapointType expected) const { + if (this->type != expected) { + ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", this->id, + static_cast(expected), static_cast(this->type)); + } + return *this; +} + TuyaInitState Tuya::get_init_state() { return this->init_state_; } } // namespace esphome::tuya diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index b8bf4e0ab1..f074d30162 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -37,11 +37,14 @@ struct TuyaDatapoint { }; std::string value_string; std::vector value_raw; + + /// Log a warning when this datapoint is not of type `expected`, and return it. + const TuyaDatapoint &expect_type(TuyaDatapointType expected) const; }; struct TuyaDatapointListener { uint8_t datapoint_id; - std::function on_datapoint; + Callback on_datapoint; }; enum class TuyaCommandType : uint8_t { @@ -91,7 +94,9 @@ class Tuya final : public Component, public uart::UARTDevice { void setup() override; void loop() override; void dump_config() override; - void register_listener(uint8_t datapoint_id, const std::function &func); + template void register_listener(uint8_t datapoint_id, F &&func) { + this->register_listener_(datapoint_id, Callback::create(std::forward(func))); + } void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value); void set_boolean_datapoint_value(uint8_t datapoint_id, bool value); void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value); @@ -117,6 +122,7 @@ class Tuya final : public Component, public uart::UARTDevice { } protected: + void register_listener_(uint8_t datapoint_id, Callback func); void handle_char_(uint8_t c); void handle_datapoints_(const uint8_t *buffer, size_t len); optional get_datapoint_(uint8_t datapoint_id); diff --git a/esphome/components/tx20/__init__.py b/esphome/components/tx20/__init__.py index e69de29bb2..83dbfc84f6 100644 --- a/esphome/components/tx20/__init__.py +++ b/esphome/components/tx20/__init__.py @@ -0,0 +1 @@ +DOMAIN = "tx20" diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 25fbfb7599..2250b9d508 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -533,6 +533,25 @@ def final_validate_device_schema( ) +def subtree_references_uart( + node: object, uart_id: str, conf_key: str = CONF_UART_ID +) -> bool: + """Return True if any dict in the subtree has a conf_key entry naming this bus. + + For the final validation of a component that needs a UART for itself. Bare + `id:` references (a uart.write action) and lambdas are not found. + """ + if isinstance(node, dict): + return any( + (key == conf_key and str(value) == uart_id) + or subtree_references_uart(value, uart_id, conf_key) + for key, value in node.items() + ) + if isinstance(node, list): + return any(subtree_references_uart(item, uart_id, conf_key) for item in node) + return False + + async def register_uart_device(var, config): """Register a UART device, setting up all the internal values. diff --git a/esphome/components/uart/uart_component.cpp b/esphome/components/uart/uart_component.cpp index 6516174635..82bc592702 100644 --- a/esphome/components/uart/uart_component.cpp +++ b/esphome/components/uart/uart_component.cpp @@ -1,9 +1,15 @@ #include "uart_component.h" +#include "esphome/core/application.h" + +#include namespace esphome::uart { ESPHOME_LOG_TAG(TAG, "uart"); +// Keeps the pacing product in 32 bits up to about 10 Mbaud. +static constexpr uint32_t MAX_PACE_SPAN_MS = 4000; + bool UARTComponent::check_read_timeout_(size_t len) { if (this->available() >= len) return true; @@ -19,6 +25,19 @@ bool UARTComponent::check_read_timeout_(size_t len) { return true; } +size_t UARTComponent::paced_write_room(uint32_t last_write_ms) { + size_t room = this->available_for_write(); + if (room != SIZE_MAX) { + return room; + } + // A pass woken early writes little. + uint32_t span = + std::min({App.get_loop_component_start_time() - last_write_ms, App.get_loop_interval(), MAX_PACE_SPAN_MS}); + // 10 bits per byte on the line. + uint32_t paced = this->baud_rate_ / 10 * span / 1000; + return std::max(1, paced); +} + void UARTComponent::set_rx_full_threshold_ms(uint8_t time) { uint8_t bytelength = this->data_bits_ + this->stop_bits_ + 1; if (this->parity_ != UARTParityOptions::UART_CONFIG_PARITY_NONE) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 8f143b7977..2543cb5476 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -86,6 +86,10 @@ class UARTComponent { // Platforms that cannot tell return SIZE_MAX: write_array() takes everything and may block. virtual size_t available_for_write() { return SIZE_MAX; } + // Bytes a write can take now. Where available_for_write() cannot tell (SIZE_MAX), paces to the line time since + // last_write_ms, a loop start time: at most one loop interval and 4 s, at least one byte. + size_t paced_write_room(uint32_t last_write_ms); + // Pure virtual method to block until all bytes have been written to the UART bus. // @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. virtual UARTFlushResult flush() = 0; diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 41fae283b4..cccb7289fb 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -238,6 +238,10 @@ void IDFUARTComponent::load_settings(bool dump_config) { this->mark_failed(); return; } +#ifdef USE_GPIO_HOLD + // Release held pins so the UART peripheral can drive them + this->set_pins_hold_(false); +#endif #ifdef USE_UART_WAKE_LOOP_ON_RX // Register ISR callback to wake the main loop when UART data arrives. @@ -514,7 +518,20 @@ void IDFUARTComponent::on_shutdown() { if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); } +#ifdef USE_GPIO_HOLD + // Hold the pins again so they keep their state through the reset + this->set_pins_hold_(true); +#endif } +#ifdef USE_GPIO_HOLD +void IDFUARTComponent::set_pins_hold_(bool hold) { + for (auto *pin : {this->tx_pin_, this->rx_pin_, this->flow_control_pin_}) { + if (pin != nullptr) + pin->set_hold(hold); + } +} +#endif + } // namespace esphome::uart #endif // USE_ESP32 diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 7c93b74fbd..260117c429 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -90,6 +90,9 @@ class IDFUARTComponent final : public UARTComponent, public Component { protected: void check_logger_conflict() override; +#ifdef USE_GPIO_HOLD + void set_pins_hold_(bool hold); +#endif uint32_t line_inversion_mask_(); // Re-applies what uart_param_config() resets: inversion, RX threshold/timeout, mode. esp_err_t apply_line_settings_(); diff --git a/esphome/components/uart_tcp/__init__.py b/esphome/components/uart_tcp/__init__.py index 6e7a6fef32..9d62d48d7c 100644 --- a/esphome/components/uart_tcp/__init__.py +++ b/esphome/components/uart_tcp/__init__.py @@ -7,6 +7,7 @@ from esphome.components.const import ( CONF_RECONNECT_INTERVAL, CONF_ROLE, ) +from esphome.components.tcp_uart import DOMAIN as TCP_UART_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_DEBUG, @@ -70,18 +71,6 @@ CONFIG_SCHEMA = cv.All( ) -def _subtree_references_uart(node: object, uart_id: str) -> bool: - if isinstance(node, dict): - return any( - (key == CONF_UART_ID and str(value) == uart_id) - or _subtree_references_uart(value, uart_id) - for key, value in node.items() - ) - if isinstance(node, list): - return any(_subtree_references_uart(item, uart_id) for item in node) - return False - - def _reject_dummy_receiver(uart_conf: ConfigType) -> ConfigType: debug = uart_conf.get(CONF_DEBUG) if isinstance(debug, dict) and debug.get(CONF_DUMMY_RECEIVER): @@ -110,7 +99,7 @@ def _final_validate(config: ConfigType) -> ConfigType: # Bare `id:` references (a uart.write action) and lambdas are not caught. if not CORE.testing_mode: for domain, domain_conf in full_config.items(): - if domain != DOMAIN and _subtree_references_uart(domain_conf, uart_id): + if domain != DOMAIN and uart.subtree_references_uart(domain_conf, uart_id): raise cv.Invalid( f"The UART '{uart_id}' is also used by '{domain}'. " "uart_tcp requires exclusive use of that UART.", @@ -125,7 +114,7 @@ def _final_validate(config: ConfigType) -> ConfigType: ports = data.setdefault(CONF_PORT, set()) if port in ports or any( conf[CONF_ROLE] == "server" and conf[CONF_PORT] == port - for conf in full_config.get("tcp_uart", []) + for conf in full_config.get(TCP_UART_DOMAIN, []) ): raise cv.Invalid( f"Port {port} is already the listen port of another uart_tcp " diff --git a/esphome/components/uart_tcp/uart_tcp.cpp b/esphome/components/uart_tcp/uart_tcp.cpp index bc75eb04c7..d634ac579f 100644 --- a/esphome/components/uart_tcp/uart_tcp.cpp +++ b/esphome/components/uart_tcp/uart_tcp.cpp @@ -11,9 +11,6 @@ namespace esphome::uart_tcp { ESPHOME_LOG_TAG(TAG, "uart_tcp"); -// Keeps the pacing product in 32 bits up to about 10 Mbaud. -static constexpr uint32_t MAX_PACE_SPAN_MS = 4000; - void UartTcp::setup() { this->link_.begin(TAG); #ifdef USE_SOCKET_TCP_LISTENER @@ -75,16 +72,7 @@ void UartTcp::sync_link_() { void UartTcp::read_socket_() { // A hardware write blocks until the driver takes every byte. Leave what does // not fit in the socket, so TCP flow control throttles the peer. - size_t room = this->parent_->available_for_write(); - if (room == SIZE_MAX) { - // Capacity unknown on this platform; pace to the UART time since the last write, - // at most one loop interval and 4 s, so a pass woken early by the socket writes little. - uint32_t span = std::min( - {App.get_loop_component_start_time() - this->last_write_ms_, App.get_loop_interval(), MAX_PACE_SPAN_MS}); - // 10 bits per byte on the line. - uint32_t paced = this->parent_->get_baud_rate() / 10 * span / 1000; - room = std::max(1, paced); - } + size_t room = this->parent_->paced_write_room(this->last_write_ms_); if (room == 0) { this->rx_pending_ = true; return; diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index cd3cb44ae1..81149f056b 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -21,6 +21,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] AUTO_LOAD = ["socket"] +DOMAIN = "udp" MULTI_CONF = True udp_ns = cg.esphome_ns.namespace("udp") diff --git a/esphome/components/ufire_ec/__init__.py b/esphome/components/ufire_ec/__init__.py index 08f36c7934..e5461b4ff7 100644 --- a/esphome/components/ufire_ec/__init__.py +++ b/esphome/components/ufire_ec/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@pvizeli"] +DOMAIN = "ufire_ec" diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index afa806d246..1bb1222941 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -6,6 +6,8 @@ namespace esphome::ufire_ec { ESPHOME_LOG_TAG(TAG, "ufire_ec"); +static constexpr uint32_t DATA_TIMEOUT_ID = 0; + void UFireECComponent::setup() { uint8_t version; if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) { @@ -45,7 +47,7 @@ void UFireECComponent::update() { } if (wait > 0) { - this->set_timeout("data", wait, [this]() { this->update_internal_(); }); + this->set_timeout(DATA_TIMEOUT_ID, wait, [this]() { this->update_internal_(); }); } } diff --git a/esphome/components/ufire_ise/__init__.py b/esphome/components/ufire_ise/__init__.py index 08f36c7934..9fb4cb9c65 100644 --- a/esphome/components/ufire_ise/__init__.py +++ b/esphome/components/ufire_ise/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@pvizeli"] +DOMAIN = "ufire_ise" diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index c0fa715485..aa899968e4 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -7,6 +7,8 @@ namespace esphome::ufire_ise { ESPHOME_LOG_TAG(TAG, "ufire_ise"); +static constexpr uint32_t DATA_TIMEOUT_ID = 0; + void UFireISEComponent::setup() { uint8_t version; if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) { @@ -38,7 +40,7 @@ void UFireISEComponent::update() { } // Wait until measurement are taken - this->set_timeout("data", wait, [this]() { this->update_internal_(); }); + this->set_timeout(DATA_TIMEOUT_ID, wait, [this]() { this->update_internal_(); }); } void UFireISEComponent::update_internal_() { diff --git a/esphome/components/ufm01/__init__.py b/esphome/components/ufm01/__init__.py index 85ca0eecae..e1fcef107d 100644 --- a/esphome/components/ufm01/__init__.py +++ b/esphome/components/ufm01/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@ljungqvist"] +DOMAIN = "ufm01" MULTI_CONF = True diff --git a/esphome/components/uln2003/__init__.py b/esphome/components/uln2003/__init__.py index e69de29bb2..6f2260af57 100644 --- a/esphome/components/uln2003/__init__.py +++ b/esphome/components/uln2003/__init__.py @@ -0,0 +1 @@ +DOMAIN = "uln2003" diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index 3bca12bffc..cdc67e7813 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@swoboda1337", "@ssieb"] +DOMAIN = "ultrasonic" diff --git a/esphome/components/unicode/__init__.py b/esphome/components/unicode/__init__.py index f70ffa9520..c5464cbe6c 100644 --- a/esphome/components/unicode/__init__.py +++ b/esphome/components/unicode/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@esphome/core"] +DOMAIN = "unicode" diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 1533a893ba..e455a4f768 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -25,6 +25,7 @@ from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] +DOMAIN = "update" IS_PLATFORM_COMPONENT = True update_ns = cg.esphome_ns.namespace("update") diff --git a/esphome/components/uponor_smatrix/__init__.py b/esphome/components/uponor_smatrix/__init__.py index ba686dc22a..5dc32259ea 100644 --- a/esphome/components/uponor_smatrix/__init__.py +++ b/esphome/components/uponor_smatrix/__init__.py @@ -6,6 +6,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@kroimon"] +DOMAIN = "uponor_smatrix" DEPENDENCIES = ["uart"] diff --git a/esphome/components/uptime/__init__.py b/esphome/components/uptime/__init__.py index e69de29bb2..74eb20c656 100644 --- a/esphome/components/uptime/__init__.py +++ b/esphome/components/uptime/__init__.py @@ -0,0 +1 @@ +DOMAIN = "uptime" diff --git a/esphome/components/uptime/sensor/__init__.py b/esphome/components/uptime/sensor/__init__.py index a13d6cd7fd..04140ba431 100644 --- a/esphome/components/uptime/sensor/__init__.py +++ b/esphome/components/uptime/sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor, time +from esphome.components.time import DOMAIN as TIME_DOMAIN from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( @@ -44,7 +45,8 @@ CONFIG_SCHEMA = cv.typed_schema( cv.Schema( { cv.GenerateID(CONF_TIME_ID): cv.All( - cv.requires_component("time"), cv.use_id(time.RealTimeClock) + cv.requires_component(TIME_DOMAIN), + cv.use_id(time.RealTimeClock), ), } ) diff --git a/esphome/components/usb_cdc_acm/__init__.py b/esphome/components/usb_cdc_acm/__init__.py index 8cd078ab49..e49531c80b 100644 --- a/esphome/components/usb_cdc_acm/__init__.py +++ b/esphome/components/usb_cdc_acm/__init__.py @@ -15,6 +15,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] AUTO_LOAD = ["uart"] DEPENDENCIES = ["tinyusb"] +DOMAIN = "usb_cdc_acm" CONF_INTERFACES = "interfaces" diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 94532958ed..317cec8236 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -1,4 +1,5 @@ -from itertools import combinations +import itertools +import logging import esphome.codegen as cg from esphome.components.const import CONF_MANUFACTURER @@ -22,6 +23,8 @@ import esphome.final_validate as fv from esphome.helpers import cpp_u16string_escape from esphome.types import ConfigType +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["bytebuffer"] CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["esp32"] @@ -71,15 +74,19 @@ def usb_device_schema( ) -def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]: +def _clients_overlap(first: ConfigType, second: ConfigType) -> bool: # Two entries overlap when no field they both constrain tells them apart - for first, second in combinations(configs, 2): - for key, wildcard in _FILTER_WILDCARDS.items(): - a = first.get(key) - b = second.get(key) - if wildcard not in (a, b) and a != b: - break - else: + for key, wildcard in _FILTER_WILDCARDS.items(): + a = first.get(key) + b = second.get(key) + if wildcard not in (a, b) and a != b: + return False + return True + + +def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]: + for first, second in itertools.combinations(configs, 2): + if _clients_overlap(first, second): raise cv.Invalid( f"USB configs overlap: {first[CONF_ID]}, {second[CONF_ID]}" ) @@ -87,11 +94,29 @@ def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]: def _final_validate(config: ConfigType) -> ConfigType: - # Every USB client on the bus, whichever component configured it: any two could - # otherwise open the same device - clients = list(config.get(CONF_DEVICES) or ()) - clients.extend(fv.full_config.get().get("usb_uart") or ()) - validate_usb_clients(clients) + # Any two overlapping clients could otherwise open the same device + devices = config.get(CONF_DEVICES) or [] + uarts = fv.full_config.get().get("usb_uart") or [] + validate_usb_clients(devices) + validate_usb_clients(uarts) + # Remove before 2027.10.0, then pool devices and uarts in one validate_usb_clients call + for device, uart in itertools.product(devices, uarts): + if not _clients_overlap(device, uart): + continue + # The device matches nothing the usb_uart entry does not already claim + redundant = all( + uart.get(key) in (wildcard, device.get(key)) + for key, wildcard in _FILTER_WILDCARDS.items() + ) + _LOGGER.warning( + "usb_host device '%s' matches the same USB device as usb_uart '%s'; %s. " + "This will be an error in 2027.10.0", + device[CONF_ID], + uart[CONF_ID], + "remove it from usb_host devices" + if redundant + else "narrow its filter so it no longer matches that device", + ) return config diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index a632b4e38d..2898576b32 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -24,6 +24,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] CODEOWNERS = ["@clydebarrow"] +DOMAIN = "usb_uart" usb_uart_ns = cg.esphome_ns.namespace("usb_uart") USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component) @@ -33,7 +34,7 @@ USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent) def is_usb_uart_channel(uart_id: ID, full_config: ConfigType) -> bool: return any( channel[CONF_ID] == uart_id - for device in full_config.get("usb_uart") or [] + for device in full_config.get(DOMAIN) or [] for channel in device[CONF_CHANNELS] ) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 7df58c7a2a..41e25d63b1 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -15,7 +15,6 @@ from esphome.const import ( CONF_POSITION_STATE_TOPIC, CONF_STATE, CONF_STOP, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, DEVICE_CLASS_GAS, @@ -33,6 +32,7 @@ from esphome.cpp_generator import MockObjClass IS_PLATFORM_COMPONENT = True CODEOWNERS = ["@esphome/core"] +DOMAIN = "valve" DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, @@ -61,17 +61,18 @@ VALVE_OPERATIONS = { } validate_valve_operation = cv.enum(VALVE_OPERATIONS, upper=True) -# Actions -ValvePublishAction = valve_ns.class_("ValvePublishAction", automation.Action) - -# Triggers -ValveOpenTrigger = valve_ns.class_("ValveOpenTrigger", automation.Trigger.template()) -ValveClosedTrigger = valve_ns.class_( - "ValveClosedTrigger", automation.Trigger.template() -) - CONF_ON_CLOSED = "on_closed" +# The state callback carries nothing; each automation fires only in its end state. +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_OPEN, "add_on_state_callback", when="{parent}->is_fully_open()" + ), + automation.CallbackAutomation( + CONF_ON_CLOSED, "add_on_state_callback", when="{parent}->is_fully_closed()" + ), +) + _VALVE_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) @@ -88,16 +89,8 @@ _VALVE_SCHEMA = ( cv.Optional(CONF_POSITION_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), - cv.Optional(CONF_ON_OPEN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValveOpenTrigger), - } - ), - cv.Optional(CONF_ON_CLOSED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValveClosedTrigger), - } - ), + cv.Optional(CONF_ON_OPEN): automation.validate_automation({}), + cv.Optional(CONF_ON_CLOSED): automation.validate_automation({}), } ) ) @@ -133,12 +126,7 @@ def valve_schema( async def _setup_valve_core(var, config): setup_device_class(config) - for conf in config.get(CONF_ON_OPEN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_CLOSED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) if mqtt_id_config := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id_config, var) diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h deleted file mode 100644 index 598bbc1678..0000000000 --- a/esphome/components/valve/automation.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "esphome/core/component.h" -#include "valve.h" - -namespace esphome::valve { - -class ValveOpenTrigger final : public Trigger<> { - public: - ValveOpenTrigger(Valve *a_valve) : valve_(a_valve) { - a_valve->add_on_state_callback([this]() { - if (this->valve_->is_fully_open()) { - this->trigger(); - } - }); - } - - protected: - Valve *valve_; -}; - -class ValveClosedTrigger final : public Trigger<> { - public: - ValveClosedTrigger(Valve *a_valve) : valve_(a_valve) { - a_valve->add_on_state_callback([this]() { - if (this->valve_->is_fully_closed()) { - this->trigger(); - } - }); - } - - protected: - Valve *valve_; -}; - -} // namespace esphome::valve diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index fd54658912..533acfbbb9 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType CODEOWNERS = ["@ssieb"] +DOMAIN = "vbus" DEPENDENCIES = ["uart"] diff --git a/esphome/components/veml3235/__init__.py b/esphome/components/veml3235/__init__.py index e69de29bb2..57c95f46ef 100644 --- a/esphome/components/veml3235/__init__.py +++ b/esphome/components/veml3235/__init__.py @@ -0,0 +1 @@ +DOMAIN = "veml3235" diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp index 3d555bc469..3a85669678 100644 --- a/esphome/components/veml3235/veml3235.cpp +++ b/esphome/components/veml3235/veml3235.cpp @@ -6,6 +6,8 @@ namespace esphome::veml3235 { ESPHOME_LOG_TAG(TAG, "veml3235.sensor"); +static constexpr uint32_t REREAD_TIMEOUT_ID = 0; + // ADC counts at or above this value (98% of full scale) are treated as clipped: the true light level cannot // be estimated from such a reading, so auto-gain restarts from minimum sensitivity instead static const uint16_t CLIPPED_COUNTS = 64224; @@ -71,7 +73,7 @@ void VEML3235Sensor::read_and_publish_(uint8_t adjustments_left) { const uint32_t old_integration_time_ms = this->integration_time_ms_(); if (this->adjust_sensitivity_(als_counts)) { const uint32_t wait_ms = old_integration_time_ms + 2 * this->integration_time_ms_(); - this->set_timeout("reread", wait_ms, + this->set_timeout(REREAD_TIMEOUT_ID, wait_ms, [this, adjustments_left]() { this->read_and_publish_(adjustments_left - 1); }); return; } diff --git a/esphome/components/veml7700/__init__.py b/esphome/components/veml7700/__init__.py index dd06cfffea..9c8f1442d7 100644 --- a/esphome/components/veml7700/__init__.py +++ b/esphome/components/veml7700/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@latonita"] +DOMAIN = "veml7700" diff --git a/esphome/components/version/__init__.py b/esphome/components/version/__init__.py index f70ffa9520..f9c04b2596 100644 --- a/esphome/components/version/__init__.py +++ b/esphome/components/version/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@esphome/core"] +DOMAIN = "version" diff --git a/esphome/components/vl53l0x/__init__.py b/esphome/components/vl53l0x/__init__.py index e69de29bb2..3363ba9961 100644 --- a/esphome/components/vl53l0x/__init__.py +++ b/esphome/components/vl53l0x/__init__.py @@ -0,0 +1 @@ +DOMAIN = "vl53l0x" diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 68cb98b053..2e3d1c8529 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -265,7 +265,7 @@ void VL53L0XSensor::setup() { void VL53L0XSensor::update() { if (this->initiated_read_ || this->waiting_for_interrupt_) { this->publish_state(NAN); - this->status_momentary_warning("update", 5000); + this->status_momentary_warning(5000); ESP_LOGW(TAG, "%s - update called before prior reading complete - initiated:%d waiting_for_interrupt:%d", this->name_.c_str(), this->initiated_read_, this->waiting_for_interrupt_); return; diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 14c036de8e..d306947ec1 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -19,6 +19,7 @@ AUTO_LOAD = ["audio", "ring_buffer", "socket"] DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz", "@kahrendt"] +DOMAIN = "voice_assistant" CONF_ON_END = "on_end" CONF_ON_INTENT_END = "on_intent_end" diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index e952308437..039410abdb 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -14,6 +14,11 @@ namespace esphome::voice_assistant { ESPHOME_LOG_TAG(TAG, "voice_assistant"); +static constexpr uint32_t RESET_CONVERSATION_TIMEOUT_ID = 0; +static constexpr uint32_t PLAYBACK_TIMEOUT_ID = 1; +static constexpr uint32_t SPEAKER_IDLE_TIMEOUT_ID = 2; +static constexpr uint32_t TIMER_TICK_INTERVAL_ID = 3; + #ifdef SAMPLE_RATE_HZ #undef SAMPLE_RATE_HZ #endif @@ -370,7 +375,7 @@ void VoiceAssistant::loop() { break; } this->set_state_(State::STARTING_PIPELINE); - this->set_timeout("reset-conversation_id", this->conversation_timeout_, + this->set_timeout(RESET_CONVERSATION_TIMEOUT_ID, this->conversation_timeout_, [this]() { this->reset_conversation_id(); }); break; } @@ -460,10 +465,10 @@ void VoiceAssistant::loop() { if (this->speaker_bytes_received_ > RECEIVE_SIZE * 4 || end_of_stream) this->write_speaker_(); if (this->wait_for_stream_end_) { - this->cancel_timeout("playing"); + this->cancel_timeout(PLAYBACK_TIMEOUT_ID); if (end_of_stream) { ESP_LOGD(TAG, "End of audio stream received"); - this->cancel_timeout("speaker-timeout"); + this->cancel_timeout(SPEAKER_IDLE_TIMEOUT_ID); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); } break; // We dont want to timeout here as the STREAM_END event will take care of that. @@ -477,7 +482,7 @@ void VoiceAssistant::loop() { if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) { this->media_player_response_state_ = MediaPlayerResponseState::IDLE; - this->cancel_timeout("playing"); + this->cancel_timeout(PLAYBACK_TIMEOUT_ID); ESP_LOGD(TAG, "Announcement finished playing"); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); @@ -507,8 +512,8 @@ void VoiceAssistant::loop() { } ESP_LOGD(TAG, "Speaker has finished outputting all audio"); this->speaker_->stop(); - this->cancel_timeout("speaker-timeout"); - this->cancel_timeout("playing"); + this->cancel_timeout(SPEAKER_IDLE_TIMEOUT_ID); + this->cancel_timeout(PLAYBACK_TIMEOUT_ID); this->clear_buffers_(); @@ -540,7 +545,7 @@ void VoiceAssistant::write_speaker_() { memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); this->speaker_buffer_size_ -= written; this->speaker_buffer_index_ -= written; - this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); + this->set_timeout(SPEAKER_IDLE_TIMEOUT_ID, 5000, [this]() { this->speaker_->stop(); }); } else { ESP_LOGV(TAG, "Speaker buffer full, trying again next loop"); } @@ -755,8 +760,8 @@ void VoiceAssistant::signal_stop_() { } void VoiceAssistant::start_playback_timeout_() { - this->set_timeout("playing", 2000, [this]() { - this->cancel_timeout("speaker-timeout"); + this->set_timeout(PLAYBACK_TIMEOUT_ID, 2000, [this]() { + this->cancel_timeout(SPEAKER_IDLE_TIMEOUT_ID); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); if (this->api_client_ == nullptr) @@ -1046,10 +1051,10 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse } if (this->timers_.empty()) { - this->cancel_interval("timer-event"); + this->cancel_interval(TIMER_TICK_INTERVAL_ID); this->timer_tick_running_ = false; } else if (!this->timer_tick_running_) { - this->set_interval("timer-event", 1000, [this]() { this->timer_tick_(); }); + this->set_interval(TIMER_TICK_INTERVAL_ID, 1000, [this]() { this->timer_tick_(); }); this->timer_tick_running_ = true; } } diff --git a/esphome/components/voltage_sampler/__init__.py b/esphome/components/voltage_sampler/__init__.py index e60918096e..1dad646322 100644 --- a/esphome/components/voltage_sampler/__init__.py +++ b/esphome/components/voltage_sampler/__init__.py @@ -1,4 +1,6 @@ import esphome.codegen as cg +DOMAIN = "voltage_sampler" + voltage_sampler_ns = cg.esphome_ns.namespace("voltage_sampler") VoltageSampler = voltage_sampler_ns.class_("VoltageSampler") diff --git a/esphome/components/wake_on_lan/__init__.py b/esphome/components/wake_on_lan/__init__.py index 90539e5d3c..ad052dd85e 100644 --- a/esphome/components/wake_on_lan/__init__.py +++ b/esphome/components/wake_on_lan/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@willwill2will54", "@clydebarrow"] +DOMAIN = "wake_on_lan" diff --git a/esphome/components/watchdog/__init__.py b/esphome/components/watchdog/__init__.py index 6fb87e45a4..2608ff7c57 100644 --- a/esphome/components/watchdog/__init__.py +++ b/esphome/components/watchdog/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@oarcher"] +DOMAIN = "watchdog" diff --git a/esphome/components/water_heater/__init__.py b/esphome/components/water_heater/__init__.py index 6bb2b2f8fe..b5821e55af 100644 --- a/esphome/components/water_heater/__init__.py +++ b/esphome/components/water_heater/__init__.py @@ -18,6 +18,7 @@ from esphome.cpp_generator import MockObjClass from esphome.types import ConfigType CODEOWNERS = ["@dhoeben"] +DOMAIN = "water_heater" IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/waveshare_epaper/__init__.py b/esphome/components/waveshare_epaper/__init__.py index b410406a58..4bcfcdfda5 100644 --- a/esphome/components/waveshare_epaper/__init__.py +++ b/esphome/components/waveshare_epaper/__init__.py @@ -1,4 +1,5 @@ CODEOWNERS = ["@clydebarrow"] +DOMAIN = "waveshare_epaper" DEPRECATED_COMPONENT = """ The 'waveshare_epaper' component is deprecated and no new models will be added to it. diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index cc9c34cb42..5e19782f6d 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -4,7 +4,7 @@ namespace esphome::waveshare_epaper { -static const char *const TAG = "waveshare_2.13v3"; +ESPHOME_LOG_TAG(TAG, "waveshare_2.13v3"); static const uint8_t PARTIAL_LUT[] = { 0x32, // cmd diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 93f23424c0..60d711d1cd 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -7,7 +7,7 @@ namespace esphome::waveshare_epaper { -static const char *const TAG = "waveshare_epaper"; +ESPHOME_LOG_TAG(TAG, "waveshare_epaper"); static const uint8_t LUT_SIZE_WAVESHARE = 30; diff --git a/esphome/components/waveshare_io_ch32v003/__init__.py b/esphome/components/waveshare_io_ch32v003/__init__.py index 29a939c523..5e53d30149 100644 --- a/esphome/components/waveshare_io_ch32v003/__init__.py +++ b/esphome/components/waveshare_io_ch32v003/__init__.py @@ -14,6 +14,7 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@latonita"] +DOMAIN = "waveshare_io_ch32v003" AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 5e77c7496a..4f233e9965 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -48,6 +48,7 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["json", "web_server_base"] +DOMAIN = "web_server" AUTH_TYPE_BASIC = "basic" AUTH_TYPE_DIGEST = "digest" @@ -239,11 +240,11 @@ WEBSERVER_SORTING_SCHEMA = cv.Schema( { cv.OnlyWith(CONF_WEB_SERVER_ID, "web_server"): cv.use_id(WebServer), cv.Optional(CONF_SORTING_WEIGHT): cv.All( - cv.requires_component("web_server"), + cv.requires_component(DOMAIN), cv.float_, ), cv.Optional(CONF_SORTING_GROUP_ID): cv.All( - cv.requires_component("web_server"), + cv.requires_component(DOMAIN), cv.use_id(cg.int_), ), } @@ -473,7 +474,7 @@ def FILTER_SOURCE_FILES() -> list[str]: files_to_filter: list[str] = [] # web_server_v1.cpp is only needed when version is 1 - config = CORE.config.get("web_server", {}) + config = CORE.config.get(DOMAIN, {}) if config.get(CONF_VERSION, 2) != 1: files_to_filter.append("web_server_v1.cpp") diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 03a5c2ca9b..937d99ede8 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -2,15 +2,22 @@ import logging import esphome.codegen as cg from esphome.components.esp32 import add_idf_component -from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.components.ota import ( + BASE_OTA_SCHEMA, + DOMAIN as OTA_DOMAIN, + OTAComponent, + ota_to_code, +) from esphome.config_helpers import merge_config import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER +from esphome.const import CONF_ID, CONF_PLATFORM from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority import esphome.final_validate as fv from esphome.types import ConfigType +from .. import DOMAIN + _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] @@ -27,13 +34,13 @@ def _web_server_ota_final_validate(config: ConfigType) -> None: causing undefined behavior. Merge them into a single instance. """ full_conf = fv.full_config.get() - ota_confs = full_conf.get(CONF_OTA, []) + ota_confs = full_conf.get(OTA_DOMAIN, []) web_server_ota_configs: list[ConfigType] = [] other_ota_configs: list[ConfigType] = [] for ota_conf in ota_confs: - if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER: + if ota_conf.get(CONF_PLATFORM) == DOMAIN: web_server_ota_configs.append(ota_conf) else: other_ota_configs.append(ota_conf) @@ -62,7 +69,7 @@ def _web_server_ota_final_validate(config: ConfigType) -> None: # Replace OTA configs with merged web_server + other OTA platforms other_ota_configs.append(merged) - full_conf[CONF_OTA] = other_ota_configs + full_conf[OTA_DOMAIN] = other_ota_configs fv.full_config.set(full_conf) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 546e10ff5e..c94eea1190 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -10,6 +10,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] +DOMAIN = "web_server_base" def AUTO_LOAD() -> list[str]: diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index b9d079b53f..98e067665a 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -7,6 +7,7 @@ import esphome.config_validation as cv from esphome.types import ConfigType CODEOWNERS = ["@dentra"] +DOMAIN = "web_server_idf" CONFIG_SCHEMA = cv.All( cv.Schema({}), diff --git a/esphome/components/weikai/__init__.py b/esphome/components/weikai/__init__.py index 8f0cf4ba33..58bfc6f815 100644 --- a/esphome/components/weikai/__init__.py +++ b/esphome/components/weikai/__init__.py @@ -17,6 +17,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] AUTO_LOAD = ["uart"] +DOMAIN = "weikai" MULTI_CONF = True CONF_CRYSTAL = "crystal" diff --git a/esphome/components/weikai_i2c/__init__.py b/esphome/components/weikai_i2c/__init__.py index 2c6a421a0a..9bbe558dcf 100644 --- a/esphome/components/weikai_i2c/__init__.py +++ b/esphome/components/weikai_i2c/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@DrCoolZic"] +DOMAIN = "weikai_i2c" diff --git a/esphome/components/weikai_spi/__init__.py b/esphome/components/weikai_spi/__init__.py index 2c6a421a0a..2a13bda034 100644 --- a/esphome/components/weikai_spi/__init__.py +++ b/esphome/components/weikai_spi/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@DrCoolZic"] +DOMAIN = "weikai_spi" diff --git a/esphome/components/whirlpool/__init__.py b/esphome/components/whirlpool/__init__.py index e69de29bb2..4bc63aeba8 100644 --- a/esphome/components/whirlpool/__init__.py +++ b/esphome/components/whirlpool/__init__.py @@ -0,0 +1 @@ +DOMAIN = "whirlpool" diff --git a/esphome/components/whynter/__init__.py b/esphome/components/whynter/__init__.py index 7316776d78..f69112ca3d 100644 --- a/esphome/components/whynter/__init__.py +++ b/esphome/components/whynter/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@aeonsablaze"] +DOMAIN = "whynter" diff --git a/esphome/components/wiegand/__init__.py b/esphome/components/wiegand/__init__.py index dd6e84ac25..93d5e4f0f0 100644 --- a/esphome/components/wiegand/__init__.py +++ b/esphome/components/wiegand/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_ON_KEY, CONF_ON_TAG, CONF_TRIGGER_ID from esphome.types import ConfigType CODEOWNERS = ["@ssieb"] +DOMAIN = "wiegand" AUTO_LOAD = ["key_provider"] diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index f357bf75b3..aa6d113e16 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -61,7 +61,6 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, - CONF_WIFI, PLACEHOLDER_WIFI_SSID, Platform, PlatformFramework, @@ -81,6 +80,7 @@ from . import wpa2_eap _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["network"] +DOMAIN = "wifi" NO_WIFI_VARIANTS = [ const.VARIANT_ESP32H2, @@ -1088,7 +1088,7 @@ def _placeholder_wifi_credentials(config: ConfigType) -> list[str]: values still appear. Empty list means no placeholders were found. """ placeholders: list[str] = [] - wifi_conf = config.get(CONF_WIFI) + wifi_conf = config.get(DOMAIN) if not wifi_conf: return placeholders diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index c14341330f..eb3d7471dc 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -76,13 +76,13 @@ template class WiFiConfigureAction final : public Action, // Enable WiFi global_wifi_component->enable(); // Set timeout for the connection - this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { + this->set_timeout(CONNECT_TIMEOUT_ID, this->connection_timeout_.value(x...), [this, x...]() { // If the timeout is reached, stop connecting and revert to the old AP global_wifi_component->disable(); global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); global_wifi_component->enable(); // Start a timeout for the fallback if the connection to the old AP fails - this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { + this->set_timeout(FALLBACK_TIMEOUT_ID, this->connection_timeout_.value(x...), [this]() { this->connecting_ = false; this->error_trigger_.trigger(); }); @@ -97,8 +97,8 @@ template class WiFiConfigureAction final : public Action, return; if (global_wifi_component->is_connected()) { // The WiFi is connected, stop the timeout and reset the connecting flag - this->cancel_timeout("wifi-connect-timeout"); - this->cancel_timeout("wifi-fallback-timeout"); + this->cancel_timeout(CONNECT_TIMEOUT_ID); + this->cancel_timeout(FALLBACK_TIMEOUT_ID); this->connecting_ = false; char ssid_buf[SSID_BUFFER_SIZE]; if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) { @@ -112,6 +112,9 @@ template class WiFiConfigureAction final : public Action, } protected: + static constexpr uint32_t CONNECT_TIMEOUT_ID = 0; + static constexpr uint32_t FALLBACK_TIMEOUT_ID = 1; + bool connecting_{false}; WiFiAP new_sta_; WiFiAP old_sta_; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index ef8b22d239..001dae98b8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -398,6 +398,7 @@ static constexpr uint32_t WIFI_AP_EXCLUSIVE_DWELL_MS = 300000; /// disconnect event cannot keep the networks from being tried for good. static constexpr uint32_t WIFI_AP_EXCLUSIVE_MAX_DWELL_MS = 3 * WIFI_AP_EXCLUSIVE_DWELL_MS; static constexpr uint32_t WIFI_AP_EXCLUSIVE_HANDOVER_MS = 1000; +static constexpr uint32_t AP_HANDOVER_TIMEOUT_ID = 0; #endif /// Cooldown duration when fallback AP is active and captive portal may be running @@ -1222,7 +1223,7 @@ void WiFiComponent::connect_soon_() { // New credentials from the portal: the AP pauses for them once its answer // is out, and the STA state machine picks them up from there. if (this->ap_setup_) { - this->set_timeout("wifi-ap-handover", WIFI_AP_EXCLUSIVE_HANDOVER_MS, [this]() { + this->set_timeout(AP_HANDOVER_TIMEOUT_ID, WIFI_AP_EXCLUSIVE_HANDOVER_MS, [this]() { if (this->ap_setup_) this->pause_exclusive_ap_(); }); diff --git a/esphome/components/wifi_info/__init__.py b/esphome/components/wifi_info/__init__.py index e69de29bb2..77d5dd714a 100644 --- a/esphome/components/wifi_info/__init__.py +++ b/esphome/components/wifi_info/__init__.py @@ -0,0 +1 @@ +DOMAIN = "wifi_info" diff --git a/esphome/components/wifi_signal/__init__.py b/esphome/components/wifi_signal/__init__.py index e69de29bb2..febfbf7387 100644 --- a/esphome/components/wifi_signal/__init__.py +++ b/esphome/components/wifi_signal/__init__.py @@ -0,0 +1 @@ +DOMAIN = "wifi_signal" diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 3b67aa46d0..e9641a5957 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -33,6 +33,7 @@ CONF_WIREGUARD_ID = "wireguard_id" DEPENDENCIES = ["time"] CODEOWNERS = ["@lhoracek", "@droscy", "@thomas0bernard"] +DOMAIN = "wireguard" # The key validation regex has been described by Jason Donenfeld himself # url: https://lists.zx2c4.com/pipermail/wireguard/2020-December/006222.html @@ -120,16 +121,19 @@ async def to_code(config): ) ) + # Shared flash table ended by an empty entry, so the component stores only a pointer. + entries = [ + cg.StructInitializer( + AllowedIP, + ("ip", str(ip.network_address)), + ("netmask", str(ip.netmask)), + ) + for ip in allowed_ips + ] + end = cg.StructInitializer(AllowedIP, ("ip", cg.nullptr), ("netmask", cg.nullptr)) cg.add( var.set_allowed_ips( - [ - cg.StructInitializer( - AllowedIP, - ("ip", str(ip.network_address)), - ("netmask", str(ip.netmask)), - ) - for ip in allowed_ips - ] + cg.shared_progmem_array("wireguard_allowed_ips", AllowedIP, [*entries, end]) ) ) diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 73e34b6592..01921f17e8 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -154,8 +154,8 @@ void Wireguard::dump_config() { (this->preshared_key_ != nullptr ? preshared_key_masked : LOG_STR_LITERAL("NOT IN USE"))); // clang-format on ESP_LOGCONFIG(TAG, " Peer Allowed IPs:"); - for (const AllowedIP &allowed_ip : this->allowed_ips_) { - ESP_LOGCONFIG(TAG, " - %s/%s", allowed_ip.ip, allowed_ip.netmask); + for (const AllowedIP *it = this->allowed_ips_; it != nullptr && it->ip != nullptr; it++) { + ESP_LOGCONFIG(TAG, " - %s/%s", it->ip, it->netmask); } ESP_LOGCONFIG(TAG, " Peer Persistent Keepalive: %d%s", this->keepalive_, (this->keepalive_ > 0 ? LOG_STR_LITERAL("s") : LOG_STR_LITERAL(" (DISABLED)"))); @@ -249,8 +249,8 @@ void Wireguard::start_connection_() { ESP_LOGD(TAG, "Configuring allowed IPs list"); bool allowed_ips_ok = true; - for (const AllowedIP &ip : this->allowed_ips_) { - allowed_ips_ok &= (esp_wireguard_add_allowed_ip(&(this->wg_ctx_), ip.ip, ip.netmask) == ESP_OK); + for (const AllowedIP *it = this->allowed_ips_; it != nullptr && it->ip != nullptr; it++) { + allowed_ips_ok &= (esp_wireguard_add_allowed_ip(&(this->wg_ctx_), it->ip, it->netmask) == ESP_OK); } if (allowed_ips_ok) { diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index faf63cd444..6125d2f52c 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -2,7 +2,6 @@ #include "esphome/core/defines.h" #ifdef USE_WIREGUARD #include -#include #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -58,9 +57,8 @@ class Wireguard final : public PollingComponent { void set_peer_public_key(const std::string &key) = delete; void set_preshared_key(const std::string &key) = delete; - void set_allowed_ips(std::initializer_list ips) { this->allowed_ips_ = ips; } - /// Prevent accidental use of std::string which would dangle - void set_allowed_ips(std::initializer_list> ips) = delete; + /// Table ended by an entry with a null ip; must outlive the component. + void set_allowed_ips(const AllowedIP *ips) { this->allowed_ips_ = ips; } void set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; } void set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; } @@ -105,7 +103,7 @@ class Wireguard final : public PollingComponent { const char *peer_public_key_{nullptr}; const char *preshared_key_{nullptr}; - FixedVector allowed_ips_; + const AllowedIP *allowed_ips_{nullptr}; uint16_t peer_port_; uint16_t keepalive_; diff --git a/esphome/components/wk2132_i2c/__init__.py b/esphome/components/wk2132_i2c/__init__.py index ecb7a5da09..7e0999b08f 100644 --- a/esphome/components/wk2132_i2c/__init__.py +++ b/esphome/components/wk2132_i2c/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["weikai", "weikai_i2c"] +DOMAIN = "wk2132_i2c" MULTI_CONF = True weikai_i2c_ns = cg.esphome_ns.namespace("weikai_i2c") diff --git a/esphome/components/wk2132_spi/__init__.py b/esphome/components/wk2132_spi/__init__.py index 94b692f170..de482839e1 100644 --- a/esphome/components/wk2132_spi/__init__.py +++ b/esphome/components/wk2132_spi/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] DEPENDENCIES = ["spi"] AUTO_LOAD = ["weikai", "weikai_spi"] +DOMAIN = "wk2132_spi" MULTI_CONF = True weikai_spi_ns = cg.esphome_ns.namespace("weikai_spi") diff --git a/esphome/components/wk2168_i2c/__init__.py b/esphome/components/wk2168_i2c/__init__.py index d4aacaf97d..c450717ea2 100644 --- a/esphome/components/wk2168_i2c/__init__.py +++ b/esphome/components/wk2168_i2c/__init__.py @@ -9,6 +9,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["weikai", "weikai_i2c"] +DOMAIN = "wk2168_i2c" MULTI_CONF = True CONF_WK2168_I2C = "wk2168_i2c" diff --git a/esphome/components/wk2168_spi/__init__.py b/esphome/components/wk2168_spi/__init__.py index b4efaf65ac..ef12117a2c 100644 --- a/esphome/components/wk2168_spi/__init__.py +++ b/esphome/components/wk2168_spi/__init__.py @@ -9,6 +9,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] DEPENDENCIES = ["spi"] AUTO_LOAD = ["weikai", "weikai_spi"] +DOMAIN = "wk2168_spi" MULTI_CONF = True CONF_WK2168_SPI = "wk2168_spi" diff --git a/esphome/components/wk2204_i2c/__init__.py b/esphome/components/wk2204_i2c/__init__.py index 1502cc6cdb..b8882d41c1 100644 --- a/esphome/components/wk2204_i2c/__init__.py +++ b/esphome/components/wk2204_i2c/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["weikai", "weikai_i2c"] +DOMAIN = "wk2204_i2c" MULTI_CONF = True weikai_i2c_ns = cg.esphome_ns.namespace("weikai_i2c") diff --git a/esphome/components/wk2204_spi/__init__.py b/esphome/components/wk2204_spi/__init__.py index 930f534bb0..2de911aebd 100644 --- a/esphome/components/wk2204_spi/__init__.py +++ b/esphome/components/wk2204_spi/__init__.py @@ -7,6 +7,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] DEPENDENCIES = ["spi"] AUTO_LOAD = ["weikai", "weikai_spi"] +DOMAIN = "wk2204_spi" MULTI_CONF = True weikai_spi_ns = cg.esphome_ns.namespace("weikai_spi") diff --git a/esphome/components/wk2212_i2c/__init__.py b/esphome/components/wk2212_i2c/__init__.py index 00018dd735..1f51fea314 100644 --- a/esphome/components/wk2212_i2c/__init__.py +++ b/esphome/components/wk2212_i2c/__init__.py @@ -9,6 +9,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["weikai", "weikai_i2c"] +DOMAIN = "wk2212_i2c" MULTI_CONF = True CONF_WK2212_I2C = "wk2212_i2c" diff --git a/esphome/components/wk2212_spi/__init__.py b/esphome/components/wk2212_spi/__init__.py index c1a050e8ca..a0cce3331a 100644 --- a/esphome/components/wk2212_spi/__init__.py +++ b/esphome/components/wk2212_spi/__init__.py @@ -9,6 +9,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] DEPENDENCIES = ["spi"] AUTO_LOAD = ["weikai", "weikai_spi"] +DOMAIN = "wk2212_spi" MULTI_CONF = True CONF_WK2212_SPI = "wk2212_spi" diff --git a/esphome/components/wl_134/__init__.py b/esphome/components/wl_134/__init__.py index e69de29bb2..03fae78280 100644 --- a/esphome/components/wl_134/__init__.py +++ b/esphome/components/wl_134/__init__.py @@ -0,0 +1 @@ +DOMAIN = "wl_134" diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py index a7135975f0..6b3263e6a3 100644 --- a/esphome/components/wled/__init__.py +++ b/esphome/components/wled/__init__.py @@ -6,6 +6,8 @@ from esphome.const import CONF_NAME, CONF_PORT from esphome.core import CORE, ID from esphome.types import ConfigType +DOMAIN = "wled" + wled_ns = cg.esphome_ns.namespace("wled") WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect) diff --git a/esphome/components/wts01/__init__.py b/esphome/components/wts01/__init__.py index e69de29bb2..5d5a06ea05 100644 --- a/esphome/components/wts01/__init__.py +++ b/esphome/components/wts01/__init__.py @@ -0,0 +1 @@ +DOMAIN = "wts01" diff --git a/esphome/components/x9c/__init__.py b/esphome/components/x9c/__init__.py index e69de29bb2..46a7316bd1 100644 --- a/esphome/components/x9c/__init__.py +++ b/esphome/components/x9c/__init__.py @@ -0,0 +1 @@ +DOMAIN = "x9c" diff --git a/esphome/components/xdb401/__init__.py b/esphome/components/xdb401/__init__.py index 943139e19a..181b38caca 100644 --- a/esphome/components/xdb401/__init__.py +++ b/esphome/components/xdb401/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@RT530"] +DOMAIN = "xdb401" diff --git a/esphome/components/xgzp68xx/__init__.py b/esphome/components/xgzp68xx/__init__.py index 122ffaf6ed..6cebeffba1 100644 --- a/esphome/components/xgzp68xx/__init__.py +++ b/esphome/components/xgzp68xx/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@gcormier"] +DOMAIN = "xgzp68xx" diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index 35e1ed950b..795db448f0 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -10,6 +10,8 @@ namespace esphome::xgzp68xx { ESPHOME_LOG_TAG(TAG, "xgzp68xx.sensor"); +static constexpr uint32_t MEASUREMENT_TIMEOUT_ID = 0; + static const uint8_t CMD_ADDRESS = 0x30; static const uint8_t SYSCONFIG_ADDRESS = 0xA5; static const uint8_t PCONFIG_ADDRESS = 0xA6; @@ -54,7 +56,7 @@ void XGZP68XXComponent::update() { this->write_register(0x30, &READ_COMMAND, 1); // Wait 20mS per datasheet - this->set_timeout("measurement", 20, [this]() { + this->set_timeout(MEASUREMENT_TIMEOUT_ID, 20, [this]() { uint8_t data[5] = {}; uint32_t pressure_raw = 0; uint16_t temperature_raw = 0; diff --git a/esphome/components/xiaomi_ble/__init__.py b/esphome/components/xiaomi_ble/__init__.py index 16da83934e..de7998b290 100644 --- a/esphome/components/xiaomi_ble/__init__.py +++ b/esphome/components/xiaomi_ble/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from esphome.types import ConfigType AUTO_LOAD = ["ble_device_base"] +DOMAIN = "xiaomi_ble" xiaomi_ble_ns = cg.esphome_ns.namespace("xiaomi_ble") XiaomiListener = xiaomi_ble_ns.class_( diff --git a/esphome/components/xiaomi_body_scale/__init__.py b/esphome/components/xiaomi_body_scale/__init__.py index 3d75ef24ec..aeb29d234f 100644 --- a/esphome/components/xiaomi_body_scale/__init__.py +++ b/esphome/components/xiaomi_body_scale/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@dckiller51"] +DOMAIN = "xiaomi_body_scale" diff --git a/esphome/components/xiaomi_cgd1/__init__.py b/esphome/components/xiaomi_cgd1/__init__.py index e69de29bb2..f76e529bb2 100644 --- a/esphome/components/xiaomi_cgd1/__init__.py +++ b/esphome/components/xiaomi_cgd1/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_cgd1" diff --git a/esphome/components/xiaomi_cgdk2/__init__.py b/esphome/components/xiaomi_cgdk2/__init__.py index e69de29bb2..5a9b433236 100644 --- a/esphome/components/xiaomi_cgdk2/__init__.py +++ b/esphome/components/xiaomi_cgdk2/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_cgdk2" diff --git a/esphome/components/xiaomi_cgg1/__init__.py b/esphome/components/xiaomi_cgg1/__init__.py index e69de29bb2..ea01df8c57 100644 --- a/esphome/components/xiaomi_cgg1/__init__.py +++ b/esphome/components/xiaomi_cgg1/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_cgg1" diff --git a/esphome/components/xiaomi_cgpr1/__init__.py b/esphome/components/xiaomi_cgpr1/__init__.py index e69de29bb2..b17553724d 100644 --- a/esphome/components/xiaomi_cgpr1/__init__.py +++ b/esphome/components/xiaomi_cgpr1/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_cgpr1" diff --git a/esphome/components/xiaomi_gcls002/__init__.py b/esphome/components/xiaomi_gcls002/__init__.py index e69de29bb2..643a26ffe2 100644 --- a/esphome/components/xiaomi_gcls002/__init__.py +++ b/esphome/components/xiaomi_gcls002/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_gcls002" diff --git a/esphome/components/xiaomi_hhccjcy01/__init__.py b/esphome/components/xiaomi_hhccjcy01/__init__.py index e69de29bb2..e8e38dc51f 100644 --- a/esphome/components/xiaomi_hhccjcy01/__init__.py +++ b/esphome/components/xiaomi_hhccjcy01/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_hhccjcy01" diff --git a/esphome/components/xiaomi_hhccjcy10/__init__.py b/esphome/components/xiaomi_hhccjcy10/__init__.py index d47cef13c6..eeb0e7d7ef 100644 --- a/esphome/components/xiaomi_hhccjcy10/__init__.py +++ b/esphome/components/xiaomi_hhccjcy10/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@fariouche"] +DOMAIN = "xiaomi_hhccjcy10" diff --git a/esphome/components/xiaomi_hhccpot002/__init__.py b/esphome/components/xiaomi_hhccpot002/__init__.py index e69de29bb2..4d74c2f432 100644 --- a/esphome/components/xiaomi_hhccpot002/__init__.py +++ b/esphome/components/xiaomi_hhccpot002/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_hhccpot002" diff --git a/esphome/components/xiaomi_jqjcy01ym/__init__.py b/esphome/components/xiaomi_jqjcy01ym/__init__.py index e69de29bb2..e381fe6172 100644 --- a/esphome/components/xiaomi_jqjcy01ym/__init__.py +++ b/esphome/components/xiaomi_jqjcy01ym/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_jqjcy01ym" diff --git a/esphome/components/xiaomi_lywsd02/__init__.py b/esphome/components/xiaomi_lywsd02/__init__.py index e69de29bb2..e8eb493338 100644 --- a/esphome/components/xiaomi_lywsd02/__init__.py +++ b/esphome/components/xiaomi_lywsd02/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_lywsd02" diff --git a/esphome/components/xiaomi_lywsd02mmc/__init__.py b/esphome/components/xiaomi_lywsd02mmc/__init__.py index e69de29bb2..2398660e95 100644 --- a/esphome/components/xiaomi_lywsd02mmc/__init__.py +++ b/esphome/components/xiaomi_lywsd02mmc/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_lywsd02mmc" diff --git a/esphome/components/xiaomi_lywsd03mmc/__init__.py b/esphome/components/xiaomi_lywsd03mmc/__init__.py index e69de29bb2..3d1638fc11 100644 --- a/esphome/components/xiaomi_lywsd03mmc/__init__.py +++ b/esphome/components/xiaomi_lywsd03mmc/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_lywsd03mmc" diff --git a/esphome/components/xiaomi_lywsdcgq/__init__.py b/esphome/components/xiaomi_lywsdcgq/__init__.py index e69de29bb2..bbf235aa96 100644 --- a/esphome/components/xiaomi_lywsdcgq/__init__.py +++ b/esphome/components/xiaomi_lywsdcgq/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_lywsdcgq" diff --git a/esphome/components/xiaomi_mccgq02hl/__init__.py b/esphome/components/xiaomi_mccgq02hl/__init__.py index 3aecbd8207..a65bad1cb1 100644 --- a/esphome/components/xiaomi_mccgq02hl/__init__.py +++ b/esphome/components/xiaomi_mccgq02hl/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@ahpohl", "@morph027"] +DOMAIN = "xiaomi_mccgq02hl" diff --git a/esphome/components/xiaomi_mhoc303/__init__.py b/esphome/components/xiaomi_mhoc303/__init__.py index e7005b3957..5ffa02332f 100644 --- a/esphome/components/xiaomi_mhoc303/__init__.py +++ b/esphome/components/xiaomi_mhoc303/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@drug123"] +DOMAIN = "xiaomi_mhoc303" diff --git a/esphome/components/xiaomi_mhoc401/__init__.py b/esphome/components/xiaomi_mhoc401/__init__.py index e69de29bb2..14fe18af57 100644 --- a/esphome/components/xiaomi_mhoc401/__init__.py +++ b/esphome/components/xiaomi_mhoc401/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_mhoc401" diff --git a/esphome/components/xiaomi_miscale/__init__.py b/esphome/components/xiaomi_miscale/__init__.py index e69de29bb2..ddda4a0a5b 100644 --- a/esphome/components/xiaomi_miscale/__init__.py +++ b/esphome/components/xiaomi_miscale/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_miscale" diff --git a/esphome/components/xiaomi_miscale2/__init__.py b/esphome/components/xiaomi_miscale2/__init__.py index e69de29bb2..346db02445 100644 --- a/esphome/components/xiaomi_miscale2/__init__.py +++ b/esphome/components/xiaomi_miscale2/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_miscale2" diff --git a/esphome/components/xiaomi_mjyd02yla/__init__.py b/esphome/components/xiaomi_mjyd02yla/__init__.py index e69de29bb2..eb881ca39a 100644 --- a/esphome/components/xiaomi_mjyd02yla/__init__.py +++ b/esphome/components/xiaomi_mjyd02yla/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_mjyd02yla" diff --git a/esphome/components/xiaomi_mue4094rt/__init__.py b/esphome/components/xiaomi_mue4094rt/__init__.py index e69de29bb2..3d539b267d 100644 --- a/esphome/components/xiaomi_mue4094rt/__init__.py +++ b/esphome/components/xiaomi_mue4094rt/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_mue4094rt" diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp index afd8715ec5..683e8a2a92 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp @@ -5,6 +5,8 @@ namespace esphome::xiaomi_mue4094rt { ESPHOME_LOG_TAG(TAG, "xiaomi_mue4094rt"); +static constexpr uint32_t MOTION_TIMEOUT_ID = 0; + void XiaomiMUE4094RT::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi MUE4094RT"); LOG_BINARY_SENSOR(" ", "Motion", this); @@ -40,7 +42,7 @@ bool XiaomiMUE4094RT::parse_device(const ble_device_base::ESPBTDevice &device) { } if (res->has_motion.has_value()) { this->publish_state(*res->has_motion); - this->set_timeout("motion_timeout", timeout_, [this]() { this->publish_state(false); }); + this->set_timeout(MOTION_TIMEOUT_ID, timeout_, [this]() { this->publish_state(false); }); } success = true; } diff --git a/esphome/components/xiaomi_rtcgq02lm/__init__.py b/esphome/components/xiaomi_rtcgq02lm/__init__.py index 7b289a8ee3..71d7e0a265 100644 --- a/esphome/components/xiaomi_rtcgq02lm/__init__.py +++ b/esphome/components/xiaomi_rtcgq02lm/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["ble_device_base", "xiaomi_ble"] CODEOWNERS = ["@jesserockz"] +DOMAIN = "xiaomi_rtcgq02lm" MULTI_CONF = True xiaomi_rtcgq02lm_ns = cg.esphome_ns.namespace("xiaomi_rtcgq02lm") diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp index 8bf098a676..f7ecf15c3b 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -6,6 +6,9 @@ namespace esphome::xiaomi_rtcgq02lm { ESPHOME_LOG_TAG(TAG, "xiaomi_rtcgq02lm"); +static constexpr uint32_t MOTION_TIMEOUT_ID = 0; +static constexpr uint32_t BUTTON_TIMEOUT_ID = 1; + static constexpr size_t RTCGQ02LM_BINDKEY_SIZE = 16; void XiaomiRTCGQ02LM::dump_config() { @@ -55,13 +58,13 @@ bool XiaomiRTCGQ02LM::parse_device(const ble_device_base::ESPBTDevice &device) { #ifdef USE_BINARY_SENSOR if (res->has_motion.has_value() && this->motion_ != nullptr) { this->motion_->publish_state(*res->has_motion); - this->set_timeout("motion_timeout", this->motion_timeout_, [this]() { this->motion_->publish_state(false); }); + this->set_timeout(MOTION_TIMEOUT_ID, this->motion_timeout_, [this]() { this->motion_->publish_state(false); }); } if (res->is_light.has_value() && this->light_ != nullptr) this->light_->publish_state(*res->is_light); if (res->button_press.has_value() && this->button_ != nullptr) { this->button_->publish_state(*res->button_press); - this->set_timeout("button_timeout", this->button_timeout_, [this]() { this->button_->publish_state(false); }); + this->set_timeout(BUTTON_TIMEOUT_ID, this->button_timeout_, [this]() { this->button_->publish_state(false); }); } #endif #ifdef USE_SENSOR diff --git a/esphome/components/xiaomi_wx08zm/__init__.py b/esphome/components/xiaomi_wx08zm/__init__.py index e69de29bb2..d795931df9 100644 --- a/esphome/components/xiaomi_wx08zm/__init__.py +++ b/esphome/components/xiaomi_wx08zm/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_wx08zm" diff --git a/esphome/components/xiaomi_xmwsdj04mmc/__init__.py b/esphome/components/xiaomi_xmwsdj04mmc/__init__.py index e69de29bb2..98af32069c 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/__init__.py +++ b/esphome/components/xiaomi_xmwsdj04mmc/__init__.py @@ -0,0 +1 @@ +DOMAIN = "xiaomi_xmwsdj04mmc" diff --git a/esphome/components/xl9535/__init__.py b/esphome/components/xl9535/__init__.py index 2656b60c10..1c6548bb5f 100644 --- a/esphome/components/xl9535/__init__.py +++ b/esphome/components/xl9535/__init__.py @@ -17,6 +17,7 @@ CONF_XL9535 = "xl9535" DEPENDENCIES = ["i2c"] CODEOWNERS = ["@mreditor97"] +DOMAIN = "xl9535" xl9535_ns = cg.esphome_ns.namespace(CONF_XL9535) diff --git a/esphome/components/xpt2046/__init__.py b/esphome/components/xpt2046/__init__.py index 3b8a925bb2..a2b1372b62 100644 --- a/esphome/components/xpt2046/__init__.py +++ b/esphome/components/xpt2046/__init__.py @@ -1,5 +1,7 @@ import esphome.config_validation as cv +DOMAIN = "xpt2046" + CONFIG_SCHEMA = cv.invalid( "This component sould now be used as platform of the Touchscreen component." ) diff --git a/esphome/components/xxtea/__init__.py b/esphome/components/xxtea/__init__.py index 7a7bce1781..79d31dc44a 100644 --- a/esphome/components/xxtea/__init__.py +++ b/esphome/components/xxtea/__init__.py @@ -1,3 +1,4 @@ """ESPHome XXTEA encryption component.""" CODEOWNERS = ["@clydebarrow"] +DOMAIN = "xxtea" diff --git a/esphome/components/yashima/__init__.py b/esphome/components/yashima/__init__.py index e69de29bb2..587acabe78 100644 --- a/esphome/components/yashima/__init__.py +++ b/esphome/components/yashima/__init__.py @@ -0,0 +1 @@ +DOMAIN = "yashima" diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 49654736cc..4f69f00339 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -26,6 +26,7 @@ from .const import ( ) CODEOWNERS = ["@tomaszduda23"] +DOMAIN = "zephyr" class HexValue: diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp index 4400f0a0c5..57a31e302e 100644 --- a/esphome/components/zephyr/gpio.cpp +++ b/esphome/components/zephyr/gpio.cpp @@ -1,5 +1,6 @@ #ifdef USE_ZEPHYR #include "gpio.h" +#include "esphome/core/hal.h" #include #include #include "esphome/core/log.h" @@ -79,7 +80,7 @@ ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const { return ISRInternalGPIOPin((void *) arg); } -void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { +void ZephyrGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const { if (!device_is_ready(this->gpio_)) { ESP_LOGE(TAG, "Cannot attach interrupt: GPIO device not ready"); return; diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 71d1620a67..dd26e34eb8 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -1,7 +1,7 @@ #pragma once #ifdef USE_ZEPHYR -#include "esphome/core/hal.h" +#include "esphome/core/gpio_pin.h" #include #include namespace esphome::zephyr { @@ -16,7 +16,7 @@ struct ZephyrGPIOInterrupt { void *arg{nullptr}; }; -class ZephyrGPIOPin final : public InternalGPIOPin { +class ZephyrGPIOPin final : public GPIOPin { public: ZephyrGPIOPin(const device *gpio, int gpio_size, const char *pin_name_prefix) { this->gpio_ = gpio; @@ -32,14 +32,18 @@ class ZephyrGPIOPin final : public InternalGPIOPin { bool digital_read() override; void digital_write(bool value) override; size_t dump_summary(char *buffer, size_t len) const override; - void detach_interrupt() const override; - ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return this->pin_; } - bool is_inverted() const override { return this->inverted_; } + void detach_interrupt() const; + template void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const { + this->attach_interrupt_(reinterpret_cast(func), arg, type); + } + ISRInternalGPIOPin to_isr() const; + uint8_t get_pin() const { return this->pin_; } + bool is_inverted() const { return this->inverted_; } + bool is_internal() override { return true; } gpio::Flags get_flags() const override { return flags_; } protected: - void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const; const device *gpio_{nullptr}; const char *pin_name_prefix_{nullptr}; gpio::Flags flags_{}; @@ -48,8 +52,7 @@ class ZephyrGPIOPin final : public InternalGPIOPin { bool inverted_{}; bool value_{false}; - // attach_interrupt()/detach_interrupt() are const (matching the base class), so - // the interrupt state they manage has to be mutable. + // attach_interrupt_()/detach_interrupt() are const, so their interrupt state is mutable. mutable ZephyrGPIOInterrupt interrupt_{}; }; diff --git a/esphome/components/zephyr/hal.cpp b/esphome/components/zephyr/hal.cpp index 10e8340a40..96c7df896c 100644 --- a/esphome/components/zephyr/hal.cpp +++ b/esphome/components/zephyr/hal.cpp @@ -23,6 +23,7 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); // components/zephyr/hal.h. void arch_init() { + main_loop_thread() = k_current_get(); #ifdef CONFIG_WATCHDOG if (device_is_ready(WDT)) { static wdt_timeout_cfg wdt_config{}; diff --git a/esphome/components/zephyr/hal.h b/esphome/components/zephyr/hal.h index 11994b68b7..8384225d33 100644 --- a/esphome/components/zephyr/hal.h +++ b/esphome/components/zephyr/hal.h @@ -17,6 +17,14 @@ namespace esphome { /// Zephyr/nRF52: not currently consulted — wake path is platform-specific. __attribute__((always_inline)) inline bool in_isr_context() { return false; } +inline k_tid_t &main_loop_thread() { + static k_tid_t thread = nullptr; + return thread; +} + +/// arch_init() stores the thread before any component runs, so no unset case exists. +__attribute__((always_inline)) inline bool is_main_loop_thread() { return k_current_get() == main_loop_thread(); } + __attribute__((always_inline)) inline void yield() { ::k_yield(); } __attribute__((always_inline)) inline void delay(uint32_t ms) { ::k_msleep(ms); } __attribute__((always_inline)) inline uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } diff --git a/esphome/components/zephyr_mcumgr/__init__.py b/esphome/components/zephyr_mcumgr/__init__.py index e69de29bb2..945cc5807d 100644 --- a/esphome/components/zephyr_mcumgr/__init__.py +++ b/esphome/components/zephyr_mcumgr/__init__.py @@ -0,0 +1 @@ +DOMAIN = "zephyr_mcumgr" diff --git a/esphome/components/zephyr_mcumgr/ota/__init__.py b/esphome/components/zephyr_mcumgr/ota/__init__.py index f5ab04809e..ed7069a540 100644 --- a/esphome/components/zephyr_mcumgr/ota/__init__.py +++ b/esphome/components/zephyr_mcumgr/ota/__init__.py @@ -13,6 +13,7 @@ from esphome.components.zephyr import ( from esphome.components.zephyr.const import ( BOOTLOADER_MCUBOOT, KEY_BOOTLOADER, + KEY_PM_STATIC, KEY_SYSBUILD, ) from esphome.components.zephyr_ble_server import request_ble_l2cap_mtu @@ -146,14 +147,15 @@ async def to_code(config: ConfigType) -> None: zephyr_add_prj_conf("MCUMGR_MGMT_NOTIFICATION_HOOKS", True) zephyr_add_prj_conf("MCUMGR_GRP_IMG_STATUS_HOOKS", True) zephyr_add_prj_conf("MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK", True) + # OS management group (reset, MCUMGR_PARAMETERS query, etc.) isn't transport-specific -- + # every mcumgr transport needs it, not just BLE. + zephyr_add_prj_conf("MCUMGR_GRP_OS", True) + zephyr_add_prj_conf("MCUMGR_GRP_OS_MCUMGR_PARAMS", True) transport = config[CONF_TRANSPORT] if transport[CONF_BLE]: zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT", True) zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT_REASSEMBLY", True) - zephyr_add_prj_conf("MCUMGR_GRP_OS", True) - zephyr_add_prj_conf("MCUMGR_GRP_OS_MCUMGR_PARAMS", True) - zephyr_add_prj_conf("NCS_SAMPLE_MCUMGR_BT_OTA_DFU_SPEEDUP", True) request_ble_l2cap_mtu(498) # matches NCS_SAMPLE_MCUMGR_BT_OTA_DFU_SPEEDUP if CONF_HARDWARE_UART in transport: @@ -207,10 +209,11 @@ async def to_code(config: ConfigType) -> None: bootloader = zephyr_data()[KEY_BOOTLOADER] if bootloader != BOOTLOADER_MCUBOOT: - sections = BOOTLOADER_CONFIG[bootloader] - # Derive partition addresses from the SoftDevice and bootloader sections so - # that the DTS flash map matches what the Partition Manager produces: + # Derive partition addresses from the SoftDevice and bootloader sections + # registered with the Partition Manager (moved in testing mode) so that + # the DTS flash map matches what it produces: # MCUboot sits immediately after the SoftDevice, then slot0, then slot1. + sections = zephyr_data()[KEY_PM_STATIC] mcuboot_size = 0x9000 sd_end = next(s.address + s.size for s in sections if "SoftDevice" in s.name) bl_start = next(s.address for s in sections if "Adafruit" in s.name) diff --git a/esphome/components/zephyr_pwm/__init__.py b/esphome/components/zephyr_pwm/__init__.py index 4bcce84845..4d3382d617 100644 --- a/esphome/components/zephyr_pwm/__init__.py +++ b/esphome/components/zephyr_pwm/__init__.py @@ -1 +1,2 @@ CODEOWNERS = ["@wiomoc"] +DOMAIN = "zephyr_pwm" diff --git a/esphome/components/zephyr_pwm/output.py b/esphome/components/zephyr_pwm/output.py index 2cb62b85b5..604cffb0e0 100644 --- a/esphome/components/zephyr_pwm/output.py +++ b/esphome/components/zephyr_pwm/output.py @@ -4,6 +4,7 @@ from typing import Any from esphome import pins import esphome.codegen as cg from esphome.components import output +from esphome.components.output import DOMAIN as OUTPUT_DOMAIN from esphome.components.zephyr import zephyr_add_overlay_builder, zephyr_add_prj_conf import esphome.config_validation as cv from esphome.const import ( @@ -12,7 +13,6 @@ from esphome.const import ( CONF_ID, CONF_INVERTED, CONF_NUMBER, - CONF_OUTPUT, CONF_PIN, CONF_PLATFORM, ) @@ -74,7 +74,7 @@ def _allocate_blocks() -> None: full_config = fv.full_config.get() zephyr_pwm_conf = [ cfg - for cfg in full_config.get(CONF_OUTPUT, []) + for cfg in full_config.get(OUTPUT_DOMAIN, []) if cfg.get(CONF_PLATFORM) == DOMAIN ] diff --git a/esphome/components/zhlt01/__init__.py b/esphome/components/zhlt01/__init__.py index e69de29bb2..c17009d722 100644 --- a/esphome/components/zhlt01/__init__.py +++ b/esphome/components/zhlt01/__init__.py @@ -0,0 +1 @@ +DOMAIN = "zhlt01" diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 0a99db8958..0dc3b4b6f7 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -3,7 +3,7 @@ from typing import Any from esphome import automation import esphome.codegen as cg -from esphome.components.esp32 import only_on_variant +from esphome.components.esp32 import DOMAIN as ESP32_DOMAIN, only_on_variant from esphome.components.esp32.const import ( VARIANT_ESP32C5, VARIANT_ESP32C6, @@ -57,6 +57,7 @@ from .zigbee_zephyr import ( _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@luar123", "@tomaszduda23"] +DOMAIN = "zigbee" CONFLICTS_WITH = ["openthread"] @@ -73,19 +74,19 @@ def _get_base_schema(cluster_options: list[str] | None = None) -> cv.Schema: schema = cv.Schema( { cv.Optional(CONF_REPORT): cv.All( - cv.requires_component("zigbee"), - cv.requires_component("esp32"), + cv.requires_component(DOMAIN), + cv.requires_component(ESP32_DOMAIN), _check_report_deprecation, cv.enum(REPORT, lower=True), ), cv.Optional(CONF_ENDPOINT): cv.All( - cv.requires_component("zigbee"), - cv.requires_component("esp32"), + cv.requires_component(DOMAIN), + cv.requires_component(ESP32_DOMAIN), cv.int_range(1, CONF_MAX_EP_NUMBER), ), cv.Optional(CONF_USE_DEVICE_TYPE): cv.All( - cv.requires_component("zigbee"), - cv.requires_component("esp32"), + cv.requires_component(DOMAIN), + cv.requires_component(ESP32_DOMAIN), cv.boolean, ), } @@ -96,8 +97,8 @@ def _get_base_schema(cluster_options: list[str] | None = None) -> cv.Schema: cv.OnlyWith( CONF_CLUSTER, ["esp32", "zigbee"], default=cluster_options[0] ): cv.All( - cv.requires_component("zigbee"), - cv.requires_component("esp32"), + cv.requires_component(DOMAIN), + cv.requires_component(ESP32_DOMAIN), cv.one_of(*cluster_options, lower=True), ), } @@ -230,7 +231,7 @@ async def to_code(config: ConfigType) -> None: async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None: - if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + if DOMAIN not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return if CORE.using_zephyr: if not config.get(CONF_ZIGBEE_ID): @@ -242,7 +243,7 @@ async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None: async def setup_sensor(entity: cg.MockObj, config: ConfigType) -> None: - if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + if DOMAIN not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return if CORE.using_zephyr: if not config.get(CONF_ZIGBEE_ID): @@ -254,7 +255,7 @@ async def setup_sensor(entity: cg.MockObj, config: ConfigType) -> None: async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None: - if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + if DOMAIN not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return if CORE.using_zephyr: if not config.get(CONF_ZIGBEE_ID): @@ -272,7 +273,7 @@ async def setup_number( max_value: float, step: float, ) -> None: - if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + if DOMAIN not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return if CORE.using_zephyr: if not config.get(CONF_ZIGBEE_ID): @@ -302,7 +303,7 @@ def consume_endpoint(config: ConfigType) -> ConfigType: def validate_binary_sensor(config: ConfigType) -> ConfigType: - if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + if DOMAIN not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return config if CORE.is_esp32: return validate_binary_sensor_esp32(config) @@ -310,7 +311,7 @@ def validate_binary_sensor(config: ConfigType) -> ConfigType: def validate_sensor(config: ConfigType) -> ConfigType: - if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + if DOMAIN not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return config if CORE.is_esp32: return validate_sensor_esp32(config) @@ -318,7 +319,7 @@ def validate_sensor(config: ConfigType) -> ConfigType: def validate_switch(config: ConfigType) -> ConfigType: - if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + if DOMAIN not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return config if CORE.is_esp32: return validate_switch_esp32(config) @@ -326,7 +327,7 @@ def validate_switch(config: ConfigType) -> ConfigType: def validate_number(config: ConfigType) -> ConfigType: - if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + if DOMAIN not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return config if CORE.is_esp32: return validate_number_esp32(config) diff --git a/esphome/components/zigbee/time/zigbee_time_esp32.cpp b/esphome/components/zigbee/time/zigbee_time_esp32.cpp index a6a25ef2f3..909c714c5e 100644 --- a/esphome/components/zigbee/time/zigbee_time_esp32.cpp +++ b/esphome/components/zigbee/time/zigbee_time_esp32.cpp @@ -1,12 +1,14 @@ #include "zigbee_time_esp32.h" #if defined(USE_ZIGBEE) && defined(USE_ESP32) && defined(USE_TIME) #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome::zigbee { ESPHOME_LOG_TAG(TAG, "zigbee.time"); +static constexpr uint32_t REGISTER_RETRY_TIMEOUT_ID = 0; +static constexpr uint32_t SYNC_RETRY_TIMEOUT_ID = 1; + // This time standard is the number of // seconds since 0 hrs 0 mins 0 sec on 1st January 2000 UTC (Universal Coordinated Time). constexpr time_t EPOCH_2000 = 946684800; @@ -29,7 +31,7 @@ void ZigbeeTime::register_zb_time_() { }; ezb_err_t ret; if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) { - this->set_timeout("zb_time_register", 100, [this]() { this->register_zb_time_(); }); + this->set_timeout(REGISTER_RETRY_TIMEOUT_ID, 100, [this]() { this->register_zb_time_(); }); return; } ret = ezb_zcl_time_server_interface_register(this->endpoint_, time_interface); @@ -69,7 +71,7 @@ void ZigbeeTime::update() { ESP_LOGW(TAG, "Could not acquire Zigbee lock to synchronize time, will retry maximum 3 times"); } if (this->retry_count_ < 3) { - this->set_timeout("zb_time_sync", 100, [this]() { this->update(); }); + this->set_timeout(SYNC_RETRY_TIMEOUT_ID, 100, [this]() { this->update(); }); this->retry_count_++; } else { ESP_LOGW(TAG, "Could not acquire Zigbee lock to synchronize time"); @@ -102,7 +104,6 @@ void ZigbeeTime::set_epoch_time(uint32_t utc) { ESP_LOGV(TAG, "Setting device time to UTC: %u", static_cast(utc)); this->synchronize_epoch_(utc); }); - App.wake_loop_threadsafe(); } void ZigbeeTime::dump_config() { diff --git a/esphome/components/zigbee/time/zigbee_time_zephyr.cpp b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp index f0ef3f2af2..7f0a5f979c 100644 --- a/esphome/components/zigbee/time/zigbee_time_zephyr.cpp +++ b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp @@ -1,7 +1,6 @@ #include "zigbee_time_zephyr.h" #if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME) #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome::zigbee { @@ -48,7 +47,6 @@ void ZigbeeTime::set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); this->has_time_ = true; }); - App.wake_loop_threadsafe(); } void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) { diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp index 676407d751..89417e748d 100644 --- a/esphome/components/zigbee/zigbee_esp32.cpp +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -19,6 +19,8 @@ namespace esphome::zigbee { ESPHOME_LOG_TAG(TAG, "zigbee"); +static constexpr uint32_t COMMISSIONING_RETRY_TIMEOUT_ID = 0; + static ZigbeeComponent *global_zigbee = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size) { @@ -49,8 +51,8 @@ void ZigbeeComponent::factory_reset() { void ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(ezb_bdb_comm_mode_mask_t mode) { if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) { - global_zigbee->set_timeout("zb_init", 100, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); }); - App.wake_loop_threadsafe(); + global_zigbee->set_timeout(COMMISSIONING_RETRY_TIMEOUT_ID, 100, + [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); }); return; } if (ezb_bdb_start_top_level_commissioning(mode) != EZB_ERR_NONE) { @@ -86,10 +88,9 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) { } } else { ESP_LOGW(TAG, "The %s failed with status(0x%02x), please retry", ezb_app_signal_to_string(signal_type), status); - global_zigbee->set_timeout("zb_init", 1000, []() { + global_zigbee->set_timeout(COMMISSIONING_RETRY_TIMEOUT_ID, 1000, []() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_INITIALIZATION); }); - App.wake_loop_threadsafe(); } } break; case EZB_BDB_SIGNAL_STEERING: { @@ -107,15 +108,14 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) { ESP_LOGD(TAG, "Failed to join network with status(0x%02x)", status); if (steering_retry_count < 10) { steering_retry_count++; - global_zigbee->set_timeout("zb_init", 1000, []() { + global_zigbee->set_timeout(COMMISSIONING_RETRY_TIMEOUT_ID, 1000, []() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_NETWORK_STEERING); }); } else { - global_zigbee->set_timeout("zb_init", 600 * 1000, []() { + global_zigbee->set_timeout(COMMISSIONING_RETRY_TIMEOUT_ID, 600 * 1000, []() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_NETWORK_STEERING); }); } - App.wake_loop_threadsafe(); } } break; case EZB_ZDO_SIGNAL_LEAVE: { diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index f1eadbe6f0..1b2d7392df 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -1,7 +1,6 @@ #include "zigbee_zephyr.h" #if defined(USE_ZIGBEE) && defined(USE_NRF52) #include "esphome/core/log.h" -#include "esphome/core/application.h" #include #include #include "esphome/core/hal.h" @@ -120,8 +119,6 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { /* Set default response value. */ p_device_cb_param->status = RET_OK; - App.wake_loop_threadsafe(); - // endpoints are enumerated from 1 if (global_zigbee->callbacks_.size() >= endpoint) { const auto &cb = global_zigbee->callbacks_[endpoint - 1]; @@ -138,7 +135,6 @@ void ZigbeeComponent::on_join_(bool factory_new) { ESP_LOGD(TAG, "Joined the network"); this->join_cb_.call(factory_new); }); - App.wake_loop_threadsafe(); } void ZigbeeComponent::on_start_() { @@ -146,7 +142,6 @@ void ZigbeeComponent::on_start_() { ESP_LOGD(TAG, "Started zigbee stack"); this->start_cb_.call(); }); - App.wake_loop_threadsafe(); } #ifdef USE_ZIGBEE_WIPE_ON_BOOT @@ -199,6 +194,7 @@ void ZigbeeComponent::setup() { zigbee_configure_sleepy_behavior(this->sleepy_); #endif zigbee_enable(); + this->disable_loop(); } #ifdef ESPHOME_LOG_HAS_CONFIG @@ -263,7 +259,10 @@ static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) { zb_buf_free(bufid); } -void ZigbeeComponent::force_report() { this->force_report_ = true; } +void ZigbeeComponent::force_report() { + this->force_report_ = true; + this->enable_loop_soon_any_context(); +} void ZigbeeComponent::add_radio_sleep_time_ms(uint32_t ms) { this->radio_sleep_remainder_ += ms; @@ -277,6 +276,7 @@ void ZigbeeComponent::loop() { this->force_report_ = false; zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0); } + this->disable_loop(); } void ZigbeeComponent::factory_reset() { diff --git a/esphome/components/zio_ultrasonic/__init__.py b/esphome/components/zio_ultrasonic/__init__.py index e69de29bb2..7b2178dc7c 100644 --- a/esphome/components/zio_ultrasonic/__init__.py +++ b/esphome/components/zio_ultrasonic/__init__.py @@ -0,0 +1 @@ +DOMAIN = "zio_ultrasonic" diff --git a/esphome/components/zwave_proxy/__init__.py b/esphome/components/zwave_proxy/__init__.py index 660e3f49a0..e451fe79f4 100644 --- a/esphome/components/zwave_proxy/__init__.py +++ b/esphome/components/zwave_proxy/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import uart +from esphome.components.wifi import DOMAIN as WIFI_DOMAIN import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI import esphome.final_validate as fv @@ -7,6 +8,7 @@ from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["api", "uart"] +DOMAIN = "zwave_proxy" zwave_proxy_ns = cg.esphome_ns.namespace("zwave_proxy") ZWaveProxy = zwave_proxy_ns.class_("ZWaveProxy", cg.Component, uart.UARTDevice) @@ -14,7 +16,7 @@ ZWaveProxy = zwave_proxy_ns.class_("ZWaveProxy", cg.Component, uart.UARTDevice) def final_validate(config: ConfigType) -> None: full_config = fv.full_config.get() - if (wifi_conf := full_config.get(CONF_WIFI)) and ( + if (wifi_conf := full_config.get(WIFI_DOMAIN)) and ( wifi_conf.get(CONF_POWER_SAVE_MODE).lower() != "none" ): raise cv.Invalid( diff --git a/esphome/components/zyaura/__init__.py b/esphome/components/zyaura/__init__.py index e69de29bb2..dcabd7a6bd 100644 --- a/esphome/components/zyaura/__init__.py +++ b/esphome/components/zyaura/__init__.py @@ -0,0 +1 @@ +DOMAIN = "zyaura" diff --git a/esphome/core/alloc_helpers.h b/esphome/core/alloc_helpers.h index 1da3162333..6f824b5383 100644 --- a/esphome/core/alloc_helpers.h +++ b/esphome/core/alloc_helpers.h @@ -10,12 +10,16 @@ /// Prefer the stack-based alternatives documented on each function instead. /// New code should avoid using these functions. +#include #include #include #include #include +#include #include +#include "esphome/core/helpers.h" + namespace esphome { // --- String helpers (allocating) --- @@ -70,6 +74,19 @@ std::string format_hex(const uint8_t *data, size_t length); /// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. std::string format_hex(const std::vector &data); +/// Format an unsigned integer in lowercased hex, starting with the most significant byte. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +template::value, int> = 0> std::string format_hex(T val) { + val = convert_big_endian(val); + return format_hex(reinterpret_cast(&val), sizeof(T)); +} + +/// Format the std::array \p data in lowercased hex. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +template std::string format_hex(const std::array &data) { + return format_hex(data.data(), data.size()); +} + /// Format a byte array in pretty-printed, human-readable hex format. /// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true); @@ -90,10 +107,25 @@ std::string format_hex_pretty(const std::vector &data, char separator /// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true); +/// Format an unsigned integer in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +template::value, int> = 0> +std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) { + val = convert_big_endian(val); + return format_hex_pretty(reinterpret_cast(&val), sizeof(T), separator, show_length); +} + /// Format the byte array \p data of length \p len in binary. /// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. std::string format_bin(const uint8_t *data, size_t length); +/// Format an unsigned integer in binary, starting with the most significant byte. +/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. +template::value, int> = 0> std::string format_bin(T val) { + val = convert_big_endian(val); + return format_bin(reinterpret_cast(&val), sizeof(T)); +} + // --- Base64 helpers (allocating) --- /// Encode a byte buffer to base64 string. diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 0aa2dc21a2..edcd922aeb 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -292,14 +292,17 @@ void Component::status_set_warning(const LogString *message) { } void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const LogString *message) { - if (!this->set_status_flag_(STATUS_LED_ERROR)) - return; - ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), - message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); - if (message != nullptr) { + if (this->set_error_flag_(message) && message != nullptr) { store_component_error_message(this, message); } } +bool Component::set_error_flag_(const LogString *message) { + if (!this->set_status_flag_(STATUS_LED_ERROR)) + return false; + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); + return true; +} void Component::status_clear_warning_slow_path_() { this->component_state_ &= ~STATUS_LED_WARNING; // Clear the app-wide STATUS_LED_WARNING bit only if setup has finished @@ -329,6 +332,21 @@ void Component::status_momentary_error(const char *name, uint32_t length) { this->status_set_error(); this->set_timeout(name, length, [this]() { this->status_clear_error(); }); } +void Component::status_momentary_warning(uint32_t length) { + this->status_momentary_warning(static_cast(nullptr), length); +} +void Component::status_momentary_warning(const LogString *message, uint32_t length) { + this->status_set_warning(message); + this->set_timeout(InternalSchedulerID::STATUS_WARNING, length, [this]() { this->status_clear_warning(); }); +} +void Component::status_momentary_error(uint32_t length) { + this->status_momentary_error(static_cast(nullptr), length); +} +void Component::status_momentary_error(const LogString *message, uint32_t length) { + // Not stored: a cleared momentary error must not show up later as a FAILED reason + this->set_error_flag_(message); + this->set_timeout(InternalSchedulerID::STATUS_ERROR, length, [this]() { this->status_clear_error(); }); +} void Component::dump_config() {} // Function implementation of LOG_UPDATE_INTERVAL macro to reduce code size diff --git a/esphome/core/component.h b/esphome/core/component.h index ecaf863ecf..0e95f8d4e9 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -67,6 +67,8 @@ inline constexpr uint32_t SCHEDULER_DONT_RUN = 4294967295UL; /// with component-level NUMERIC_ID values, even if the uint32_t values overlap. enum class InternalSchedulerID : uint32_t { POLLING_UPDATE = 0, // PollingComponent interval + STATUS_WARNING = 1, // status_momentary_warning() without a name + STATUS_ERROR = 2, // status_momentary_error() without a name }; // Forward declaration @@ -298,24 +300,24 @@ class Component { this->status_clear_error_slow_path_(); } - /** Set warning status flag and automatically clear it after a timeout. - * - * @param name Identifier for the timeout (used to cancel/replace existing timeouts with the same name). - * Must be a static string literal (stored in flash/rodata), not a temporary or dynamic string. - * This is NOT a message to display - use status_set_warning() with a message if logging is needed. - * @param length Duration in milliseconds before the warning is automatically cleared. - */ + // Remove before 2027.5.0 + ESPDEPRECATED("Use status_momentary_warning() without a name. Removed in 2027.5.0", "2026.11.0") void status_momentary_warning(const char *name, uint32_t length = 5000); - /** Set error status flag and automatically clear it after a timeout. - * - * @param name Identifier for the timeout (used to cancel/replace existing timeouts with the same name). - * Must be a static string literal (stored in flash/rodata), not a temporary or dynamic string. - * This is NOT a message to display - use status_set_error() with a message if logging is needed. - * @param length Duration in milliseconds before the error is automatically cleared. - */ + /// Set warning status flag and clear it after `length` ms. A new call restarts the timeout. + /// `message` is logged in place of "unspecified" when the flag gets set. + void status_momentary_warning(uint32_t length = 5000); + void status_momentary_warning(const LogString *message, uint32_t length = 5000); + + // Remove before 2027.5.0 + ESPDEPRECATED("Use status_momentary_error() without a name. Removed in 2027.5.0", "2026.11.0") void status_momentary_error(const char *name, uint32_t length = 5000); + /// Set error status flag and clear it after `length` ms. A new call restarts the timeout. + /// `message` is logged in place of "unspecified"; unlike status_set_error() it is not stored. + void status_momentary_error(uint32_t length = 5000); + void status_momentary_error(const LogString *message, uint32_t length = 5000); + bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; } /** Get the integration where this component was declared as a LogString for logging. @@ -356,6 +358,8 @@ class Component { /// Note: Callers often use the return value to decide whether to log a warning/error, /// so once a flag is set, subsequent (potentially different) messages may be suppressed. bool set_status_flag_(uint8_t flag); + /// Set the error flag and log it; true when it was not set before. + bool set_error_flag_(const LogString *message); /** Set an interval function with a const char* name. Empty name means no cancelling possible. * diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 12f61467d6..731c0d2017 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -364,7 +364,6 @@ #ifdef USE_ESP32 #define USE_ESP32_CRASH_HANDLER #define USE_ESP32_VASPRINTF_STUB -#define USE_ESP32_INTERNAL_GPIO #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 768 @@ -474,6 +473,7 @@ #define USE_USB_UART_CP210X #define USE_USB_UART_FT23XX #define USE_USB_UART_PL2303 +#define USE_GPIO_HOLD // USB identity on serial proxy ports needs the usb_host stack #if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4) diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h index 43db3b7c0c..8ed0d70f6b 100644 --- a/esphome/core/gpio.h +++ b/esphome/core/gpio.h @@ -1,141 +1,44 @@ #pragma once -#include -#include +#include "esphome/core/gpio_pin.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" +// Each platform has exactly one internal pin class, so InternalGPIOPin is an alias for +// it instead of an abstract base class: calls through the alias are direct and can be inlined. +#if defined(USE_ESP32) +#include "esphome/components/esp32/gpio.h" +namespace esphome { +using InternalGPIOPin = esp32::ESP32InternalGPIOPin; +} +#elif defined(USE_ESP8266) +#include "esphome/components/esp8266/gpio.h" +namespace esphome { +using InternalGPIOPin = esp8266::ESP8266GPIOPin; +} +#elif defined(USE_LIBRETINY) +#include "esphome/components/libretiny/gpio_arduino.h" +namespace esphome { +using InternalGPIOPin = libretiny::ArduinoInternalGPIOPin; +} +#elif defined(USE_RP2) +#include "esphome/components/rp2/gpio.h" +namespace esphome { +using InternalGPIOPin = rp2::RP2GPIOPin; +} +#elif defined(USE_HOST) +#include "esphome/components/host/gpio.h" +namespace esphome { +using InternalGPIOPin = host::HostGPIOPin; +} +#elif defined(USE_ZEPHYR) +#include "esphome/components/zephyr/gpio.h" +namespace esphome { +using InternalGPIOPin = zephyr::ZephyrGPIOPin; +} +#else +#error "gpio.h: no internal pin class for this platform" +#endif namespace esphome { - -/// Maximum buffer size for dump_summary output -inline constexpr size_t GPIO_SUMMARY_MAX_LEN = 48; - -#ifdef USE_ESP8266 -#define LOG_PIN(prefix, pin) log_pin(TAG, F(prefix), pin) -#else -#define LOG_PIN(prefix, pin) log_pin(TAG, prefix, pin) -#endif - -// put GPIO flags in a namespace to not pollute esphome namespace -namespace gpio { - -enum Flags : uint8_t { - // Can't name these just INPUT because of Arduino defines :( - FLAG_NONE = 0x00, - FLAG_INPUT = 0x01, - FLAG_OUTPUT = 0x02, - FLAG_OPEN_DRAIN = 0x04, - FLAG_PULLUP = 0x08, - FLAG_PULLDOWN = 0x10, -}; - -class FlagsHelper { - public: - constexpr FlagsHelper(Flags val) : val_(val) {} - constexpr operator Flags() const { return val_; } - - protected: - Flags val_; -}; -constexpr FlagsHelper operator&(Flags lhs, Flags rhs) { - return static_cast(static_cast(lhs) & static_cast(rhs)); -} -constexpr FlagsHelper operator|(Flags lhs, Flags rhs) { - return static_cast(static_cast(lhs) | static_cast(rhs)); -} - -enum InterruptType : uint8_t { - INTERRUPT_RISING_EDGE = 1, - INTERRUPT_FALLING_EDGE = 2, - INTERRUPT_ANY_EDGE = 3, - INTERRUPT_LOW_LEVEL = 4, - INTERRUPT_HIGH_LEVEL = 5, -}; - -} // namespace gpio - -class GPIOPin { - public: - virtual void setup() = 0; - - virtual void pin_mode(gpio::Flags flags) = 0; - - /** - * @brief Retrieve GPIO pin flags. - * - * @return The GPIO flags describing the pin mode and properties. - */ - virtual gpio::Flags get_flags() const = 0; - - virtual bool digital_read() = 0; - - virtual void digital_write(bool value) = 0; - - /// Write a summary of this pin to the provided buffer. - /// @param buffer The buffer to write to - /// @param len The size of the buffer (must be > 0) - /// @return The number of characters that would be written (excluding null terminator), - /// which may exceed len-1 if truncation occurred (snprintf semantics) - virtual size_t dump_summary(char *buffer, size_t len) const; - - virtual bool is_internal() { return false; } -}; - -/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions) -class ISRInternalGPIOPin { - public: - ISRInternalGPIOPin() = default; - ISRInternalGPIOPin(void *arg) : arg_(arg) {} - bool digital_read(); - void digital_write(bool value); - void clear_interrupt(); - void pin_mode(gpio::Flags flags); - - protected: - void *arg_{nullptr}; -}; - -class InternalGPIOPin : public GPIOPin { - public: - template void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const { - this->attach_interrupt(reinterpret_cast(func), arg, type); - } - - virtual void detach_interrupt() const = 0; - - virtual ISRInternalGPIOPin to_isr() const = 0; - - virtual uint8_t get_pin() const = 0; - - bool is_internal() override { return true; } - - virtual bool is_inverted() const = 0; - - protected: - virtual void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const = 0; -}; - -// Inline default implementation for GPIOPin::dump_summary. -// Writes an empty summary; subclasses override to provide pin details. -inline size_t GPIOPin::dump_summary(char *buffer, size_t len) const { - if (len > 0) - buffer[0] = '\0'; - return 0; -} - -// Inline helper for log_pin - allows compiler to inline into log_pin in gpio.cpp -inline void log_pin_with_prefix(const char *tag, const char *prefix, GPIOPin *pin) { - char buffer[GPIO_SUMMARY_MAX_LEN]; - size_t len = pin->dump_summary(buffer, sizeof(buffer)); - len = std::min(len, sizeof(buffer) - 1); - esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, "%s%.*s", prefix, (int) len, buffer); -} - -// log_pin function declarations - implementation in gpio.cpp -#ifdef USE_ESP8266 -void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin); -#else -void log_pin(const char *tag, const char *prefix, GPIOPin *pin); -#endif - +static_assert(InternalGPIOPinContract, + "The platform's internal pin class is missing part of the InternalGPIOPin surface " + "(esphome/core/gpio_pin.h)"); } // namespace esphome diff --git a/esphome/core/gpio_pin.h b/esphome/core/gpio_pin.h new file mode 100644 index 0000000000..6c9acfa8d6 --- /dev/null +++ b/esphome/core/gpio_pin.h @@ -0,0 +1,147 @@ +#pragma once +#include +#include +#include +#include + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { + +/// Maximum buffer size for dump_summary output +inline constexpr size_t GPIO_SUMMARY_MAX_LEN = 48; + +#ifdef USE_ESP8266 +#define LOG_PIN(prefix, pin) log_pin(TAG, F(prefix), pin) +#else +#define LOG_PIN(prefix, pin) log_pin(TAG, prefix, pin) +#endif + +// put GPIO flags in a namespace to not pollute esphome namespace +namespace gpio { + +enum Flags : uint8_t { + // Can't name these just INPUT because of Arduino defines :( + FLAG_NONE = 0x00, + FLAG_INPUT = 0x01, + FLAG_OUTPUT = 0x02, + FLAG_OPEN_DRAIN = 0x04, + FLAG_PULLUP = 0x08, + FLAG_PULLDOWN = 0x10, + FLAG_HOLD = 0x20, +}; + +class FlagsHelper { + public: + constexpr FlagsHelper(Flags val) : val_(val) {} + constexpr operator Flags() const { return val_; } + + protected: + Flags val_; +}; +constexpr FlagsHelper operator&(Flags lhs, Flags rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr FlagsHelper operator|(Flags lhs, Flags rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +enum InterruptType : uint8_t { + INTERRUPT_RISING_EDGE = 1, + INTERRUPT_FALLING_EDGE = 2, + INTERRUPT_ANY_EDGE = 3, + INTERRUPT_LOW_LEVEL = 4, + INTERRUPT_HIGH_LEVEL = 5, +}; + +} // namespace gpio + +class GPIOPin { + public: + virtual void setup() = 0; + + virtual void pin_mode(gpio::Flags flags) = 0; + + /** + * @brief Retrieve GPIO pin flags. + * + * @return The GPIO flags describing the pin mode and properties. + */ + virtual gpio::Flags get_flags() const = 0; + + virtual bool digital_read() = 0; + + virtual void digital_write(bool value) = 0; + + /// Write a summary of this pin to the provided buffer. + /// @param buffer The buffer to write to + /// @param len The size of the buffer (must be > 0) + /// @return The number of characters that would be written (excluding null terminator), + /// which may exceed len-1 if truncation occurred (snprintf semantics) + virtual size_t dump_summary(char *buffer, size_t len) const; + + /// Only the platform pin class behind the InternalGPIOPin alias may return true: + /// callers static_cast to InternalGPIOPin on it. + virtual bool is_internal() { return false; } +#ifdef USE_GPIO_HOLD + bool get_hold() const { return this->get_flags() & gpio::FLAG_HOLD; } + virtual bool is_held() const { return false; } +#else + bool get_hold() const { return false; } + bool is_held() const { return false; } +#endif +}; + +/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions) +class ISRInternalGPIOPin { + public: + ISRInternalGPIOPin() = default; + ISRInternalGPIOPin(void *arg) : arg_(arg) {} + bool digital_read(); + void digital_write(bool value); + void clear_interrupt(); + void pin_mode(gpio::Flags flags); + + protected: + void *arg_{nullptr}; +}; + +// What callers of the InternalGPIOPin alias (bound in esphome/core/gpio.h) rely on beyond +// GPIOPin. The class must be final so calls to the GPIOPin virtuals through the alias are direct. +// int is arbitrary: any T * exercises the public attach_interrupt template. +template +concept InternalGPIOPinContract = std::derived_from && std::is_final_v && + requires(const T pin, void (*func)(int *), int *arg, gpio::InterruptType type) { + { pin.get_pin() } -> std::same_as; + { pin.is_inverted() } -> std::same_as; + { pin.to_isr() } -> std::same_as; + { pin.detach_interrupt() } -> std::same_as; + { pin.attach_interrupt(func, arg, type) } -> std::same_as; +}; + +// Inline default implementation for GPIOPin::dump_summary. +// Writes an empty summary; subclasses override to provide pin details. +inline size_t GPIOPin::dump_summary(char *buffer, size_t len) const { + if (len > 0) + buffer[0] = '\0'; + return 0; +} + +// Inline helper for log_pin - allows compiler to inline into log_pin in gpio.cpp +inline void log_pin_with_prefix(const char *tag, const char *prefix, GPIOPin *pin) { + char buffer[GPIO_SUMMARY_MAX_LEN]; + size_t len = pin->dump_summary(buffer, sizeof(buffer)); + len = std::min(len, sizeof(buffer) - 1); + esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, "%s%.*s", prefix, (int) len, buffer); +} + +// log_pin function declarations - implementation in gpio.cpp +#ifdef USE_ESP8266 +void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin); +#else +void log_pin(const char *tag, const char *prefix, GPIOPin *pin); +#endif + +} // namespace esphome diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 4c5a19c6d1..dd3618f623 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -4,9 +4,17 @@ #include #include "gpio.h" #include "esphome/core/defines.h" + #include "esphome/core/time_64.h" #include "esphome/core/time_conversion.h" +/// Address tables in ESP8266 flash must be constant-initialized: a runtime initializer would write to flash. +#if defined(USE_ESP8266) && defined(__GNUC__) && !defined(__clang__) +#define ESPHOME_FLASH_CONSTINIT constinit +#else +#define ESPHOME_FLASH_CONSTINIT +#endif + // Per-platform HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(), // inline yield/delay/micros/millis/millis_64 wrappers, ESP8266 progmem // helpers) live next to each platform component as components//hal.h diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 6130f7e27d..1b332515f6 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -261,7 +261,6 @@ bool str_contains_ignore_case_p(const char *haystack, PGM_P needle) { } #endif // USE_ESP8266 -// str_truncate, str_until, str_lower_case, str_upper_case, str_snake_case moved to alloc_helpers.cpp char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { if (buffer_size == 0) { return buffer; @@ -274,8 +273,6 @@ char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { return buffer; } -// str_sanitize, str_snprintf, str_sprintf moved to alloc_helpers.cpp - size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep, const char *suffix_ptr, size_t suffix_len) { size_t total_len = name_len + 1 + suffix_len; @@ -309,8 +306,6 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { return chars; } -// format_mac_address_pretty moved to alloc_helpers.cpp - // Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase. // When separator is set, it is written unconditionally after each byte and the last // one is overwritten with '\0', eliminating the per-byte `i < length - 1` check. @@ -428,8 +423,6 @@ const char *json_escape_into_buffer(std::span buf, StringRef value, bool s return buf.data(); } -// format_hex (std::string returning overloads) moved to alloc_helpers.cpp - char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); } @@ -465,8 +458,6 @@ char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint16_t *dat return buffer; } -// format_hex_pretty (all std::string returning overloads) moved to alloc_helpers.cpp - char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { if (buffer_size == 0) { return buffer; @@ -488,8 +479,6 @@ char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_ return buffer; } -// format_bin moved to alloc_helpers.cpp - ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0) return PARSE_ON; @@ -537,8 +526,6 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de } } -// value_accuracy_to_string moved to alloc_helpers.cpp - // Fast float-to-string for accuracy_decimals 0-3 (covers virtually all sensor usage). // Avoids snprintf("%.*f") which pulls in heavy float formatting machinery. // Caller must guarantee value is finite and |value| * mult fits in uint32_t. @@ -639,8 +626,6 @@ static constexpr uint8_t base64_char_value(uint8_t c) { return INVALID_BASE64_CHAR; } -// base64_encode (both overloads) moved to alloc_helpers.cpp - size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) { return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); } @@ -668,8 +653,6 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b return out; } -// base64_decode (vector-returning overload) moved to alloc_helpers.cpp - /// Decode base64/base64url string directly into vector of little-endian int32 values /// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted) /// @param out Output vector (cleared and filled with decoded int32 values) @@ -790,8 +773,6 @@ void HighFrequencyLoopRequester::stop() { this->started_ = false; } -// get_mac_address, get_mac_address_pretty moved to alloc_helpers.cpp - void get_mac_address_into_buffer(std::span buf) { uint8_t mac[MAC_ADDRESS_SIZE]; get_mac_address_raw(mac); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b88a9d70e9..a2ede27341 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -26,12 +26,6 @@ #include "esphome/core/optional.h" #include "esphome/core/time_conversion.h" -// Backward compatibility re-export of heap-allocating helpers. -// These functions have moved to alloc_helpers.h. External components should -// update their includes to use #include "esphome/core/alloc_helpers.h" directly. -// This re-export will be removed in 2026.11.0. -#include "esphome/core/alloc_helpers.h" - #ifdef USE_ESP8266 #include #include @@ -1105,13 +1099,8 @@ inline bool str_contains_ignore_case(const char *haystack, const char *needle) { } #endif // USE_ESP8266 -// str_truncate moved to alloc_helpers.h - remove this include before 2026.11.0 - -// str_until, str_lower_case, str_upper_case moved to alloc_helpers.h - remove this comment before 2026.11.0 - /// Convert a single char to snake_case: lowercase and space to underscore. constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } -// str_snake_case moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore. constexpr char to_sanitized_char(char c) { @@ -1134,8 +1123,6 @@ template inline char *str_sanitize_to(char (&buffer)[N], const char *s return str_sanitize_to(buffer, N, str); } -// str_sanitize moved to alloc_helpers.h - remove this comment before 2026.11.0 - /// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations. /// This computes object_id hashes directly from names without creating an intermediate buffer. /// IMPORTANT: Must match Python fnv1_hash_object_id() in esphome/helpers.py. @@ -1150,8 +1137,6 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) { return hash; } -// str_snprintf, str_sprintf moved to alloc_helpers.h - remove this comment before 2026.11.0 - #ifdef USE_ESP8266 // ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM) // Format strings must be wrapped with PSTR() macro @@ -1605,32 +1590,6 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE); } -// format_mac_address_pretty, format_hex (all overloads) moved to alloc_helpers.h -// Remove this comment and the template overloads below before 2026.11.0 - -/// Format an unsigned integer in lowercased hex, starting with the most significant byte. -/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -template::value, int> = 0> std::string format_hex(T val) { - val = convert_big_endian(val); - return format_hex(reinterpret_cast(&val), sizeof(T)); -} -/// Format the std::array \p data in lowercased hex. -/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -template std::string format_hex(const std::array &data) { - return format_hex(data.data(), data.size()); -} - -// format_hex_pretty (all overloads) moved to alloc_helpers.h -// Remove this comment and the template overload below before 2026.11.0 - -/// Format an unsigned integer in pretty-printed, human-readable hex format. -/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. -template::value, int> = 0> -std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) { - val = convert_big_endian(val); - return format_hex_pretty(reinterpret_cast(&val), sizeof(T), separator, show_length); -} - /// Calculate buffer size needed for format_bin_to: "01234567...\0" = bytes * 8 + 1 constexpr size_t format_bin_size(size_t byte_count) { return byte_count * 8 + 1; } @@ -1684,15 +1643,6 @@ inline char *format_bin_to(char (&buffer)[N], T val) { return format_bin_to(buffer, reinterpret_cast(&val), sizeof(T)); } -// format_bin moved to alloc_helpers.h - remove this comment and template overload before 2026.11.0 - -/// Format an unsigned integer in binary, starting with the most significant byte. -/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. -template::value, int> = 0> std::string format_bin(T val) { - val = convert_big_endian(val); - return format_bin(reinterpret_cast(&val), sizeof(T)); -} - /// Return values for parse_on_off(). enum ParseOnOffState : uint8_t { PARSE_NONE = 0, @@ -1703,8 +1653,6 @@ enum ParseOnOffState : uint8_t { /// Parse a string that contains either on, off or toggle. ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); -// value_accuracy_to_string moved to alloc_helpers.h - remove this comment before 2026.11.0 - /// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null) static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64; @@ -1717,8 +1665,6 @@ size_t value_accuracy_with_uom_to_buf(std::span bu /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); -// base64_encode (both overloads), base64_decode (vector overload) moved to alloc_helpers.h -// Remove this comment before 2026.11.0 size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); @@ -2177,8 +2123,6 @@ class HighFrequencyLoopRequester { /// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes). void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter) -// get_mac_address, get_mac_address_pretty moved to alloc_helpers.h - remove this comment before 2026.11.0 - /// Get the device MAC address into the given buffer, in lowercase hex notation. /// Assumes buffer length is MAC_ADDRESS_BUFFER_SIZE (12 digits for hexadecimal representation followed by null /// terminator). diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h deleted file mode 100644 index fbb089e906..0000000000 --- a/esphome/core/ring_buffer.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -// Deprecated: include "esphome/components/ring_buffer/ring_buffer.h" and use -// esphome::ring_buffer::RingBuffer. This shim will be removed in 2026.11.0. -#if __has_include("esphome/components/ring_buffer/ring_buffer.h") -#include "esphome/components/ring_buffer/ring_buffer.h" -#else -#error \ - "esphome/components/ring_buffer/ring_buffer.h not found. Add 'ring_buffer' to your component's AUTO_LOAD list to use esphome::ring_buffer::RingBuffer." -#endif -#include "esphome/core/helpers.h" // for ESPDEPRECATED - -namespace esphome { - -using RingBuffer ESPDEPRECATED("Use esphome::ring_buffer::RingBuffer instead. Removed in 2026.11.0.", - "2026.5.0") = ring_buffer::RingBuffer; - -} // namespace esphome - -#endif diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 5acd2eec27..5d0b96c38f 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -138,79 +138,84 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } // Take lock early to protect scheduler_item_pool_head_ access - LockGuard guard{this->lock_}; + { + LockGuard guard{this->lock_}; - // Create and populate the scheduler item - SchedulerItem *item = this->get_item_from_pool_locked_(); - // SELF_POINTER items store the source name (owning script) in the union slot instead of a component. - if (name_type == NameType::SELF_POINTER) { - item->source_name = source; - } else { - item->component = component; - } - item->set_name(name_type, static_name, hash_or_id); - item->type = type; - // Use destroy + placement-new instead of move-assignment. - // GCC's std::function::operator=(function&&) does a full swap dance even when the - // target is empty. Since recycled/new items always have an empty callback, we can - // destroy the empty one (no-op) and move-construct directly, saving ~40 bytes of - // swap/destructor code on Xtensa. - item->callback.~function(); - new (&item->callback) std::function(std::move(func)); - // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use - this->set_item_removed_(item, false); + // Create and populate the scheduler item + SchedulerItem *item = this->get_item_from_pool_locked_(); + // SELF_POINTER items store the source name (owning script) in the union slot instead of a component. + if (name_type == NameType::SELF_POINTER) { + item->source_name = source; + } else { + item->component = component; + } + item->set_name(name_type, static_name, hash_or_id); + item->type = type; + // Use destroy + placement-new instead of move-assignment. + // GCC's std::function::operator=(function&&) does a full swap dance even when the + // target is empty. Since recycled/new items always have an empty callback, we can + // destroy the empty one (no-op) and move-construct directly, saving ~40 bytes of + // swap/destructor code on Xtensa. + item->callback.~function(); + new (&item->callback) std::function(std::move(func)); + // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use + this->set_item_removed_(item, false); - // Determine target container: defer_queue_ for deferred items, to_add_ for everything else. - // Using a pointer lets both paths share the cancel + push_back epilogue. - auto *target = &this->to_add_; + // Determine target container: defer_queue_ for deferred items, to_add_ for everything else. + // Using a pointer lets both paths share the cancel + push_back epilogue. + auto *target = &this->to_add_; #ifndef ESPHOME_THREAD_SINGLE - // Special handling for defer() (delay = 0, type = TIMEOUT) - // Single-core platforms don't need thread-safe defer handling - if (delay == 0 && type == SchedulerItem::TIMEOUT) { - // Put in defer queue for guaranteed FIFO execution - target = &this->defer_queue_; - } else + // Special handling for defer() (delay = 0, type = TIMEOUT) + // Single-core platforms don't need thread-safe defer handling + if (delay == 0 && type == SchedulerItem::TIMEOUT) { + // Put in defer queue for guaranteed FIFO execution + target = &this->defer_queue_; + } else #endif /* not ESPHOME_THREAD_SINGLE */ - { - // Only non-defer items need a timestamp for scheduling - const uint64_t now_64 = millis_64(); + { + // Only non-defer items need a timestamp for scheduling + const uint64_t now_64 = millis_64(); - // Type-specific setup - if (type == SchedulerItem::INTERVAL) { - item->interval = delay; - // first execution happens immediately after a random smallish offset - uint32_t offset = this->calculate_interval_offset_(delay); - item->set_next_execution(now_64 + offset); + // Type-specific setup + if (type == SchedulerItem::INTERVAL) { + item->interval = delay; + // first execution happens immediately after a random smallish offset + uint32_t offset = this->calculate_interval_offset_(delay); + item->set_next_execution(now_64 + offset); #ifdef ESPHOME_LOG_HAS_VERBOSE - SchedulerNameLog name_log; - ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", - name_log.format(name_type, static_name, hash_or_id), delay, offset); + SchedulerNameLog name_log; + ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", + name_log.format(name_type, static_name, hash_or_id), delay, offset); #endif - } else { - item->interval = 0; - item->set_next_execution(now_64 + delay); - } + } else { + item->interval = 0; + item->set_next_execution(now_64 + delay); + } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item, name_type, static_name, hash_or_id, delay, now_64); + this->debug_log_timer_(item, name_type, static_name, hash_or_id, delay, now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ - } + } - // Common epilogue: atomic cancel-and-add (unless skip_cancel is true or anonymous) - // Anonymous items (STATIC_STRING with nullptr) can never match anything, so skip the scan. - if (!skip_cancel && (name_type != NameType::STATIC_STRING || static_name != nullptr)) { - this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* find_first= */ true); - } - target->push_back(item); - if (target == &this->to_add_) { - this->to_add_count_increment_locked_(); - } + // Common epilogue: atomic cancel-and-add (unless skip_cancel is true or anonymous) + // Anonymous items (STATIC_STRING with nullptr) can never match anything, so skip the scan. + if (!skip_cancel && (name_type != NameType::STATIC_STRING || static_name != nullptr)) { + this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* find_first= */ true); + } + target->push_back(item); + if (target == &this->to_add_) { + this->to_add_count_increment_locked_(); + } #ifndef ESPHOME_THREAD_SINGLE - else { - this->defer_count_increment_locked_(); - } + else { + this->defer_count_increment_locked_(); + } #endif + } + // A background insertion may shorten a sleep whose deadline was already computed. + if (!is_main_loop_thread()) [[unlikely]] + wake_scheduler_threadsafe(); } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, diff --git a/esphome/core/wake/wake_esp8266.h b/esphome/core/wake/wake_esp8266.h index 73b7a38a35..2f7ffc292f 100644 --- a/esphome/core/wake/wake_esp8266.h +++ b/esphome/core/wake/wake_esp8266.h @@ -11,10 +11,7 @@ namespace esphome { /// Inline implementation — IRAM callers inline this directly. -inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { - // Set the wake-requested flag BEFORE esp_schedule so the consumer is - // guaranteed to see it on its next gate check. - wake_request_set(); +inline void ESPHOME_ALWAYS_INLINE wake_scheduler_impl() { // Skip the post when a wake was already signalled and not yet consumed by // wakeable_delay(): esp_schedule() -> ets_post() can enter SDK WiFi pm code, // which must not be poked per-byte from the software serial RX ISR (see @@ -26,11 +23,19 @@ inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { esp_schedule(); } +inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { + // Set the wake-requested flag BEFORE esp_schedule so the consumer is + // guaranteed to see it on its next gate check. + wake_request_set(); + wake_scheduler_impl(); +} + /// IRAM_ATTR entry point for ISR callers — defined in wake_esp8266.cpp. void wake_loop_any_context(); /// Non-ISR: always inline. inline void wake_loop_threadsafe() { wake_loop_impl(); } +inline void wake_scheduler_threadsafe() { wake_scheduler_impl(); } /// ISR-safe: no task_woken arg because ESP8266 has no FreeRTOS. Caller must be IRAM_ATTR. inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); } diff --git a/esphome/core/wake/wake_freertos.cpp b/esphome/core/wake/wake_freertos.cpp index 458ef51f89..02c628718e 100644 --- a/esphome/core/wake/wake_freertos.cpp +++ b/esphome/core/wake/wake_freertos.cpp @@ -30,9 +30,6 @@ void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); } } // namespace esphome -extern "C" void esphome_wake_loop_threadsafe() { - esphome::wake_request_set(); - esphome_main_task_notify(); -} +extern "C" void esphome_wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); } #endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/wake/wake_freertos.h b/esphome/core/wake/wake_freertos.h index 16afa38fda..1df7d38765 100644 --- a/esphome/core/wake/wake_freertos.h +++ b/esphome/core/wake/wake_freertos.h @@ -34,9 +34,11 @@ __attribute__((always_inline)) inline void wake_main_task_any_context() { void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); void wake_loop_any_context(); +inline void wake_scheduler_threadsafe() { esphome_main_task_notify(); } + inline void wake_loop_threadsafe() { wake_request_set(); - esphome_main_task_notify(); + wake_scheduler_threadsafe(); } namespace internal { diff --git a/esphome/core/wake/wake_host.cpp b/esphome/core/wake/wake_host.cpp index 8cb382a77e..9fc1c71703 100644 --- a/esphome/core/wake/wake_host.cpp +++ b/esphome/core/wake/wake_host.cpp @@ -123,13 +123,12 @@ void wakeable_delay(uint32_t ms) { if (ms == 0) [[unlikely]] { yield(); } - // A socket woke select() early — open the component-phase gate so the - // owning component's loop() drains the data on this tick rather than - // waiting up to loop_interval_ ms. Idempotent if wake_loop_threadsafe() - // already set the flag (wake socket fired); required when an application - // socket fired and nothing else set the flag. + // Application sockets need the component phase to drain queued work. + // The internal wake socket may only be signaling new scheduler work. if (ret > 0) { - wake_request_set(); + const bool only_wake_socket = ret == 1 && g_wake_socket_fd >= 0 && FD_ISSET(g_wake_socket_fd, &g_read_fds); + if (!only_wake_socket) + wake_request_set(); } return; } @@ -146,16 +145,20 @@ void wakeable_delay(uint32_t ms) { } } // namespace internal -void wake_loop_threadsafe() { - // Set flag before sending so the consumer's gate check on the next loop() - // entry observes the wake regardless of select() scheduling. - wake_request_set(); +void wake_scheduler_threadsafe() { if (internal::g_wake_socket_fd >= 0) { const char dummy = 1; ::send(internal::g_wake_socket_fd, &dummy, 1, 0); } } +void wake_loop_threadsafe() { + // Set flag before sending so the consumer's gate check on the next loop() + // entry observes the wake regardless of select() scheduling. + wake_request_set(); + wake_scheduler_threadsafe(); +} + void wake_setup() { // Create UDP socket for wake notifications. internal::g_wake_socket_fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); diff --git a/esphome/core/wake/wake_host.h b/esphome/core/wake/wake_host.h index 9756ed4c39..8c58797df2 100644 --- a/esphome/core/wake/wake_host.h +++ b/esphome/core/wake/wake_host.h @@ -13,6 +13,7 @@ namespace esphome { /// Host: wakes select() via UDP loopback socket. Defined in wake_host.cpp. void wake_loop_threadsafe(); +void wake_scheduler_threadsafe(); /// Register a socket file descriptor with the host select() loop. Not /// thread-safe — main loop only. Returns false if fd is invalid or diff --git a/esphome/core/wake/wake_rp2.h b/esphome/core/wake/wake_rp2.h index 715e5aca0c..4f5439dbb8 100644 --- a/esphome/core/wake/wake_rp2.h +++ b/esphome/core/wake/wake_rp2.h @@ -11,12 +11,16 @@ namespace esphome { +inline void wake_scheduler_threadsafe() { + g_main_loop_woke = true; + __sev(); +} + inline void wake_loop_any_context() { // Set the wake-requested flag BEFORE the SEV so the consumer is guaranteed // to see it on its next gate check. wake_request_set(); - g_main_loop_woke = true; - __sev(); + wake_scheduler_threadsafe(); } inline void wake_loop_threadsafe() { wake_loop_any_context(); } diff --git a/esphome/core/wake/wake_zephyr.cpp b/esphome/core/wake/wake_zephyr.cpp index 577d53f5d9..2c46fbc951 100644 --- a/esphome/core/wake/wake_zephyr.cpp +++ b/esphome/core/wake/wake_zephyr.cpp @@ -21,9 +21,11 @@ K_SEM_DEFINE(esphome_wake_sem, 0, 1); // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) volatile uint8_t g_wake_requested = 0; +void wake_scheduler_threadsafe() { k_sem_give(&esphome_wake_sem); } + void wake_loop_threadsafe() { wake_request_set(); - k_sem_give(&esphome_wake_sem); + wake_scheduler_threadsafe(); } namespace internal { diff --git a/esphome/core/wake/wake_zephyr.h b/esphome/core/wake/wake_zephyr.h index c89cfc68e9..04cbfb230b 100644 --- a/esphome/core/wake/wake_zephyr.h +++ b/esphome/core/wake/wake_zephyr.h @@ -11,6 +11,7 @@ namespace esphome { /// Zephyr: wakes the main loop via k_sem_give(). Thread- and ISR-safe. /// Defined in wake_zephyr.cpp. void wake_loop_threadsafe(); +void wake_scheduler_threadsafe(); inline void wake_loop_any_context() { wake_loop_threadsafe(); } diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 267e86c2ed..9c9fed6601 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -10,6 +10,7 @@ from esphome.core import ( ID, Define, EnumValue, + EsphomeError, HexInt, Lambda, Library, @@ -269,6 +270,13 @@ class FlashStringLiteral(Literal): return f"ESPHOME_F({cpp_string_escape(self.string)})" +def progmem_string(value: str) -> Expression: + """A ``std::string`` argument from a literal that stays in flash on ESP8266.""" + if CORE.is_esp8266: + return RawExpression(f"progmem_string({FlashStringLiteral(value)})") + return safe_exp(value) + + class IntLiteral(Literal): __slots__ = ("i",) @@ -440,13 +448,16 @@ class LineComment(Statement): class ProgmemAssignmentExpression(AssignmentExpression): - __slots__ = () + __slots__ = ("constexpr",) - def __init__(self, type_, name, rhs): + def __init__(self, type_, name, rhs, constexpr: bool = True): super().__init__(type_, "", name, rhs) + self.constexpr = constexpr def __str__(self): - return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}" + if self.constexpr: + return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}" + return f"ESPHOME_FLASH_CONSTINIT static {self.type} const {self.name}[] PROGMEM = {self.rhs}" class StaticConstAssignmentExpression(AssignmentExpression): @@ -486,20 +497,34 @@ def extern_progmem_array( def shared_progmem_array( - name: str, type_: "MockObjClass", rhs: SafeExpType, *, share: bool = True + name: str, + type_: "MockObjClass", + rhs: SafeExpType, + *, + share: bool = True, + constexpr: bool = True, ) -> "MockObj": """Emit a global PROGMEM array once per distinct type and contents; later calls reuse it. - The array is ``static constexpr``, so elements must be constant expressions and lambdas - must be captureless. Its name is made unique against every config id and variable. + The array is ``static constexpr`` by default, so elements must be constant expressions and + lambdas must be captureless. Its name is made unique against every config id and variable. ``share=False`` always emits a new array, e.g. for lambdas that may keep static state. + ``constexpr=False`` is for tables of generated object pointers: each top level variable + element must pass ``is_static_pointer`` or ``EsphomeError`` is raised. """ from esphome.config import iter_ids from esphome.config_validation import RESERVED_IDS arrays: dict[str, MockObj] = CORE.data.setdefault("shared_progmem_array", {}) rhs = safe_exp(rhs) - key = f"{type_} {rhs}" + if not constexpr and isinstance(rhs, ArrayInitializer): + for arg in rhs.args: + if isinstance(arg, MockObj) and not is_static_pointer(arg): + raise EsphomeError( + f"'{arg}' must be created with cg.new_Pvariable so its address is known " + "at compile time" + ) + key = f"{type_} {rhs} {constexpr}" if share and (array := arrays.get(key)) is not None: return array used = {str(i) for i, _ in iter_ids(CORE.config)} @@ -507,7 +532,7 @@ def shared_progmem_array( used |= set(RESERVED_IDS) | CORE.loaded_integrations id_ = ID(ensure_unique_string(name, used), is_declaration=True, type=type_) # Global, so any scope can use it; anything a lambda references is already declared. - CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs)) + CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs, constexpr)) array = MockObj(id_, ".") CORE.register_variable(id_, array) if share: @@ -696,6 +721,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": ) placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args) CORE.add(ExpressionStatement(placement_new)) + CORE.data.setdefault(_STATIC_POINTER_IDS, set()).add(id_.id) else: decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) CORE.add_global(decl) @@ -705,6 +731,20 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": return obj +_STATIC_POINTER_IDS = "static_pointer_ids" + + +def is_static_pointer(obj: SafeExpType) -> bool: + """True if ``obj`` is a Pvariable whose object was placement constructed in static storage. + + Its pointer is then an address known at compile time and may appear in a ``constexpr=False`` + PROGMEM table; a pointer assigned in ``setup()`` may not. + """ + return isinstance(obj, MockObj) and str(obj.base) in CORE.data.get( + _STATIC_POINTER_IDS, () + ) + + def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj": """Declare a new pointer variable in the code generation by calling it's constructor with the given arguments. diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index effb817f35..af3fda6349 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -2,7 +2,6 @@ from dataclasses import dataclass, field import fnmatch -import hashlib import json import logging import os @@ -516,23 +515,21 @@ def _sync_component_mirror() -> None: def _builtin_component_cache_path() -> Path | None: - """Cache file for this build's built-in component list. + """Cache file for this target's built-in component list. The file lives inside the extracted framework directory so it is discarded together with that exact checkout (re-extract, source - override, clean-all); the target and the EXCLUDE_COMPONENTS set name it. - The sdkconfig is not part of the key: IDF components register regardless - of CONFIG_* options and only gate their sources on them. A checkout - supplied through IDF_PATH is not managed by ESPHome and is never cached. + override, clean-all). The discovery configure registers every built-in + component, so the list serves any EXCLUDE_COMPONENTS set (the project + write drops the excluded names) and any sdkconfig (IDF components + register regardless of CONFIG_* options and only gate their sources on + them). A checkout supplied through IDF_PATH is not managed by ESPHome + and is never cached. """ if not _esphome_manages_idf(): return None target = variant_to_idf_target(CORE.data[KEY_ESP32][KEY_VARIANT]) - excluded = CORE.cmake_args.get("EXCLUDE_COMPONENTS", "") - excluded_key = hashlib.sha256(excluded.encode()).hexdigest()[:12] - return ( - _get_idf_path() / ".esphome_component_lists" / f"{target}-{excluded_key}.json" - ) + return _get_idf_path() / ".esphome_component_lists" / f"{target}.json" def load_cached_builtin_components() -> list[str] | None: @@ -635,10 +632,9 @@ def has_outdated_files(): already deletes ``dependencies.lock`` on a change but that signal gets lost as soon as the lock is missing. - ``exclude_components.esphomeinternal`` -- the resolved - EXCLUDE_COMPONENTS set. Excluded components never register in - ``project_description.json``, so re-including one needs a fresh - discovery pass before it can appear in the builtin-components - property that ``src`` REQUIRES. + EXCLUDE_COMPONENTS set. The builtin-components property that + ``src`` REQUIRES drops excluded names, so re-including one needs a + reconfigure to rebuild that property from the discovery list. We deliberately don't watch: - The top-level/src ``CMakeLists.txt`` -- ESPHome owns those, and @@ -803,8 +799,7 @@ def run_compile(config, verbose: bool) -> int: Uses two-phase configure to auto-discover available components: 1. If no previous build, configure with minimal REQUIRES to discover - components (skipped when a cached list for this IDF/target/exclusion - set exists) + components (skipped when a cached list for this IDF/target exists) 2. Regenerate CMakeLists.txt with discovered components 3. Run full build """ diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index ed1202fdb9..8ac76cd41f 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -2,7 +2,7 @@ dependencies: bblanchon/arduinojson: version: "7.4.3" esphome/dlms_parser: - version: 1.1.0 + version: 2.2.0 esphome/esp-audio-libs: version: 4.0.1 esphome/esp-micro-speech-features: @@ -24,7 +24,7 @@ dependencies: espressif/esp32-camera: version: 2.1.7 espressif/mdns: - version: 1.12.0 + version: 1.14.0 espressif/esp_wifi_remote: version: 1.6.5 rules: diff --git a/esphome/writer.py b/esphome/writer.py index 0bd485bfd3..559d7d8582 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -212,14 +212,11 @@ BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h" BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp" ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h" # Headers that must not be included bare from esphome.h or the clang-tidy -# all-headers file: X-macro files, headers main.cpp includes itself, and -# deprecated headers that only resolve when their new component is loaded. +# all-headers file: X-macro files and headers main.cpp includes itself. ESPHOME_H_EXCLUDE = { Path(ENTITY_TYPES_H_TARGET), # main.cpp includes it after defining esphome_controllers() Path("esphome/core/controller_dispatch.h"), - # moved to components/ring_buffer/, removed in 2026.11.0 - Path("esphome/core/ring_buffer.h"), # build machinery, not user API Path("esphome/core/pch_prefix.h"), } diff --git a/platformio.ini b/platformio.ini index 56a12d8b31..7534b748d1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -106,7 +106,7 @@ build_unflags = ; esp32 envs (which would double-include them). [common:idf-component-libs] lib_deps = - esphome/dlms_parser@1.1.0 ; dlms_meter + esphome/dlms_parser@2.2.0 ; dlms_meter bblanchon/ArduinoJson@7.4.3 ; json lvgl/lvgl@9.5.0 ; lvgl diff --git a/requirements.txt b/requirements.txt index 8deafbad86..612bda6b1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.2.0 esptool==5.4.0 click==8.5.0 -aioesphomeapi==46.8.0 +aioesphomeapi==46.9.0 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi zeroconf==0.151.5 puremagic==2.2.0 @@ -33,7 +33,7 @@ requests==2.34.2 py7zr==1.1.3 platformdirs==4.12.3 # native esp-idf toolchain global cache dir ninja==1.13.2 # native host and esp8266 arduino toolchain build driver -filelock==4.0.10 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg +filelock==4.0.12 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg # esp-idf >= 5.0 requires this pyparsing >= 3.3.3 diff --git a/requirements_test.txt b/requirements_test.txt index c81f2f83dc..0f7d376255 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ pylint==4.1.2 flake8==7.4.1 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py ruff==0.16.10 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py -prek==0.5.4 # .github/workflows/ci.yml reads this pin +prek==0.5.5 # .github/workflows/ci.yml reads this pin yamlrocks==0.6.1 # used by script/sync_dependency_versions.py # Unit tests diff --git a/script/ci-custom.py b/script/ci-custom.py index d41d5b4302..8361387af0 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +import ast import codecs import collections from collections.abc import Iterator @@ -811,6 +812,40 @@ def lint_const_py_frozen(fname, content): return None +@lint_content_check(include=["esphome/components/*/__init__.py"]) +def lint_component_domain(fname: Path, content: str) -> str | None: + """Require every component's __init__.py to define DOMAIN as its own name.""" + if len(fname.parts) != 4: + # Platform packages such as esphome/components//sensor/__init__.py + return None + domain = fname.parts[2] + expected = f'DOMAIN = "{domain}"' + try: + tree = ast.parse(content, filename=str(fname)) + except SyntaxError: + # Reported by the Python linters; nothing useful to add here. + return None + for node in tree.body: + if isinstance(node, ast.Assign): + targets = node.targets + elif isinstance(node, ast.AnnAssign): + targets = [node.target] + else: + continue + if ( + any(isinstance(t, ast.Name) and t.id == "DOMAIN" for t in targets) + and isinstance(node.value, ast.Constant) + and node.value.value == domain + ): + return None + return ( + f"Component is missing the {highlight(expected)} constant. " + "Add it in alphabetical order with the other component metadata such as " + "CODEOWNERS and DEPENDENCIES, so other code can refer to the component by " + "name, e.g. CORE.data[DOMAIN]." + ) + + def relative_cpp_search_text(fname: Path, content) -> str: parts = fname.parts integration = parts[2] @@ -1030,15 +1065,13 @@ HEAP_ALLOCATING_HELPERS = { r"str_sprintf|" r"str_snprintf|" r"value_accuracy_to_string" - r")\s*\(" + CPP_RE_EOL, + # Explicit template arguments at any nesting depth, e.g. format_hex>( + r")\s*(?:<[^;{}()\n]*>\s*)?\(" + CPP_RE_EOL, include=cpp_include, exclude=[ # The definitions themselves "esphome/core/alloc_helpers.h", "esphome/core/alloc_helpers.cpp", - # Backward compatibility re-exports (remove before 2026.11.0) - "esphome/core/helpers.h", - "esphome/core/helpers.cpp", # Vendored third-party library "esphome/components/http_request/httplib.h", ], @@ -1091,7 +1124,6 @@ def lint_no_sprintf(fname, match): # Vendored library "esphome/components/http_request/httplib.h", # Deprecated helpers that return std::string - "esphome/core/helpers.cpp", "esphome/core/alloc_helpers.cpp", # The using declaration itself "esphome/core/helpers.h", @@ -1242,12 +1274,6 @@ def lint_no_std_nothrow(fname, match): r"\s*(?:=\s*\{?|[{(])\s*\"", prefilter="TAG", include=["esphome/components/*.cpp", "esphome/components/**/*.cpp"], - # Deprecated components, converted in a follow-up PR - exclude=[ - "esphome/components/st7735/*", - "esphome/components/st7789v/*", - "esphome/components/waveshare_epaper/*", - ], ) def lint_log_tag_macro(fname, match): return ( diff --git a/script/clang-tidy b/script/clang-tidy index ad6c99d637..a2becb59cc 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +import heapq import os from pathlib import Path import queue @@ -11,6 +12,7 @@ import subprocess import sys import tempfile import threading +import time from typing import Any import click @@ -277,10 +279,21 @@ def clang_options(idedata, environment): pids = set() +# Generated files the static analyzer is skipped on. The analyzer inlines the +# small dump helpers into every generated dump function and takes +# over 200 s on this file in CI, 99% of its clang-tidy time; any finding there +# would be fixed in script/api_protobuf anyway. The other checks still run. +ANALYZER_SKIPPED_FILES: frozenset[str] = frozenset( + {"esphome/components/api/api_pb2_dump.cpp"} +) -def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): + +def run_tidy( + executable, args, options, tmpdir, path_queue, lock, failed_files, timings +): while True: path = path_queue.get() + started = time.monotonic() invocation = [executable] if tmpdir is not None: @@ -297,16 +310,22 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): if sys.stdout.isatty(): invocation.append("--use-color") + disabled_checks = [] if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx")): # MMIO peripheral access on these bare-metal platforms is all # fixed-address. # bugprone-pointer-arithmetic-on-polymorphic-object (and its # cert-ctr56-cpp alias) crashes clang-tidy 22 with infinite matcher # recursion on lvgl_esphome.h under the RP2 defines. - invocation.append( - "--checks=-clang-analyzer-core.FixedAddressDereference," - "-bugprone-pointer-arithmetic-on-polymorphic-object,-cert-ctr56-cpp" - ) + disabled_checks += [ + "-clang-analyzer-core.FixedAddressDereference", + "-bugprone-pointer-arithmetic-on-polymorphic-object", + "-cert-ctr56-cpp", + ] + if os.path.normpath(path) in ANALYZER_SKIPPED_FILES: + disabled_checks.append("-clang-analyzer-*") + if disabled_checks: + invocation.append("--checks=" + ",".join(disabled_checks)) invocation.append(f"--header-filter={Path(basepath).resolve()}/.*") invocation.append(str(Path(path).resolve())) invocation.append("--") @@ -319,8 +338,9 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): check=False, close_fds=False, ) - if proc.returncode != 0: - with lock: + with lock: + timings.append((time.monotonic() - started, path)) + if proc.returncode != 0: print_error_for_file(path, proc.stdout) failed_files.append(path) path_queue.task_done() @@ -333,8 +353,27 @@ def progress_bar_show(value): def split_list(a, n): - k, m = divmod(len(a), n) - return [a[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n)] + """Deal the sorted files out to n splits in turn. + + Contiguous slices put whole directories on one split; expensive + neighbours such as esphome/core then land together and that split takes + several times longer than the others. Dealing spreads them out. + """ + return [a[i::n] for i in range(n)] + + +SLOWEST_FILES_SHOWN = 15 + + +def print_slowest_files(timings): + """Print the files clang-tidy spent the longest on, so the splits can be + rebalanced from CI logs.""" + if not timings: + return + total = sum(seconds for seconds, _ in timings) + print(f"\nclang-tidy spent {total:.0f} s on {len(timings)} files; slowest:") + for seconds, path in heapq.nlargest(SLOWEST_FILES_SHOWN, timings): + print(f" {seconds:6.1f} s {path}") def main(): @@ -458,6 +497,7 @@ def main(): tmpdir = tempfile.mkdtemp() failed_files = [] + timings = [] try: executable = get_binary("clang-tidy", 22) task_queue = queue.Queue(args.jobs) @@ -473,6 +513,7 @@ def main(): task_queue, lock, failed_files, + timings, ), ) t.daemon = True @@ -487,6 +528,7 @@ def main(): # Wait for all threads to be done. task_queue.join() + print_slowest_files(timings) except FileNotFoundError: return 1 diff --git a/script/helpers.py b/script/helpers.py index fb2c964bdd..11605fcef0 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1616,7 +1616,7 @@ def lpt_partition( Heaviest item first into the lightest group. Ties keep input order, so pass pre-sorted items for deterministic output. script/clang-tidy's - split_list is the unweighted contiguous sibling. + split_list is the unweighted sibling that deals files out in turn. """ groups: list[list[str]] = [[] for _ in range(count)] group_weights = [0.0] * count diff --git a/script/split_components_for_ci.py b/script/split_components_for_ci.py index cc458d5d66..663f2c6d5d 100755 --- a/script/split_components_for_ci.py +++ b/script/split_components_for_ci.py @@ -13,15 +13,20 @@ from __future__ import annotations import argparse from collections import defaultdict +from collections.abc import Set as AbstractSet +import contextlib from dataclasses import dataclass, field +import functools import json import math from pathlib import Path import sys +from typing import Any, NamedTuple # Add esphome to path sys.path.insert(0, str(Path(__file__).parent.parent)) +from esphome import yaml_util from script.analyze_component_buses import ( ISOLATED_COMPONENTS, ISOLATED_SIGNATURE_PREFIX, @@ -31,29 +36,52 @@ from script.analyze_component_buses import ( merge_compatible_bus_groups, ) from script.helpers import ( + _extract_components_from_yaml, get_component_test_files, parse_test_filename, split_conflicting_groups, ) -# Estimated CI seconds for one build per test platform, read from batch job -# logs. Absolute accuracy does not matter, only the ratios between platforms. -# A full run takes over an hour, so recording real timings is not practical. +# Estimated CI seconds for one build per test platform, fitted to the batch +# job logs of a full run (every component, PR #20389). Absolute accuracy does +# not matter, only the ratios between platforms. A full run takes over an +# hour, so recording real timings is not practical. PLATFORM_BUILD_SECONDS = { "host": 10, "esp8266-ard": 30, - "esp32-idf": 45, - "rtl87xx-ard": 35, - "ln882x-ard": 50, - "rp2040-ard": 50, - "rp2350-ard": 50, - "bk72xx-ard": 60, + "rtl87xx-ard": 40, + "esp32-c3-idf": 40, + "esp32-s2-idf": 40, + "rp2350-ard": 40, + "rp2040-ard": 45, + "esp32-idf": 55, + "esp32-p4-idf": 55, + "ln882x-ard": 55, + "esp32-s3-idf": 60, + "bk72xx-ard": 70, + "esp32-c6-idf": 70, } -NRF52_BUILD_SECONDS = 45 -# Other ESP32 chips and Arduino on ESP32 share less of the ccache -DEFAULT_BUILD_SECONDS = 90 +NRF52_BUILD_SECONDS = 50 +# Arduino on any ESP32 chip builds the IDF plus the Arduino core on top +ESP32_ARDUINO_BUILD_SECONDS = 90 +# The remaining ESP32 chips on ESP-IDF +DEFAULT_BUILD_SECONDS = 50 # Each extra component merged into a grouped build makes it larger -GROUPED_COMPONENT_SECONDS = 5 +GROUPED_COMPONENT_SECONDS = 3 +# Components that pull in a large library the build compiles from source +# (TensorFlow Lite Micro, FastLED, LVGL, the HTTP server), added to every +# build whose test config enables them. Fitted the same way as the platforms. +HEAVY_COMPONENT_SECONDS: dict[str, int] = { + "micro_wake_word": 200, + "fastled_clockless": 90, + "fastled_spi": 90, + "wled": 60, + "lvgl": 50, + "web_server": 50, + "online_image": 45, + "prometheus": 35, + "bluetooth_proxy": 30, +} # Estimated build seconds per CI runner; sets how many runners are used. # Approximate: the runner count charges each grouped build once, but a large # group spreads over several runners, which each pay for that build. @@ -90,9 +118,62 @@ def build_seconds(platform: str) -> int: return seconds if platform.startswith("nrf52"): return NRF52_BUILD_SECONDS + if platform.startswith("esp32") and platform.endswith("-ard"): + return ESP32_ARDUINO_BUILD_SECONDS return DEFAULT_BUILD_SECONDS +def _config_components(data: Any) -> set[str]: + """Return the components a loaded test config and its ``packages:`` use.""" + if not isinstance(data, dict): + return set() + found = _extract_components_from_yaml(data) + if isinstance(packages := data.get("packages"), dict): + for package in packages.values(): + if isinstance(package, yaml_util.IncludeFile): + found |= _file_components( + (package.parent_file.parent / package.file).resolve() + ) + else: + found |= _config_components(package) + return found + + +@functools.cache +def _file_components(path: Path) -> frozenset[str]: + """Return the components a config file uses; shared packages load once.""" + return frozenset( + _config_components(yaml_util.load_yaml(path, track_document_range=False)) + ) + + +def heavy_components(component: str, test_file: Path) -> frozenset[str]: + """Return the heavy components a test compiles. + + A test counts as heavy when it belongs to a heavy component or when its + config enables one, such as a display test that draws with lvgl. + """ + heavy = {component} & HEAVY_COMPONENT_SECONDS.keys() + # A config that does not load is left to the build to report + with contextlib.suppress(Exception): + heavy |= _file_components(test_file.resolve()) & HEAVY_COMPONENT_SECONDS.keys() + return frozenset(heavy) + + +def heavy_seconds(heavy: AbstractSet[str]) -> int: + """Return the library seconds a set of heavy components adds to a build.""" + return sum(HEAVY_COMPONENT_SECONDS[name] for name in heavy) + + +class _GroupedShare(NamedTuple): + """A component's part in one (signature, platform) shared build.""" + + # Seconds of its files on that platform when no other member shares it + solo: int + # Heavy components its base test brings into the shared build + heavy: frozenset[str] + + @dataclass class _BatchItem: """A component and the builds it adds to the batch it lands in.""" @@ -101,40 +182,52 @@ class _BatchItem: # Builds that always run on their own (isolated tests, variants on a # platform without a base test) own_seconds: int - # Per (signature, platform) shared build: the seconds of this component's - # files on that platform when no other batch member shares the build - grouped_builds: dict[tuple[str, str], int] = field(default_factory=dict) + grouped_builds: dict[tuple[str, str], _GroupedShare] = field(default_factory=dict) def standalone_seconds(self) -> int: - return self.own_seconds + sum(self.grouped_builds.values()) + return self.own_seconds + sum( + share.solo for share in self.grouped_builds.values() + ) -def _grouped_build_seconds(platform: str, solo_seconds: list[int]) -> int: +def _grouped_build_seconds(platform: str, members: list[_GroupedShare]) -> int: """Return the seconds of one (signature, platform) build in a batch. + A library two members both enable compiles once, so the union is charged. test_build_components only groups when two or more members share the build; a grouped member then skips its variants on that platform. """ - if len(solo_seconds) <= 1: - return sum(solo_seconds) - return build_seconds(platform) + GROUPED_COMPONENT_SECONDS * (len(solo_seconds) - 1) + if len(members) <= 1: + return sum(share.solo for share in members) + return ( + build_seconds(platform) + + GROUPED_COMPONENT_SECONDS * (len(members) - 1) + + heavy_seconds(set().union(*(share.heavy for share in members))) + ) def _make_item( tests_dir: Path, component: str, signature: str, is_isolated: bool ) -> _BatchItem: - files_by_platform: dict[str, list[str]] = defaultdict(list) + # Per platform: the seconds of its test files, and the base test's heavy + # components; a platform without a base test cannot join a shared build + seconds_by_platform: dict[str, int] = defaultdict(int) + base_heavy: dict[str, frozenset[str]] = {} for test_file in (tests_dir / component).glob("test[.-]*.yaml"): test_name, platform = parse_test_filename(test_file) - files_by_platform[platform].append(test_name) + heavy = heavy_components(component, test_file) + seconds_by_platform[platform] += build_seconds(platform) + heavy_seconds(heavy) + if test_name == "test": + base_heavy[platform] = heavy own_seconds = 0 - grouped: dict[tuple[str, str], int] = {} - for platform, test_names in files_by_platform.items(): - seconds = build_seconds(platform) * len(test_names) - if is_isolated or "test" not in test_names: + grouped: dict[tuple[str, str], _GroupedShare] = {} + for platform, seconds in seconds_by_platform.items(): + if is_isolated or platform not in base_heavy: own_seconds += seconds else: - grouped[(signature, platform)] = seconds + grouped[(signature, platform)] = _GroupedShare( + seconds, base_heavy[platform] + ) return _BatchItem(component, own_seconds, grouped) @@ -144,22 +237,24 @@ class _Batch: def __init__(self) -> None: self.components: list[str] = [] self.seconds = 0 - self.grouped_builds: dict[tuple[str, str], list[int]] = defaultdict(list) + self.grouped_builds: dict[tuple[str, str], list[_GroupedShare]] = defaultdict( + list + ) def added_seconds(self, item: _BatchItem) -> int: """Return the seconds item would add; joining an existing build is cheap.""" added = item.own_seconds - for (signature, platform), solo in item.grouped_builds.items(): + for (signature, platform), share in item.grouped_builds.items(): members = self.grouped_builds.get((signature, platform), []) added += _grouped_build_seconds( - platform, [*members, solo] + platform, [*members, share] ) - _grouped_build_seconds(platform, members) return added def add(self, item: _BatchItem) -> None: self.seconds += self.added_seconds(item) - for build, solo in item.grouped_builds.items(): - self.grouped_builds[build].append(solo) + for build, share in item.grouped_builds.items(): + self.grouped_builds[build].append(share) self.components.append(item.component) @@ -167,25 +262,36 @@ def balance_batches(items: list[_BatchItem], target_seconds: int) -> list[list[s """Spread items over enough runners to stay near target_seconds each. The runner count comes from the total estimate with every grouped build - counted once. Heaviest items go first, each to the runner that ends up - lightest, so grouped components follow the builds they can join. + counted once. Heaviest items go first, each to the runner it adds the + fewest seconds to among those ending near the lightest, so grouped + components follow the builds they can join. """ if not items: return [] - grouped_builds: dict[tuple[str, str], list[int]] = defaultdict(list) + # One batch holding every item counts each grouped build once + whole = _Batch() for item in items: - for build, solo in item.grouped_builds.items(): - grouped_builds[build].append(solo) - total = sum(item.own_seconds for item in items) + sum( - _grouped_build_seconds(platform, solos) - for (_, platform), solos in grouped_builds.items() - ) + whole.add(item) + total = whole.seconds count = min(len(items), max(1, math.ceil(total / target_seconds))) batches = [_Batch() for _ in range(count)] + # Among the runners ending within slack of the lightest, the one the item + # adds the fewest seconds to wins, so a component lands with the bus + # group partners it shares builds with; placed away from them it builds + # alone on every platform they only share there. + slack = target_seconds // 4 for item in sorted(items, key=lambda i: (-i.standalone_seconds(), i.component)): - min(batches, key=lambda b: (b.seconds + b.added_seconds(item), b.seconds)).add( - item + costs = [(batch, batch.added_seconds(item)) for batch in batches] + ceiling = min(batch.seconds + added for batch, added in costs) + slack + batch, _ = min( + ( + (batch, added) + for batch, added in costs + if batch.seconds + added <= ceiling + ), + key=lambda cost: (cost[1], cost[0].seconds + cost[1]), ) + batch.add(item) return [batch.components for batch in batches if batch.components] diff --git a/tests/benchmarks/components/api/bench_list_entities.cpp b/tests/benchmarks/components/api/bench_list_entities.cpp index 4c445c2bb6..8a4cac1e2e 100644 --- a/tests/benchmarks/components/api/bench_list_entities.cpp +++ b/tests/benchmarks/components/api/bench_list_entities.cpp @@ -3,6 +3,8 @@ #include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_buffer.h" #include "esphome/components/light/color_mode.h" +#include "esphome/components/light/light_effect.h" +#include "esphome/components/light/light_effect_names.h" namespace esphome::api::benchmarks { @@ -150,7 +152,17 @@ BENCHMARK(CalcAndEncode_ListEntitiesBinarySensorResponse); // --- ListEntitiesLightResponse --- static light::ColorModeMask light_color_modes; -static FixedVector light_effects; + +class BenchEffect : public light::LightEffect { + public: + explicit BenchEffect(const char *name) : LightEffect(name) {} + void apply() override {} +}; +static BenchEffect rainbow_effect("Rainbow"); +static BenchEffect strobe_effect("Strobe"); +static light::LightEffect *const LIGHT_EFFECT_TABLE[] = {&rainbow_effect, &strobe_effect}; +static const ConstVector light_effect_list(LIGHT_EFFECT_TABLE, 2); +static const light::LightEffectNames light_effects(light_effect_list); static ListEntitiesLightResponse make_light_response() { // Initialize static data on first call @@ -158,10 +170,6 @@ static ListEntitiesLightResponse make_light_response() { if (!initialized) { light_color_modes.insert(light::ColorMode::RGB_WHITE); light_color_modes.insert(light::ColorMode::COLOR_TEMPERATURE); - light_effects.init(3); - light_effects.push_back("None"); - light_effects.push_back("Rainbow"); - light_effects.push_back("Strobe"); initialized = true; } diff --git a/tests/benchmarks/python/fixtures/bluetooth_proxy_device.yaml b/tests/benchmarks/python/fixtures/bluetooth_proxy_device.yaml index dfa5a487b8..c74846689b 100644 --- a/tests/benchmarks/python/fixtures/bluetooth_proxy_device.yaml +++ b/tests/benchmarks/python/fixtures/bluetooth_proxy_device.yaml @@ -25,7 +25,9 @@ ethernet: type: LAN8720 mdc_pin: GPIO23 mdio_pin: GPIO18 - clk_mode: GPIO17_OUT + clk: + mode: CLK_OUT + pin: GPIO17 phy_addr: 0 power_pin: GPIO12 diff --git a/tests/component_tests/automation/__init__.py b/tests/component_tests/automation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/automation/config/condition_groups.yaml b/tests/component_tests/automation/config/condition_groups.yaml new file mode 100644 index 0000000000..9624531862 --- /dev/null +++ b/tests/component_tests/automation/config/condition_groups.yaml @@ -0,0 +1,44 @@ +--- +esphome: + name: test + +esp32: + board: nodemcu-32s + +logger: + +binary_sensor: + - platform: template + id: single_and + on_press: + - if: + condition: + - lambda: return true; + then: + - logger.log: single and + - platform: template + id: single_or + on_press: + - if: + any: + - lambda: return true; + then: + - logger.log: single or + - platform: template + id: double_and + on_press: + - if: + condition: + - lambda: return true; + - lambda: return false; + then: + - logger.log: double and + - platform: template + id: single_xor + on_press: + - if: + condition: + xor: + - lambda: return true; + then: + - logger.log: single xor diff --git a/tests/component_tests/automation/test_condition_groups.py b/tests/component_tests/automation/test_condition_groups.py new file mode 100644 index 0000000000..effe7b5b0b --- /dev/null +++ b/tests/component_tests/automation/test_condition_groups.py @@ -0,0 +1,25 @@ +"""Tests for and/or condition group codegen.""" + +from collections.abc import Callable +from pathlib import Path +import re + + +def test_single_condition_groups_are_unwrapped( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """A group of one condition is passed to the action directly.""" + main_cpp = generate_main(component_config_path("condition_groups.yaml")) + + assert "IfAction(lambdacondition_id);" in main_cpp + assert "IfAction(lambdacondition_id_2);" in main_cpp + group = re.search( + r"new\((\w+)\) AndCondition<2>\(\{lambdacondition_id_3, lambdacondition_id_4\}\);", + main_cpp, + ) + assert group is not None + assert f"IfAction({group.group(1)});" in main_cpp + # xor of one condition equals that condition, so it is unwrapped too. + assert "IfAction(lambdacondition_id_5);" in main_cpp + assert "XorCondition<" not in main_cpp diff --git a/tests/component_tests/dlms_meter/__init__.py b/tests/component_tests/dlms_meter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/dlms_meter/test_provider.py b/tests/component_tests/dlms_meter/test_provider.py new file mode 100644 index 0000000000..9ce07f118a --- /dev/null +++ b/tests/component_tests/dlms_meter/test_provider.py @@ -0,0 +1,17 @@ +"""Tests for the dlms_meter provider option removed in 2026.11.0.""" + +import pytest +from voluptuous import Invalid, MultipleInvalid + +from esphome.components.dlms_meter import CONFIG_SCHEMA + + +def test_removed_provider_shows_the_netznoe_patterns() -> None: + with pytest.raises(Invalid) as exc_info: + CONFIG_SCHEMA({"provider": "netznoe"}) + errors = ( + exc_info.value.errors + if isinstance(exc_info.value, MultipleInvalid) + else [exc_info.value] + ) + assert any('pattern: "L, TSTR"' in str(error) for error in errors) diff --git a/tests/component_tests/dlms_meter/test_removed_keys.py b/tests/component_tests/dlms_meter/test_removed_keys.py new file mode 100644 index 0000000000..f82ec79bff --- /dev/null +++ b/tests/component_tests/dlms_meter/test_removed_keys.py @@ -0,0 +1,33 @@ +"""Tests for the dlms_meter predefined keys removed in 2026.11.0.""" + +from collections.abc import Callable + +import pytest +from voluptuous import Invalid, MultipleInvalid + +from esphome.components.dlms_meter.sensor import CONFIG_SCHEMA as SENSOR_SCHEMA +from esphome.components.dlms_meter.text_sensor import ( + CONFIG_SCHEMA as TEXT_SENSOR_SCHEMA, +) +from esphome.types import ConfigType + + +def _errors(schema: Callable[[ConfigType], ConfigType], config: ConfigType) -> str: + with pytest.raises(Invalid) as exc_info: + schema(config) + errors = ( + exc_info.value.errors + if isinstance(exc_info.value, MultipleInvalid) + else [exc_info.value] + ) + return "\n".join(str(error) for error in errors) + + +def test_removed_sensor_key_names_its_replacement() -> None: + errors = _errors(SENSOR_SCHEMA, {"voltage_l1": {"name": "Voltage"}}) + assert 'obis_code: "1.0.32.7.0.255" and unit_of_measurement: V' in errors + + +def test_removed_text_sensor_key_names_its_replacement() -> None: + errors = _errors(TEXT_SENSOR_SCHEMA, {"timestamp": {"name": "Timestamp"}}) + assert 'obis_code: "0.0.1.0.0.255"' in errors diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 04416d7f57..4b7a0a6ab8 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -10,6 +10,7 @@ from typing import Any import pytest +from esphome.components.const import CONF_HOLD_STATE from esphome.components.esp32 import ( _ESP_TLS_LINKING_COMPONENTS, DEFAULT_EXCLUDED_IDF_COMPONENTS, @@ -38,13 +39,30 @@ from esphome.components.esp32.const import ( KEY_NETWORK_SDKCONFIG, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + VARIANT_ESP32H4, + VARIANT_ESP32H21, + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32S31, ) -from esphome.components.esp32.gpio import validate_gpio_pin +from esphome.components.esp32.gpio import validate_gpio_pin, validate_supports import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, CONF_IGNORE_PIN_VALIDATION_ERROR, + CONF_INPUT, + CONF_MODE, CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, PlatformFramework, Toolchain, ) @@ -57,7 +75,7 @@ def test_esp32_config( ) -> None: set_core_config(PlatformFramework.ESP32_IDF) - from esphome.components.esp32 import CONFIG_SCHEMA, VARIANT_ESP32, VARIANT_FRIENDLY + from esphome.components.esp32 import CONFIG_SCHEMA, VARIANT_FRIENDLY # Example ESP32 configuration config = { @@ -1722,9 +1740,8 @@ def test_esp32_s31_gpio_validation( caplog: pytest.LogCaptureFixture, ) -> None: """S31: GPIO26-28/30-32 are reserved for the SPI flash interface, GPIO29 - and GPIO41 do not exist, GPIO33 is a normal pin, and GPIO36 is a + and GPIO41 do not exist, GPIO35 is a normal pin, and GPIO36 is a strapping pin.""" - from esphome.components.esp32.const import VARIANT_ESP32S31 from esphome.components.esp32.gpio import validate_supports from esphome.const import CONF_INPUT, CONF_MODE, CONF_OPEN_DRAIN, CONF_OUTPUT @@ -1735,8 +1752,8 @@ def test_esp32_s31_gpio_validation( input_mode = {CONF_INPUT: True, CONF_OUTPUT: False, CONF_OPEN_DRAIN: False} # Not reserved; a normal GPIO - pin = {CONF_NUMBER: 33, CONF_IGNORE_PIN_VALIDATION_ERROR: False} - assert validate_gpio_pin(pin)[CONF_NUMBER] == 33 + pin = {CONF_NUMBER: 35, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + assert validate_gpio_pin(pin)[CONF_NUMBER] == 35 # Reserved for the SPI flash interface, but can be bypassed with # ignore_pin_validation_error @@ -1764,6 +1781,113 @@ def test_esp32_s31_gpio_validation( assert "GPIO36 is a strapping pin" in caplog.text +_INPUT_ONLY_SETTINGS = ( + (CONF_OUTPUT, "does not support output pin mode"), + (CONF_PULLUP, "does not support pullups"), + (CONF_PULLDOWN, "does not support pulldowns"), + (CONF_HOLD_STATE, "is input-only and cannot be held"), +) + + +@pytest.mark.parametrize( + ("variant", "number", "setting", "error"), + [ + pytest.param( + variant, number, setting, error, id=f"{name}-gpio{number}-{setting}" + ) + for variant, name, numbers in ( + (VARIANT_ESP32, "esp32", range(34, 40)), + (VARIANT_ESP32S2, "s2", (46,)), + ) + for number in numbers + for setting, error in _INPUT_ONLY_SETTINGS + ] + + [ + pytest.param( + VARIANT_ESP32, + 20, + CONF_HOLD_STATE, + "GPIO20 has no hold function", + id="esp32-gpio20-hold_state", + ) + ], +) +def test_input_only_gpio_rejects_unsupported_modes( + set_core_config: SetCoreConfigCallable, + variant: str, + number: int, + setting: str, + error: str, +) -> None: + set_core_config(PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: variant}) + mode = { + CONF_INPUT: True, + CONF_OUTPUT: False, + CONF_OPEN_DRAIN: False, + CONF_PULLUP: False, + CONF_PULLDOWN: False, + } + pin = {CONF_NUMBER: number, CONF_MODE: mode} + if setting == CONF_HOLD_STATE: + pin[setting] = True + else: + mode[setting] = True + + with pytest.raises(cv.Invalid, match=error): + validate_supports(pin) + + +@pytest.mark.parametrize( + ("variant", "number"), + [ + pytest.param(VARIANT_ESP32C3, 18, id="c3-18"), + pytest.param(VARIANT_ESP32C3, 19, id="c3-19"), + pytest.param(VARIANT_ESP32C5, 13, id="c5-13"), + pytest.param(VARIANT_ESP32C5, 14, id="c5-14"), + pytest.param(VARIANT_ESP32C6, 12, id="c6-12"), + pytest.param(VARIANT_ESP32C6, 13, id="c6-13"), + pytest.param(VARIANT_ESP32C61, 12, id="c61-12"), + pytest.param(VARIANT_ESP32C61, 13, id="c61-13"), + pytest.param(VARIANT_ESP32H2, 26, id="h2-26"), + pytest.param(VARIANT_ESP32H2, 27, id="h2-27"), + pytest.param(VARIANT_ESP32H4, 13, id="h4-13"), + pytest.param(VARIANT_ESP32H4, 14, id="h4-14"), + pytest.param(VARIANT_ESP32H21, 17, id="h21-17"), + pytest.param(VARIANT_ESP32H21, 18, id="h21-18"), + pytest.param(VARIANT_ESP32P4, 24, id="p4-24"), + pytest.param(VARIANT_ESP32P4, 25, id="p4-25"), + pytest.param(VARIANT_ESP32S3, 19, id="s3-19"), + pytest.param(VARIANT_ESP32S3, 20, id="s3-20"), + pytest.param(VARIANT_ESP32S31, 33, id="s31-33"), + pytest.param(VARIANT_ESP32S31, 34, id="s31-34"), + ], +) +def test_usb_jtag_gpio_hold_state_warns( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, + variant: str, + number: int, +) -> None: + set_core_config(PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: variant}) + pin = { + CONF_NUMBER: number, + CONF_MODE: { + CONF_INPUT: True, + CONF_OUTPUT: False, + CONF_OPEN_DRAIN: False, + }, + CONF_HOLD_STATE: True, + } + + with caplog.at_level(logging.WARNING): + validate_supports(pin) + + assert ( + f"GPIO{number} cannot hold at low level during wakeup from deep sleep." + in caplog.text + ) + + _TLS_SERVER_OPTIONS = ( "CONFIG_MBEDTLS_TLS_CLIENT_ONLY", "CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", diff --git a/tests/component_tests/esp8266/__init__.py b/tests/component_tests/esp8266/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/esp8266/config/minimal.yaml b/tests/component_tests/esp8266/config/minimal.yaml new file mode 100644 index 0000000000..79fde0157b --- /dev/null +++ b/tests/component_tests/esp8266/config/minimal.yaml @@ -0,0 +1,5 @@ +esphome: + name: test + +esp8266: + board: d1_mini diff --git a/tests/component_tests/esp8266/test_build_flags.py b/tests/component_tests/esp8266/test_build_flags.py new file mode 100644 index 0000000000..938089333b --- /dev/null +++ b/tests/component_tests/esp8266/test_build_flags.py @@ -0,0 +1,17 @@ +"""Tests for ESP8266 build flags.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +from esphome.core import CORE + + +def test_esp8266_keeps_switches_out_of_ram( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("minimal.yaml")) + + assert "-fno-tree-switch-conversion" in CORE.build_flags diff --git a/tests/component_tests/ethernet/test_ethernet.py b/tests/component_tests/ethernet/test_ethernet.py index 522357f972..73985597d9 100644 --- a/tests/component_tests/ethernet/test_ethernet.py +++ b/tests/component_tests/ethernet/test_ethernet.py @@ -8,6 +8,7 @@ from esphome.components.esp32 import ( KEY_BOARD, KEY_IDF_VERSION, KEY_VARIANT, + VARIANT_ESP32, VARIANT_ESP32S3, ) from esphome.components.ethernet import CONF_CLOCK_SPEED, CONFIG_SCHEMA, _final_validate @@ -82,3 +83,40 @@ def test_ch390_rejects_clock_speed_above_the_datasheet_maximum( CORE.name = "ch390-test" with pytest.raises(Invalid, match="value must be at most 72000000"): CONFIG_SCHEMA({**_CH390_CONFIG, CONF_CLOCK_SPEED: "80MHz"}) + + +_LAN8720_CONFIG = {"type": "LAN8720", "mdc_pin": 23, "mdio_pin": 18} + + +def _set_esp32_core(set_core_config: SetCoreConfigCallable) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={ + KEY_BOARD: "esp32dev", + KEY_VARIANT: VARIANT_ESP32, + KEY_IDF_VERSION: cv.Version(5, 3, 2), + }, + ) + CORE.name = "rmii-test" + + +def test_rmii_rejects_removed_clk_mode( + set_core_config: SetCoreConfigCallable, +) -> None: + """The removed clk_mode option points at the clk: replacement.""" + _set_esp32_core(set_core_config) + with pytest.raises(Invalid, match="GPIO17_OUT -> mode: CLK_OUT, pin: GPIO17"): + CONFIG_SCHEMA( + { + **_LAN8720_CONFIG, + "clk_mode": "GPIO17_OUT", + "clk": {"mode": "CLK_OUT", "pin": 17}, + } + ) + + +def test_rmii_requires_clk(set_core_config: SetCoreConfigCallable) -> None: + """An RMII PHY has no default clock, so clk must be given.""" + _set_esp32_core(set_core_config) + with pytest.raises(Invalid, match=r"required key not provided @ data\['clk'\]"): + CONFIG_SCHEMA(_LAN8720_CONFIG) diff --git a/tests/component_tests/gpio/test_output_final_validate.py b/tests/component_tests/gpio/test_output_final_validate.py new file mode 100644 index 0000000000..5d92ad44c6 --- /dev/null +++ b/tests/component_tests/gpio/test_output_final_validate.py @@ -0,0 +1,38 @@ +"""Tests for GPIO output final validation.""" + +import pytest + +from esphome import config_validation as cv +from esphome.components.const import CONF_HOLD_STATE +from esphome.components.gpio.output import FINAL_VALIDATE_SCHEMA +from esphome.config import Config +from esphome.const import CONF_ID, CONF_PIN, CONF_POWER_SUPPLY, PlatformFramework +from esphome.core import ID +from tests.component_tests.types import SetCoreConfigCallable + + +@pytest.mark.parametrize("power_supply_holds", [False, True]) +def test_output_hold_state_requires_power_supply_hold_state( + set_core_config: SetCoreConfigCallable, + power_supply_holds: bool, +) -> None: + supply_id = ID("supply", is_declaration=True, type="power_supply") + full_config = Config() + full_config["power_supply"] = [ + {CONF_ID: supply_id, CONF_PIN: {CONF_HOLD_STATE: power_supply_holds}} + ] + full_config.declare_ids.append((supply_id, ["power_supply", 0, CONF_ID])) + set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config) + + config = { + CONF_PIN: {CONF_HOLD_STATE: True}, + CONF_POWER_SUPPLY: ID("supply", is_declaration=False, type="power_supply"), + } + if power_supply_holds: + FINAL_VALIDATE_SCHEMA(config) + else: + with pytest.raises( + cv.Invalid, + match="The power supply pin must also set hold_state when an output it powers sets it", + ): + FINAL_VALIDATE_SCHEMA(config) diff --git a/tests/component_tests/improv_ble/test_improv_ble.py b/tests/component_tests/improv_ble/test_improv_ble.py index 02293bdb23..d7df3869dc 100644 --- a/tests/component_tests/improv_ble/test_improv_ble.py +++ b/tests/component_tests/improv_ble/test_improv_ble.py @@ -42,18 +42,20 @@ def test_platform_without_ble_server_rejected( assert "only available on" in capsys.readouterr().out -def test_automations_emit_renamed_triggers( +def test_automations_register_state_callbacks( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], ) -> None: main_cpp = generate_main(component_config_path("automations.yaml")) - for trigger in ( - "ImprovBLEProvisionedTrigger", - "ImprovBLEProvisioningTrigger", - "ImprovBLEStartTrigger", - "ImprovBLEStateTrigger", - "ImprovBLEStoppedTrigger", + assert "Trigger" not in main_cpp + for state in ( + "improv::STATE_PROVISIONED", + "improv::STATE_PROVISIONING", + "improv::STATE_AUTHORIZED", + "improv::STATE_STOPPED", ): - assert f"improv_ble::{trigger}" in main_cpp + assert f"state == {state}" in main_cpp + assert main_cpp.count("add_on_state_callback(") == 5 + assert "->is_failed()" in main_cpp assert "set_authorizer" in main_cpp assert "set_status_indicator" in main_cpp diff --git a/tests/component_tests/light/test_default_setters.py b/tests/component_tests/light/test_default_setters.py index a4fc24a7cb..cbbecb632f 100644 --- a/tests/component_tests/light/test_default_setters.py +++ b/tests/component_tests/light/test_default_setters.py @@ -2,6 +2,14 @@ from collections.abc import Callable from pathlib import Path +import re + +import pytest + +import esphome.codegen as cg +from esphome.components import light +from esphome.components.light import types as light_types +from esphome.core import ID, EsphomeError def test_default_flash_length_and_empty_effects_are_not_emitted( @@ -16,4 +24,20 @@ def test_default_flash_length_and_empty_effects_are_not_emitted( assert "bare_light->set_flash_transition_length(" not in main_cpp assert "bare_light->add_effects(" not in main_cpp assert "fancy_light->set_flash_transition_length(500);" in main_cpp - assert "fancy_light->add_effects({" in main_cpp + call = re.search(r"fancy_light->add_effects\((\w+), (\d+)\);", main_cpp) + assert call is not None + # The effect pointers form a flash table; they are address constants, not constexpr + assert re.search( + rf"static light::LightEffect \* const {call.group(1)}\[\] PROGMEM = \{{[^}}]+\}};", + main_cpp, + ) + + +def test_effect_assigned_in_setup_is_rejected() -> None: + """A pointer assigned in setup() would make the flash table need dynamic init.""" + dynamic_effect = cg.Pvariable( + ID("dynamic_effect", is_declaration=True, type=light_types.LightEffect), + cg.RawExpression("make_effect()"), + ) + with pytest.raises(EsphomeError, match="dynamic_effect"): + light._effects_table([dynamic_effect]) diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index ac385cc2d8..0b15be3e3a 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -10,9 +10,11 @@ import pytest from esphome import config_validation as cv from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4 -# Importing xl9535 registers its pin schema with pins.PIN_SCHEMA_REGISTRY so that -# models (e.g. SEEED-RETERMINAL-D1001) that reference xl9535-backed pins in their -# defaults can be validated by the mipi_dsi CONFIG_SCHEMA in this test. +# Importing the I/O expanders registers their pin schemas with +# pins.PIN_SCHEMA_REGISTRY so that models whose defaults reference expander-backed +# pins (e.g. SEEED-RETERMINAL-D1001 uses xl9535, the M5Stack Tab5 models use +# pi4ioe5v6408) can be validated by the mipi_dsi CONFIG_SCHEMA in this test. +import esphome.components.pi4ioe5v6408 # noqa: F401 import esphome.components.xl9535 # noqa: F401 from esphome.const import ( CONF_DIMENSIONS, diff --git a/tests/component_tests/remote_transmitter/config/ir_codes.yaml b/tests/component_tests/remote_transmitter/config/ir_codes.yaml new file mode 100644 index 0000000000..69cf1fb039 --- /dev/null +++ b/tests/component_tests/remote_transmitter/config/ir_codes.yaml @@ -0,0 +1,43 @@ +esphome: + name: test + +esp32: + board: esp32dev + +remote_transmitter: + pin: GPIO4 + carrier_duty_percent: 50% + +button: + - platform: template + name: Midea A + on_press: + - remote_transmitter.transmit_midea: + code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] + - platform: template + name: Midea B + on_press: + - remote_transmitter.transmit_midea: + code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] + - platform: template + name: AEHA + on_press: + - remote_transmitter.transmit_aeha: + address: 0x8008 + data: [0x00, 0x02, 0xFD, 0xFF, 0x00, 0x33] + - platform: template + name: Haier lambda + on_press: + - remote_transmitter.transmit_haier: + code: !lambda |- + return {0xA6, 0xDA, 0x00, 0x00}; + - platform: template + name: Haier static + on_press: + - remote_transmitter.transmit_haier: + code: [0xA6, 0xDA, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x05] + - platform: template + name: Mirage static + on_press: + - remote_transmitter.transmit_mirage: + code: [0x56, 0x6C, 0x00, 0x00, 0x20, 0x1A, 0x00, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x00, 0x42] diff --git a/tests/component_tests/remote_transmitter/test_ir_codes.py b/tests/component_tests/remote_transmitter/test_ir_codes.py new file mode 100644 index 0000000000..9be34941b6 --- /dev/null +++ b/tests/component_tests/remote_transmitter/test_ir_codes.py @@ -0,0 +1,24 @@ +"""Constant IR byte codes live in shared PROGMEM tables; lambdas stay lambdas.""" + +from collections.abc import Callable +from pathlib import Path +import re + + +def test_ir_codes_use_shared_progmem_tables( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("ir_codes.yaml")) + + tables = re.findall( + r"static constexpr uint8_t (remote_base_code\w*)\[\] PROGMEM", main_cpp + ) + assert len(tables) == 4 # Midea (shared by two actions), AEHA, Haier, Mirage + midea = tables[0] + assert main_cpp.count(f"->set_code_static({midea}, 5);") == 2 + assert re.search(r"->set_data_static\(remote_base_code\w*, 6\);", main_cpp) + assert re.search(r"->set_code_static\(remote_base_code\w*, 13\);", main_cpp) + assert re.search(r"->set_code_static\(remote_base_code\w*, 14\);", main_cpp) + assert "->set_code_template([](" in main_cpp + assert "std::vector{" not in main_cpp diff --git a/tests/component_tests/rp2040_ble/test_connection_slots.py b/tests/component_tests/rp2040_ble/test_connection_slots.py index f33180e2d0..84f849760f 100644 --- a/tests/component_tests/rp2040_ble/test_connection_slots.py +++ b/tests/component_tests/rp2040_ble/test_connection_slots.py @@ -20,7 +20,7 @@ def test_proxy_claims_its_slots_through_the_shared_accounting( # A default (3-slot) proxy build records one claim per slot, attributed # to the consumer, and passes final validation. generate_main(component_config_path("rp2_proxy_default.yaml")) - used = CORE.data[rp2040_ble.KEY_RP2040_BLE][rp2040_ble.KEY_USED_CONNECTION_SLOTS] + used = CORE.data[rp2040_ble.DOMAIN][rp2040_ble.KEY_USED_CONNECTION_SLOTS] assert used == ["bluetooth_proxy"] * 3 diff --git a/tests/component_tests/sendspin/test_unpaired_access.py b/tests/component_tests/sendspin/test_unpaired_access.py index 80bf5a2417..8660ba9f7c 100644 --- a/tests/component_tests/sendspin/test_unpaired_access.py +++ b/tests/component_tests/sendspin/test_unpaired_access.py @@ -1,24 +1,42 @@ -"""Validation tests for the hub's unpaired access policy.""" +"""Validation tests for the unpaired access policy. + +The hub's `unpaired_access` key and an `unpaired_access` switch both set the +policy, so the switch platform rejects a config that has both. +""" import logging import pytest +from esphome import config_validation as cv from esphome.components.sendspin import ( CONF_UNPAIRED_ACCESS, CONFIG_SCHEMA as HUB_CONFIG_SCHEMA, + DOMAIN, FINAL_VALIDATE_SCHEMA as HUB_FINAL_VALIDATE_SCHEMA, + _get_data, request_pairing_code_display_support, ) +from esphome.components.sendspin.switch import ( + CONFIG_SCHEMA as SWITCH_CONFIG_SCHEMA, + FINAL_VALIDATE_SCHEMA as SWITCH_FINAL_VALIDATE_SCHEMA, +) from esphome.const import PlatformFramework +from esphome.types import ConfigType from tests.component_tests.types import SetCoreConfigCallable HUB_ID = "sendspin_hub_id" -NO_PAIRING_WARNING = "nothing lets a server pair" +NO_PAIRING_WARNING = "no new server can pair with this device" + + +def _switch_config(switch_type: str) -> ConfigType: + return SWITCH_CONFIG_SCHEMA( + {"name": "Sendspin Switch", "type": switch_type, "sendspin_id": HUB_ID} + ) def test_hub_key_absent_by_default(set_core_config: SetCoreConfigCallable) -> None: - """Codegen leaves the hub's built-in default of on in place when the key is not set.""" + """With no default in the schema, the switch can tell whether the key was set.""" set_core_config(PlatformFramework.ESP32_IDF) assert CONF_UNPAIRED_ACCESS not in HUB_CONFIG_SCHEMA({"id": HUB_ID}) @@ -36,7 +54,7 @@ def test_hub_key_accepted(set_core_config: SetCoreConfigCallable, value: bool) - def test_off_without_pairing_method_warns( set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture ) -> None: - """With unpaired access off and no way to pair, no server could ever play.""" + """With unpaired access off and no way to pair, no new server could pair.""" set_core_config(PlatformFramework.ESP32_IDF) config = HUB_CONFIG_SCHEMA({"id": HUB_ID, CONF_UNPAIRED_ACCESS: False}) @@ -76,3 +94,92 @@ def test_on_does_not_warn( HUB_FINAL_VALIDATE_SCHEMA(config) assert NO_PAIRING_WARNING not in caplog.text + + +def test_switch_with_hub_key_rejected(set_core_config: SetCoreConfigCallable) -> None: + """Both would set the policy, and the hub's value would be silently ignored.""" + set_core_config( + PlatformFramework.ESP32_IDF, + full_config={DOMAIN: {"id": HUB_ID, CONF_UNPAIRED_ACCESS: False}}, + ) + config = _switch_config(CONF_UNPAIRED_ACCESS) + + with pytest.raises(cv.Invalid, match="set the switch's restore_mode instead"): + SWITCH_FINAL_VALIDATE_SCHEMA(config) + + +def test_switch_without_hub_key_accepted( + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config(PlatformFramework.ESP32_IDF, full_config={DOMAIN: {"id": HUB_ID}}) + config = _switch_config(CONF_UNPAIRED_ACCESS) + + assert SWITCH_FINAL_VALIDATE_SCHEMA(config) is config + + +def test_switch_without_pairing_method_warns( + set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture +) -> None: + """Turning the switch off would leave no way for a new server to pair.""" + set_core_config(PlatformFramework.ESP32_IDF, full_config={DOMAIN: {"id": HUB_ID}}) + config = _switch_config(CONF_UNPAIRED_ACCESS) + + with caplog.at_level(logging.WARNING): + SWITCH_FINAL_VALIDATE_SCHEMA(config) + + assert NO_PAIRING_WARNING in caplog.text + + +def test_switch_with_pairing_method_does_not_warn( + set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture +) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + full_config={DOMAIN: {"id": HUB_ID, "static_pairing_code": "01234567"}}, + ) + config = _switch_config(CONF_UNPAIRED_ACCESS) + + with caplog.at_level(logging.WARNING): + SWITCH_FINAL_VALIDATE_SCHEMA(config) + + assert NO_PAIRING_WARNING not in caplog.text + + +def test_enabled_switch_ignores_hub_key(set_core_config: SetCoreConfigCallable) -> None: + """Only the unpaired access switch competes with the hub key.""" + set_core_config( + PlatformFramework.ESP32_IDF, + full_config={DOMAIN: {"id": HUB_ID, CONF_UNPAIRED_ACCESS: False}}, + ) + config = _switch_config("enabled") + + assert SWITCH_FINAL_VALIDATE_SCHEMA(config) is config + + +@pytest.mark.parametrize("switch_type", ["enabled", CONF_UNPAIRED_ACCESS]) +def test_duplicate_switch_type_rejected( + set_core_config: SetCoreConfigCallable, switch_type: str +) -> None: + """Two switches of one type would fight over the same hub setting.""" + entry = {"platform": DOMAIN, "type": switch_type} + set_core_config( + PlatformFramework.ESP32_IDF, + full_config={DOMAIN: {"id": HUB_ID}, "switch": [entry, dict(entry)]}, + ) + config = _switch_config(switch_type) + + with pytest.raises(cv.Invalid, match="Only one sendspin"): + SWITCH_FINAL_VALIDATE_SCHEMA(config) + + +@pytest.mark.parametrize("switch_type", ["enabled", CONF_UNPAIRED_ACCESS]) +def test_switch_requests_its_type( + set_core_config: SetCoreConfigCallable, switch_type: str +) -> None: + """The hub skips the codegen value for a setting a switch drives, so the client + waits for that switch to restore before its first start.""" + set_core_config(PlatformFramework.ESP32_IDF) + + _switch_config(switch_type) + + assert _get_data().switch_types == {switch_type} diff --git a/tests/component_tests/sensor/test_timeout_throttle.py b/tests/component_tests/sensor/test_timeout_throttle.py new file mode 100644 index 0000000000..8154471e47 --- /dev/null +++ b/tests/component_tests/sensor/test_timeout_throttle.py @@ -0,0 +1,24 @@ +"""timeout (value last) followed by throttle_with_priority with the same period becomes one filter.""" + +from collections.abc import Callable +from pathlib import Path +import re + + +def test_timeout_throttle_pair_is_merged( + generate_main: Callable[[str | Path], str], +) -> None: + main_cpp = generate_main("tests/component_tests/sensor/timeout_throttle.yaml") + + # Only the `merged` sensor's adjacent pair becomes one object + assert ( + len(re.findall(r"new\(\w+\) sensor::TimeoutThrottleFilter\(1000\);", main_cpp)) + == 1 + ) + merged = re.search(r"merged->set_filters\(\{(\w+)\}\);", main_cpp) + assert merged is not None + assert f"new({merged.group(1)}) sensor::TimeoutThrottleFilter(1000);" in main_cpp + # Every other shape keeps its two filters + for sensor_id in ("other_periods", "other_order", "value_list", "configured_value"): + call = re.search(rf"{sensor_id}->set_filters\(\{{([^}}]*)\}}\);", main_cpp) + assert call is not None and len(call.group(1).split(",")) == 2, sensor_id diff --git a/tests/component_tests/sensor/timeout_throttle.yaml b/tests/component_tests/sensor/timeout_throttle.yaml new file mode 100644 index 0000000000..2557db683c --- /dev/null +++ b/tests/component_tests/sensor/timeout_throttle.yaml @@ -0,0 +1,52 @@ +esphome: + name: test + +esp32: + board: esp32dev + +sensor: + - platform: template + id: merged + filters: + - timeout: + timeout: 1s + value: last + - throttle_with_priority: 1s + - platform: template + id: other_periods + filters: + - timeout: + timeout: 1s + value: last + - throttle_with_priority: 2s + - platform: template + id: other_order + filters: + - throttle_with_priority: 1s + - timeout: + timeout: 1s + value: last + - platform: template + id: value_list + filters: + - timeout: + timeout: 1s + value: last + - throttle_with_priority: + timeout: 1s + value: [0.0] + - platform: template + id: configured_value + filters: + - timeout: + timeout: 1s + value: 0.0 + - throttle_with_priority: 1s + - platform: template + id: inside_or + filters: + - or: + - timeout: + timeout: 1s + value: last + - throttle_with_priority: 1s diff --git a/tests/component_tests/sntp/test_init.py b/tests/component_tests/sntp/test_init.py index 9197ff55d0..db0d3c9d9c 100644 --- a/tests/component_tests/sntp/test_init.py +++ b/tests/component_tests/sntp/test_init.py @@ -8,7 +8,8 @@ from typing import Any import pytest from esphome import config_validation as cv -from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate +from esphome.components.sntp import DOMAIN as SNTP_DOMAIN +from esphome.components.sntp.time import _sntp_final_validate from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME from esphome.core import ID import esphome.final_validate as fv @@ -20,7 +21,7 @@ import esphome.final_validate as fv pytest.param( [ { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time", is_manual=False), CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], } @@ -33,12 +34,12 @@ import esphome.final_validate as fv pytest.param( [ { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_1", is_manual=False), CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], }, { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_2", is_manual=False), CONF_SERVERS: ["192.168.1.2"], }, @@ -51,12 +52,12 @@ import esphome.final_validate as fv pytest.param( [ { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_1", is_manual=False), CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], }, { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_2", is_manual=False), CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], }, @@ -69,17 +70,17 @@ import esphome.final_validate as fv pytest.param( [ { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_1", is_manual=False), CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], }, { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_2", is_manual=False), CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"], }, { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_3", is_manual=False), CONF_SERVERS: ["pool3.ntp.org"], }, @@ -95,7 +96,7 @@ import esphome.final_validate as fv pytest.param( [ { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_1", is_manual=False), CONF_SERVERS: [ "192.168.1.1", @@ -105,7 +106,7 @@ import esphome.final_validate as fv ], }, { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_2", is_manual=False), CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], }, @@ -143,7 +144,7 @@ def test_sntp_instance_merging( sntp_instances = [ tc for tc in updated_conf[CONF_TIME] - if tc.get(CONF_PLATFORM) == CONF_SNTP + if tc.get(CONF_PLATFORM) == SNTP_DOMAIN ] assert len(sntp_instances) == expected_count @@ -169,12 +170,12 @@ def test_sntp_inconsistent_manual_ids() -> None: # Create configs with manual IDs that are inconsistent time_configs = [ { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_1", is_manual=True), CONF_SERVERS: ["192.168.1.1"], }, { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_2", is_manual=True), CONF_SERVERS: ["192.168.1.2"], }, @@ -197,7 +198,7 @@ def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> Non """Test that SNTP merging doesn't affect other time platforms.""" time_configs = [ { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_1", is_manual=False), CONF_SERVERS: ["192.168.1.1"], }, @@ -206,7 +207,7 @@ def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> Non CONF_ID: ID("homeassistant_time", is_manual=False), }, { - CONF_PLATFORM: CONF_SNTP, + CONF_PLATFORM: SNTP_DOMAIN, CONF_ID: ID("sntp_time_2", is_manual=False), CONF_SERVERS: ["192.168.1.2"], }, @@ -226,11 +227,11 @@ def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> Non # Find the platforms platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]} - assert platforms == {CONF_SNTP, "homeassistant"} + assert platforms == {SNTP_DOMAIN, "homeassistant"} # Verify SNTP was merged sntp_instances = [ - tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP + tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == SNTP_DOMAIN ] assert len(sntp_instances) == 1 assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"] diff --git a/tests/component_tests/template/config/alarm_codes.yaml b/tests/component_tests/template/config/alarm_codes.yaml new file mode 100644 index 0000000000..afeb45e1d2 --- /dev/null +++ b/tests/component_tests/template/config/alarm_codes.yaml @@ -0,0 +1,23 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +alarm_control_panel: + - platform: template + id: panel_a + name: Panel A + codes: + - "1234" + - "5678" + - platform: template + id: panel_b + name: Panel B + codes: + - "1234" + - "5678" + - platform: template + id: panel_none + name: Panel None + codes: [] diff --git a/tests/component_tests/template/test_template_alarm_codes.py b/tests/component_tests/template/test_template_alarm_codes.py new file mode 100644 index 0000000000..1a8e94be90 --- /dev/null +++ b/tests/component_tests/template/test_template_alarm_codes.py @@ -0,0 +1,20 @@ +"""Tests for the template alarm control panel codes codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_codes_share_one_progmem_table( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Identical code lists share one nullptr-terminated flash table; empty lists set none.""" + main_cpp = generate_main(component_config_path("alarm_codes.yaml")) + + assert ( + 'static constexpr const char * alarm_codes[] PROGMEM = {"1234", "5678", nullptr};' + in main_cpp + ) + assert "panel_a->set_codes(alarm_codes);" in main_cpp + assert "panel_b->set_codes(alarm_codes);" in main_cpp + assert "panel_none->set_codes(" not in main_cpp diff --git a/tests/component_tests/time/test_triggers.py b/tests/component_tests/time/test_triggers.py index 34fd5a3157..d6c3eed584 100644 --- a/tests/component_tests/time/test_triggers.py +++ b/tests/component_tests/time/test_triggers.py @@ -1,5 +1,5 @@ -"""automation.cpp (CronTrigger and SyncTrigger) is only compiled when an -on_time or on_time_sync automation exists, so the define must follow them.""" +"""automation.cpp (CronTrigger) is only compiled when an on_time automation +exists, so the define must follow it; on_time_sync needs no class.""" from collections.abc import Callable from pathlib import Path @@ -14,7 +14,7 @@ from esphome.core import CORE [ ("no_triggers.yaml", False), ("on_time.yaml", True), - ("on_time_sync.yaml", True), + ("on_time_sync.yaml", False), ], ) def test_triggers_define_follows_automations( diff --git a/tests/component_tests/wireguard/config/allowed_ips.yaml b/tests/component_tests/wireguard/config/allowed_ips.yaml new file mode 100644 index 0000000000..7f191f90fb --- /dev/null +++ b/tests/component_tests/wireguard/config/allowed_ips.yaml @@ -0,0 +1,26 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: test + password: password1 + +time: + - platform: sntp + id: sntp_time + +wireguard: + time_id: sntp_time + address: 172.16.34.100 + netmask: 255.255.255.0 + private_key: wPBMxtNYH3mChicrbpsRpZIasIdPq3yZuthn23FbGG8= + peer_public_key: Hs2JfikvYU03/Kv3YoAs1hrUIPPTEkpsZKSPUljE9yc= + peer_endpoint: wg.server.example + peer_allowed_ips: + - 172.16.34.0/24 + - 192.168.0.0/16 diff --git a/tests/component_tests/wireguard/test_allowed_ips.py b/tests/component_tests/wireguard/test_allowed_ips.py new file mode 100644 index 0000000000..ff490c516e --- /dev/null +++ b/tests/component_tests/wireguard/test_allowed_ips.py @@ -0,0 +1,21 @@ +"""Tests for the wireguard allowed IPs codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_allowed_ips_are_a_progmem_table( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Allowed IPs live in a flash table ended by an empty entry instead of a heap vector.""" + main_cpp = generate_main(component_config_path("allowed_ips.yaml")) + + table = main_cpp.split( + "wireguard::AllowedIP wireguard_allowed_ips[] PROGMEM = ", 1 + )[1] + table = table.split(";", 1)[0] + assert '.ip = "172.16.34.0"' in table + assert '.netmask = "255.255.0.0"' in table + assert ".ip = nullptr" in table + assert "->set_allowed_ips(wireguard_allowed_ips);" in main_cpp diff --git a/tests/components/adc/test.nrf52-mcumgr.yaml b/tests/components/adc/test.nrf52-mcumgr.yaml deleted file mode 100644 index 4be5b4b5c2..0000000000 --- a/tests/components/adc/test.nrf52-mcumgr.yaml +++ /dev/null @@ -1,23 +0,0 @@ -sensor: - - platform: adc - pin: VDDHDIV5 - name: "VDDH Voltage" - update_interval: 5sec - filters: - - multiply: 5 - - platform: adc - pin: VDD - name: "VDD Voltage" - update_interval: 5sec - - platform: adc - pin: AIN0 - name: "AIN0 Voltage" - update_interval: 5sec - - platform: adc - pin: P0.03 - name: "AIN1 Voltage" - update_interval: 5sec - - platform: adc - name: "AIN2 Voltage" - update_interval: 5sec - pin: 4 diff --git a/tests/components/adc/test.bk72xx-ard.yaml b/tests/components/adc/validate.bk72xx-ard.yaml similarity index 64% rename from tests/components/adc/test.bk72xx-ard.yaml rename to tests/components/adc/validate.bk72xx-ard.yaml index 09ef0e1fad..0064276e30 100644 --- a/tests/components/adc/test.bk72xx-ard.yaml +++ b/tests/components/adc/validate.bk72xx-ard.yaml @@ -1,3 +1,5 @@ +# Config only: every LibreTiny chip compiles the same ADC code, covered by the +# ln882x-ard build; this checks the BK72xx analog pin names. sensor: - id: adc_my_sensor platform: adc diff --git a/tests/components/adc/test.esp32-p4-idf.yaml b/tests/components/adc/validate.esp32-p4-idf.yaml similarity index 67% rename from tests/components/adc/test.esp32-p4-idf.yaml rename to tests/components/adc/validate.esp32-p4-idf.yaml index 77cf50d17c..708ab03f76 100644 --- a/tests/components/adc/test.esp32-p4-idf.yaml +++ b/tests/components/adc/validate.esp32-p4-idf.yaml @@ -1,3 +1,5 @@ +# Config only: the P4 compiles the same ADC code as the esp32-s3-idf build +# (curve fitting calibration), only the pin table differs. sensor: - id: adc_my_sensor platform: adc diff --git a/tests/components/adc/test.esp32-s2-idf.yaml b/tests/components/adc/validate.esp32-s2-idf.yaml similarity index 58% rename from tests/components/adc/test.esp32-s2-idf.yaml rename to tests/components/adc/validate.esp32-s2-idf.yaml index a3019466b5..b8d6923bc2 100644 --- a/tests/components/adc/test.esp32-s2-idf.yaml +++ b/tests/components/adc/validate.esp32-s2-idf.yaml @@ -1,3 +1,6 @@ +# Config only: the S2 shares the C2's line fitting path (no default_vref) and +# the original ESP32's ADC init (no clk_src); both are compiled by those +# builds, only the S2 pin table differs. sensor: - id: adc_my_sensor platform: adc diff --git a/tests/components/ags10/common.yaml b/tests/components/ags10/common.yaml index 8009c56295..8441729a8f 100644 --- a/tests/components/ags10/common.yaml +++ b/tests/components/ags10/common.yaml @@ -1,6 +1,7 @@ sensor: - platform: ags10 id: ags10_1 + i2c_id: i2c_bus tvoc: name: AGS10 TVOC update_interval: 60s diff --git a/tests/components/api/__init__.py b/tests/components/api/__init__.py index 4a66287c33..bd84c8d9bb 100644 --- a/tests/components/api/__init__.py +++ b/tests/components/api/__init__.py @@ -1,6 +1,12 @@ +from pathlib import Path + import esphome.codegen as cg +from esphome.const import CONF_ESPHOME, CONF_INCLUDES +from esphome.core import CORE from tests.testing_helpers import ComponentManifestOverride +TESTS_DIR = Path(__file__).parent + def override_manifest(manifest: ComponentManifestOverride) -> None: # USE_API compiles every api source, so emit what they need. No socket @@ -10,16 +16,18 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: cg.add_define("USE_API_PLAINTEXT") # Linked wizard inputs only exist next to homeassistant entities, which need this cg.add_define("USE_API_HOMEASSISTANT_STATES") - # test_wizard.cpp supplies the tables that codegen emits for a real build - for define in ( - "USE_API_WIZARD", - "USE_API_WIZARD_INPUTS", - "USE_API_WIZARD_LINKED_INPUTS", - "USE_API_WIZARD_STANDALONE_INPUTS", - ): - cg.add_define(define) - cg.add_define("API_WIZARD_DATA_SIZE", 200) - cg.add_define("API_WIZARD_INPUT_COUNT", 2) + # test_wizard.cpp supplies the tables that codegen emits for a real build; + # it is only compiled when api's own tests are, not when api is a dependency + if any(Path(p) == TESTS_DIR for p in CORE.config[CONF_ESPHOME][CONF_INCLUDES]): + for define in ( + "USE_API_WIZARD", + "USE_API_WIZARD_INPUTS", + "USE_API_WIZARD_LINKED_INPUTS", + "USE_API_WIZARD_STANDALONE_INPUTS", + ): + cg.add_define(define) + cg.add_define("API_WIZARD_DATA_SIZE", 200) + cg.add_define("API_WIZARD_INPUT_COUNT", 2) cg.add_define("API_MAX_SEND_QUEUE", 8) cg.add_define("MAX_API_CONNECTIONS", 1) cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") diff --git a/tests/components/bmi270/common.yaml b/tests/components/bmi270/common.yaml index 0f9b70631c..41f30fd5c6 100644 --- a/tests/components/bmi270/common.yaml +++ b/tests/components/bmi270/common.yaml @@ -61,6 +61,7 @@ sensor: motion: - platform: bmi270 id: bmi270_motion + i2c_id: i2c_bus # Accelerometer full-scale range: 2G | 4G | 8G | 16G accelerometer_range: 4G diff --git a/tests/components/datetime/common-triggers.yaml b/tests/components/datetime/common-triggers.yaml new file mode 100644 index 0000000000..e93e8b2b51 --- /dev/null +++ b/tests/components/datetime/common-triggers.yaml @@ -0,0 +1,10 @@ +datetime: + - platform: template + name: Datetime Trigger Test + id: datetime_trigger_test + type: datetime + optimistic: true + on_value: + - logger.log: + format: "New value %04d-%02d-%02d" + args: [x.year, x.month, x.day_of_month] diff --git a/tests/components/datetime/test-triggers.esp8266-ard.yaml b/tests/components/datetime/test-triggers.esp8266-ard.yaml new file mode 100644 index 0000000000..3829bedb1a --- /dev/null +++ b/tests/components/datetime/test-triggers.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + datetime: !include common.yaml + triggers: !include common-triggers.yaml diff --git a/tests/components/debug/test.bk72xx-ard.yaml b/tests/components/debug/test.bk72xx-ard.yaml index fdae374788..65320a47d6 100644 --- a/tests/components/debug/test.bk72xx-ard.yaml +++ b/tests/components/debug/test.bk72xx-ard.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + debug: !include common.yaml sensor: - platform: debug diff --git a/tests/components/debug/test.esp32-ard.yaml b/tests/components/debug/test.esp32-ard.yaml index 8f93b0925e..cdb1615be1 100644 --- a/tests/components/debug/test.esp32-ard.yaml +++ b/tests/components/debug/test.esp32-ard.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + debug: !include common.yaml esp32: cpu_frequency: 240MHz diff --git a/tests/components/debug/test.esp32-idf.yaml b/tests/components/debug/test.esp32-idf.yaml index 6a9996ad06..73bc97b2a9 100644 --- a/tests/components/debug/test.esp32-idf.yaml +++ b/tests/components/debug/test.esp32-idf.yaml @@ -1,12 +1,11 @@ -<<: !include common.yaml +packages: + debug: !include common.yaml esp32: cpu_frequency: 240MHz sensor: - platform: debug - free: - name: "Heap Free" psram: name: "Free PSRAM" fragmentation: diff --git a/tests/components/debug/test.esp32-s2-idf.yaml b/tests/components/debug/test.esp32-s2-idf.yaml deleted file mode 100644 index 80919b0bab..0000000000 --- a/tests/components/debug/test.esp32-s2-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -<<: !include common.yaml - -sensor: - - platform: debug - fragmentation: - name: "Heap Fragmentation" - min_free: - name: "Heap Min Free" diff --git a/tests/components/debug/test.esp8266-ard.yaml b/tests/components/debug/test.esp8266-ard.yaml index 1398087bf0..da54acfc0d 100644 --- a/tests/components/debug/test.esp8266-ard.yaml +++ b/tests/components/debug/test.esp8266-ard.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + debug: !include common.yaml sensor: - platform: debug diff --git a/tests/components/debug/test.host.yaml b/tests/components/debug/test.host.yaml index dade44d145..25c05c6f81 100644 --- a/tests/components/debug/test.host.yaml +++ b/tests/components/debug/test.host.yaml @@ -1 +1,2 @@ -<<: !include common.yaml +packages: + debug: !include common.yaml diff --git a/tests/components/debug/test.ln882x-ard.yaml b/tests/components/debug/test.ln882x-ard.yaml deleted file mode 100644 index fdae374788..0000000000 --- a/tests/components/debug/test.ln882x-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -<<: !include common.yaml - -sensor: - - platform: debug - min_free: - name: "Heap Min Free" diff --git a/tests/components/debug/test.nrf52-adafruit.yaml b/tests/components/debug/test.nrf52-adafruit.yaml index 6a446634af..53eea184f5 100644 --- a/tests/components/debug/test.nrf52-adafruit.yaml +++ b/tests/components/debug/test.nrf52-adafruit.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + debug: !include common.yaml nrf52: reg0: diff --git a/tests/components/debug/test.nrf52-mcumgr.yaml b/tests/components/debug/test.nrf52-mcumgr.yaml index dade44d145..25c05c6f81 100644 --- a/tests/components/debug/test.nrf52-mcumgr.yaml +++ b/tests/components/debug/test.nrf52-mcumgr.yaml @@ -1 +1,2 @@ -<<: !include common.yaml +packages: + debug: !include common.yaml diff --git a/tests/components/debug/test.nrf52-xiao-ble.yaml b/tests/components/debug/test.nrf52-xiao-ble.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/debug/test.nrf52-xiao-ble.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/debug/test.rp2040-ard.yaml b/tests/components/debug/test.rp2040-ard.yaml index dade44d145..25c05c6f81 100644 --- a/tests/components/debug/test.rp2040-ard.yaml +++ b/tests/components/debug/test.rp2040-ard.yaml @@ -1 +1,2 @@ -<<: !include common.yaml +packages: + debug: !include common.yaml diff --git a/tests/components/debug/test.rtl87xx-ard.yaml b/tests/components/debug/test.rtl87xx-ard.yaml deleted file mode 100644 index fdae374788..0000000000 --- a/tests/components/debug/test.rtl87xx-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -<<: !include common.yaml - -sensor: - - platform: debug - min_free: - name: "Heap Min Free" diff --git a/tests/components/deep_sleep/test-timer-only.esp32-idf.yaml b/tests/components/deep_sleep/test-timer-only.esp32-idf.yaml new file mode 100644 index 0000000000..ad93ea225f --- /dev/null +++ b/tests/components/deep_sleep/test-timer-only.esp32-idf.yaml @@ -0,0 +1,4 @@ +# No pin anywhere in the config: the pin class methods deep_sleep calls must still link +deep_sleep: + run_duration: 10s + sleep_duration: 50s diff --git a/tests/components/deep_sleep/test.esp32-idf.yaml b/tests/components/deep_sleep/test.esp32-idf.yaml index e45eb08349..cc9bf5af16 100644 --- a/tests/components/deep_sleep/test.esp32-idf.yaml +++ b/tests/components/deep_sleep/test.esp32-idf.yaml @@ -1,5 +1,27 @@ substitutions: wakeup_pin: GPIO4 -<<: !include common.yaml -<<: !include common-esp32-all.yaml +packages: + deep_sleep: !include common.yaml + deep_sleep_esp32: !include common-esp32-all.yaml + +power_supply: + - id: power_supply_ds_esp32 + pin: + number: GPIO5 + hold_state: true + +output: + - platform: gpio + pin: + number: GPIO22 + hold_state: true + id: gpio_output_ds_esp32 + power_supply: power_supply_ds_esp32 + +switch: + - platform: gpio + pin: + number: GPIO23 + hold_state: true + id: gpio_switch_ds_esp32 diff --git a/tests/components/display/common-page-change.yaml b/tests/components/display/common-page-change.yaml new file mode 100644 index 0000000000..649cad767b --- /dev/null +++ b/tests/components/display/common-page-change.yaml @@ -0,0 +1,26 @@ +display: + - platform: ssd1306_i2c + i2c_id: i2c_bus + model: SSD1306_128X64 + reset_pin: ${reset_pin} + id: display_page_change_display + pages: + - id: display_page_change_page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: display_page_change_page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + - then: + lambda: 'ESP_LOGD("display", "any change, %p %p", from, to);' + - from: display_page_change_page1 + then: + lambda: 'ESP_LOGD("display", "left page 1, %p %p", from, to);' + - to: display_page_change_page1 + then: + lambda: 'ESP_LOGD("display", "entered page 1, %p %p", from, to);' + - from: display_page_change_page1 + to: display_page_change_page2 + then: + lambda: 'ESP_LOGD("display", "1 -> 2, %p %p", from, to);' diff --git a/tests/components/display/test-page-change.esp32-idf.yaml b/tests/components/display/test-page-change.esp32-idf.yaml new file mode 100644 index 0000000000..b44cc076b2 --- /dev/null +++ b/tests/components/display/test-page-change.esp32-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + reset_pin: GPIO15 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + display: !include common-page-change.yaml diff --git a/tests/components/dlms_meter/common.yaml b/tests/components/dlms_meter/common.yaml index 59d854a3ae..fa6deff6be 100644 --- a/tests/components/dlms_meter/common.yaml +++ b/tests/components/dlms_meter/common.yaml @@ -4,63 +4,47 @@ dlms_meter: decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" auth_key: "11223344556677889900AABBCCDDEEFF" skip_crc: true - provider: "netznoe" custom_patterns: - "custom_pattern_1" - "custom_pattern_2" + - pattern: "L, TSTR" + name: "MeterID" + default_obis: "0.0.96.1.0.255" + - pattern: "F, TDTM" + name: "DateTime" + default_obis: "0.0.1.0.0.255" sensor: - # Old Schema tests - platform: dlms_meter - voltage_l1: - name: "Voltage L1" - voltage_l2: - name: "Voltage L2" - voltage_l3: - name: "Voltage L3" - current_l1: - name: "Current L1" - current_l2: - name: "Current L2" - current_l3: - name: "Current L3" - active_power_plus: - name: "Active power taken from grid" - active_power_minus: - name: "Active power put into grid" - active_energy_plus: - name: "Active energy taken from grid" - active_energy_minus: - name: "Active energy put into grid" - reactive_energy_plus: - name: "Reactive energy taken from grid" - reactive_energy_minus: - name: "Reactive energy put into grid" - power_factor: - name: "Power factor" + obis_code: "1.0.32.7.0.255" + name: "Voltage L1" + unit_of_measurement: V + accuracy_decimals: 1 + device_class: voltage + state_class: measurement + - platform: dlms_meter + obis_code: "1.0.1.8.0.255" + name: "Active energy taken from grid" + unit_of_measurement: Wh + device_class: energy + state_class: total_increasing - # Dynamic Schema tests - platform: dlms_meter dlms_meter_id: dlms_meter_hub obis_code: "1-0:99.99.9" name: "Custom Dynamic Sensor" text_sensor: - # Old Schema tests - platform: dlms_meter - timestamp: - name: "timestamp" - meternumber: - name: "Meter Number" + obis_code: "0.0.1.0.0.255" + name: "timestamp" - # Dynamic Schema tests - platform: dlms_meter dlms_meter_id: dlms_meter_hub obis_code: "0-0:99.99.9" name: "Custom Dynamic Text Sensor" binary_sensor: - # Dynamic Schema tests (Binary sensors only use the dynamic schema) - platform: dlms_meter dlms_meter_id: dlms_meter_hub obis_code: "0-1:2.3.4" diff --git a/tests/components/ds248x/common.yaml b/tests/components/ds248x/common.yaml index 53ae56f20a..4429f5b772 100644 --- a/tests/components/ds248x/common.yaml +++ b/tests/components/ds248x/common.yaml @@ -5,10 +5,12 @@ # - DS2484: adjustable 1-Wire timing + RWPU pullup resistor selection ds248x: - id: ds2482_100 + i2c_id: i2c_bus address: 0x18 type: ds2482-100 active_pullup: true - id: ds2482_101 + i2c_id: i2c_bus address: 0x19 type: ds2482-101 active_pullup: true @@ -18,10 +20,12 @@ ds248x: bus_sleep: true hub_sleep: true - id: ds2482_800 + i2c_id: i2c_bus address: 0x1a type: ds2482-800 active_pullup: true - id: ds2484_hub + i2c_id: i2c_bus address: 0x1b type: ds2484 active_pullup: true diff --git a/tests/components/ethernet/common-w5500-spi-id.yaml b/tests/components/ethernet/common-w5500-spi-id.yaml deleted file mode 100644 index 14c01842f0..0000000000 --- a/tests/components/ethernet/common-w5500-spi-id.yaml +++ /dev/null @@ -1,17 +0,0 @@ -ethernet: - type: W5500 - spi_id: spi_bus - cs_pin: 5 - interrupt_pin: 36 - reset_pin: 22 - clock_speed: 10Mhz - manual_ip: - static_ip: 192.168.178.56 - gateway: 192.168.178.1 - subnet: 255.255.255.0 - domain: .local - mac_address: "02:AA:BB:CC:DD:01" - on_connect: - - logger.log: "Ethernet connected!" - on_disconnect: - - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-ip101.esp32-idf.yaml b/tests/components/ethernet/test-ip101.esp32-idf.yaml index e52329d7ea..628b63d4e9 100644 --- a/tests/components/ethernet/test-ip101.esp32-idf.yaml +++ b/tests/components/ethernet/test-ip101.esp32-idf.yaml @@ -1 +1,6 @@ -<<: !include common-ip101.yaml +packages: + ethernet: !include common-ip101.yaml + +# IPv6 on the internal EMAC: compiles the IPv6 event handler and multicast filter +network: + enable_ipv6: true diff --git a/tests/components/ethernet/test-ipv6.esp32-idf.yaml b/tests/components/ethernet/test-ipv6.esp32-idf.yaml deleted file mode 100644 index 607e82dfa9..0000000000 --- a/tests/components/ethernet/test-ipv6.esp32-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -network: - enable_ipv6: true - -ethernet: - type: IP101 - mdc_pin: 23 - mdio_pin: 18 - clk: - pin: 0 - mode: CLK_EXT_IN - phy_addr: 1 - power_pin: 5 diff --git a/tests/components/ethernet/test-lifecycle.esp32-idf.yaml b/tests/components/ethernet/test-lifecycle.esp32-idf.yaml deleted file mode 100644 index 904a916789..0000000000 --- a/tests/components/ethernet/test-lifecycle.esp32-idf.yaml +++ /dev/null @@ -1,39 +0,0 @@ -ethernet: - id: eth - type: W5500 - clk_pin: 19 - mosi_pin: 21 - miso_pin: 23 - cs_pin: 18 - interrupt_pin: 36 - reset_pin: 22 - enable_on_boot: false - manual_ip: - static_ip: 192.168.178.56 - gateway: 192.168.178.1 - subnet: 255.255.255.0 - mac_address: "02:AA:BB:CC:DD:01" - interface: spi2 - -esphome: - on_boot: - priority: 200 - then: - - if: - condition: - not: - ethernet.enabled: - then: - - ethernet.enable: - -button: - - platform: template - name: "Disable Ethernet" - on_press: - - ethernet.disable: - -binary_sensor: - - platform: template - name: "Ethernet Connected" - lambda: |- - return id(eth).is_connected(); diff --git a/tests/components/ethernet/test-w5500-spi-id.esp32-idf.yaml b/tests/components/ethernet/test-w5500-spi-id.esp32-idf.yaml deleted file mode 100644 index 16a25ced2a..0000000000 --- a/tests/components/ethernet/test-w5500-spi-id.esp32-idf.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-idf.yaml - ethernet: !include common-w5500-spi-id.yaml diff --git a/tests/components/ethernet/test-w5500-wifi-psram.esp32-s3-idf.yaml b/tests/components/ethernet/test-w5500-wifi-psram.esp32-s3-idf.yaml deleted file mode 100644 index d454dbadc3..0000000000 --- a/tests/components/ethernet/test-w5500-wifi-psram.esp32-s3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# W5500 next to wifi with PSRAM: compiles the input path that moves received frames to PSRAM -packages: - ethernet: !include common-w5500.yaml - -psram: - mode: quad - -wifi: - ssid: MySSID - password: password1 - -network: - priority: - - ethernet - - wifi diff --git a/tests/components/ethernet/test-w5500.esp32-idf.yaml b/tests/components/ethernet/test-w5500.esp32-idf.yaml index f1551fef60..c99dfaf528 100644 --- a/tests/components/ethernet/test-w5500.esp32-idf.yaml +++ b/tests/components/ethernet/test-w5500.esp32-idf.yaml @@ -1,20 +1,47 @@ +# W5500 on a shared SPI bus, started from an automation instead of on boot. +# Covers the spi_id attach path and the enable/disable lifecycle in one build. +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + ethernet: + id: eth type: W5500 - clk_pin: 19 - mosi_pin: 21 - miso_pin: 23 - cs_pin: 18 + spi_id: spi_bus + cs_pin: 5 interrupt_pin: 36 reset_pin: 22 clock_speed: 10Mhz + enable_on_boot: false manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" - interface: spi2 on_connect: - logger.log: "Ethernet connected!" on_disconnect: - logger.log: "Ethernet disconnected!" + +esphome: + on_boot: + priority: 200 + then: + - if: + condition: + not: + ethernet.enabled: + then: + - ethernet.enable: + +button: + - platform: template + name: "Disable Ethernet" + on_press: + - ethernet.disable: + +binary_sensor: + - platform: template + name: "Ethernet Connected" + lambda: |- + return id(eth).is_connected(); diff --git a/tests/components/ethernet/test.esp32-s3-idf.yaml b/tests/components/ethernet/test.esp32-s3-idf.yaml index 36f1b5365f..d454dbadc3 100644 --- a/tests/components/ethernet/test.esp32-s3-idf.yaml +++ b/tests/components/ethernet/test.esp32-s3-idf.yaml @@ -1 +1,15 @@ -<<: !include common-w5500.yaml +# W5500 next to wifi with PSRAM: compiles the input path that moves received frames to PSRAM +packages: + ethernet: !include common-w5500.yaml + +psram: + mode: quad + +wifi: + ssid: MySSID + password: password1 + +network: + priority: + - ethernet + - wifi diff --git a/tests/components/ethernet/test-ksz8081rna.esp32-idf.yaml b/tests/components/ethernet/validate-ksz8081rna.esp32-idf.yaml similarity index 100% rename from tests/components/ethernet/test-ksz8081rna.esp32-idf.yaml rename to tests/components/ethernet/validate-ksz8081rna.esp32-idf.yaml diff --git a/tests/components/ethernet/test-lan8720-with-expander.esp32-idf.yaml b/tests/components/ethernet/validate-lan8720-with-expander.esp32-idf.yaml similarity index 100% rename from tests/components/ethernet/test-lan8720-with-expander.esp32-idf.yaml rename to tests/components/ethernet/validate-lan8720-with-expander.esp32-idf.yaml diff --git a/tests/components/ethernet/validate-w5500-interface.esp32-idf.yaml b/tests/components/ethernet/validate-w5500-interface.esp32-idf.yaml new file mode 100644 index 0000000000..a2c62e4a52 --- /dev/null +++ b/tests/components/ethernet/validate-w5500-interface.esp32-idf.yaml @@ -0,0 +1,12 @@ +# Config only: a user supplied SPI interface on dedicated pins exercises the +# interface option in validation. +ethernet: + type: W5500 + interface: spi3 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 17 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 12 + clock_speed: 10Mhz diff --git a/tests/components/gpio/test.esp32-c3-idf.yaml b/tests/components/gpio/test.esp32-c3-idf.yaml index e9071b4356..42c207d627 100644 --- a/tests/components/gpio/test.esp32-c3-idf.yaml +++ b/tests/components/gpio/test.esp32-c3-idf.yaml @@ -6,4 +6,19 @@ substitutions: switch_pin_3: GPIO6 switch_pin_4: GPIO7 -<<: !include common.yaml +packages: + gpio: !include common.yaml + +binary_sensor: + - platform: gpio + pin: + number: GPIO8 + hold_state: true + id: gpio_binary_sensor_esp32 + +output: + - platform: gpio + pin: + number: GPIO9 + hold_state: true + id: gpio_output_esp32 diff --git a/tests/components/gpio/test.esp32-idf.yaml b/tests/components/gpio/test.esp32-idf.yaml index 862aa533ea..12be2ef6fe 100644 --- a/tests/components/gpio/test.esp32-idf.yaml +++ b/tests/components/gpio/test.esp32-idf.yaml @@ -6,4 +6,26 @@ substitutions: switch_pin_3: GPIO16 switch_pin_4: GPIO17 -<<: !include common.yaml +packages: + gpio: !include common.yaml + +binary_sensor: + - platform: gpio + pin: + number: GPIO18 + hold_state: true + id: gpio_binary_sensor_esp32 + +output: + - platform: gpio + pin: + number: GPIO19 + hold_state: true + id: gpio_output_esp32 + +switch: + - platform: gpio + pin: + number: GPIO21 + hold_state: true + id: gpio_switch_esp32 diff --git a/tests/components/graphical_display_menu/common-triggers.yaml b/tests/components/graphical_display_menu/common-triggers.yaml new file mode 100644 index 0000000000..ea4daeb162 --- /dev/null +++ b/tests/components/graphical_display_menu/common-triggers.yaml @@ -0,0 +1,24 @@ +display: + - platform: ssd1306_i2c + i2c_id: i2c_bus + id: graphical_display_menu_triggers_display + model: SSD1306_128X64 + reset_pin: ${reset_pin} + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +font: + - file: "gfonts://Roboto" + id: graphical_display_menu_triggers_font + size: 20 + +graphical_display_menu: + id: graphical_display_menu_triggers_menu + display: graphical_display_menu_triggers_display + font: graphical_display_menu_triggers_font + on_redraw: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "redraw, active: %d", it->is_active());' + items: + - type: label + text: "Label" diff --git a/tests/components/graphical_display_menu/test-triggers.esp32-idf.yaml b/tests/components/graphical_display_menu/test-triggers.esp32-idf.yaml new file mode 100644 index 0000000000..2adb772294 --- /dev/null +++ b/tests/components/graphical_display_menu/test-triggers.esp32-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + reset_pin: GPIO15 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + graphical_display_menu: !include common-triggers.yaml diff --git a/tests/components/http_request/http_request.yaml b/tests/components/http_request/http_request.yaml index eb8a6d33e7..b82c0581d1 100644 --- a/tests/components/http_request/http_request.yaml +++ b/tests/components/http_request/http_request.yaml @@ -71,6 +71,8 @@ esphome: # encode_json_ receives const copies of non-const reference args. - http_request.post: url: https://esphome.io + request_headers: + X-Body-Length: !lambda 'return body.empty() ? "0" : "1";' json: status: "ok" # Same with json: lambda variant, exercises json_func_ path @@ -102,3 +104,23 @@ script: then: - http_request.get: url: "http://google.com" + + - id: send_with_headers + parameters: + token: string + then: + - http_request.get: + url: https://esphome.io + request_headers: + Accept: application/json + X-Device: esphome + - http_request.get: + url: https://esphome.io/other + request_headers: + Accept: application/json + X-Device: esphome + - http_request.get: + url: https://esphome.io + request_headers: + Accept: application/json + X-Token-Set: !lambda 'return token.empty() ? "no" : "yes";' diff --git a/tests/components/improv_ble/test-triggers.esp32-idf.yaml b/tests/components/improv_ble/test-triggers.esp32-idf.yaml new file mode 100644 index 0000000000..ceeb515ef3 --- /dev/null +++ b/tests/components/improv_ble/test-triggers.esp32-idf.yaml @@ -0,0 +1,21 @@ +wifi: + ssid: MySSID + password: password1 + +binary_sensor: + - platform: gpio + pin: 0 + id: improv_trigger_authorizer + +improv_ble: + authorizer: improv_trigger_authorizer + on_start: + - logger.log: Improv started + on_provisioning: + - logger.log: Provisioning + on_provisioned: + - logger.log: Provisioned + on_stop: + - logger.log: Improv stopped + on_state: + - lambda: ESP_LOGD("improv", "State %d error %d", (int) state, (int) error); diff --git a/tests/components/improv_serial/common-usb_serial_jtag.yaml b/tests/components/improv_serial/common-usb_serial_jtag.yaml deleted file mode 100644 index 0abcc07168..0000000000 --- a/tests/components/improv_serial/common-usb_serial_jtag.yaml +++ /dev/null @@ -1,8 +0,0 @@ -wifi: - ssid: MySSID - password: password1 - -logger: - hardware_uart: USB_SERIAL_JTAG - -improv_serial: diff --git a/tests/components/improv_serial/common-uart0.yaml b/tests/components/improv_serial/common.yaml similarity index 100% rename from tests/components/improv_serial/common-uart0.yaml rename to tests/components/improv_serial/common.yaml diff --git a/tests/components/improv_serial/test-uart-bus.esp8266-ard.yaml b/tests/components/improv_serial/test-uart-bus.esp8266-ard.yaml deleted file mode 100644 index 40a6b7f4fe..0000000000 --- a/tests/components/improv_serial/test-uart-bus.esp8266-ard.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml - improv_serial: !include common-uart-bus.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-c3-idf.yaml b/tests/components/improv_serial/test-uart0.esp32-c3-idf.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-idf.yaml b/tests/components/improv_serial/test-uart0.esp32-idf.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.esp32-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-s2-idf.yaml b/tests/components/improv_serial/test-uart0.esp32-s2-idf.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.esp32-s2-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-s3-idf.yaml b/tests/components/improv_serial/test-uart0.esp32-s3-idf.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.esp32-s3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp8266-ard.yaml b/tests/components/improv_serial/test-uart0.esp8266-ard.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.esp8266-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.rp2040-ard.yaml b/tests/components/improv_serial/test-uart0.rp2040-ard.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.rp2040-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.esp32-s2-idf.yaml b/tests/components/improv_serial/test-usb_cdc.esp32-s2-idf.yaml index cfdaec9771..f4217b994d 100644 --- a/tests/components/improv_serial/test-usb_cdc.esp32-s2-idf.yaml +++ b/tests/components/improv_serial/test-usb_cdc.esp32-s2-idf.yaml @@ -1 +1,2 @@ -<<: !include common-usb_cdc.yaml +packages: + improv_serial: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.rp2040-ard.yaml b/tests/components/improv_serial/test-usb_cdc.rp2040-ard.yaml deleted file mode 100644 index cfdaec9771..0000000000 --- a/tests/components/improv_serial/test-usb_cdc.rp2040-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_serial_jtag.esp32-c3-idf.yaml b/tests/components/improv_serial/test-usb_serial_jtag.esp32-c3-idf.yaml deleted file mode 100644 index 46a927ff10..0000000000 --- a/tests/components/improv_serial/test-usb_serial_jtag.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_serial_jtag.yaml diff --git a/tests/components/improv_serial/test-usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/improv_serial/test-usb_serial_jtag.esp32-s3-idf.yaml deleted file mode 100644 index 46a927ff10..0000000000 --- a/tests/components/improv_serial/test-usb_serial_jtag.esp32-s3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_serial_jtag.yaml diff --git a/tests/components/improv_serial/test.esp32-c3-idf.yaml b/tests/components/improv_serial/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2a833811fc --- /dev/null +++ b/tests/components/improv_serial/test.esp32-c3-idf.yaml @@ -0,0 +1,2 @@ +packages: + improv_serial: !include common.yaml diff --git a/tests/components/improv_serial/test.esp32-idf.yaml b/tests/components/improv_serial/test.esp32-idf.yaml new file mode 100644 index 0000000000..2a833811fc --- /dev/null +++ b/tests/components/improv_serial/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + improv_serial: !include common.yaml diff --git a/tests/components/improv_serial/test.esp8266-ard.yaml b/tests/components/improv_serial/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2a833811fc --- /dev/null +++ b/tests/components/improv_serial/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + improv_serial: !include common.yaml diff --git a/tests/components/improv_serial/test.rp2040-ard.yaml b/tests/components/improv_serial/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2a833811fc --- /dev/null +++ b/tests/components/improv_serial/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + improv_serial: !include common.yaml diff --git a/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml deleted file mode 100644 index e25c47ab23..0000000000 --- a/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - cs_pin: GPIO5 - gdo0_pin: GPIO4 - gdo2_pin: GPIO16 - -packages: - common: !include common.yaml - spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml - cc1101: !include common-cc1101.yaml diff --git a/tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml deleted file mode 100644 index 8172885b31..0000000000 --- a/tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - common: !include common.yaml - rx: !include common-rx.yaml diff --git a/tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml b/tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml deleted file mode 100644 index 8172885b31..0000000000 --- a/tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - common: !include common.yaml - rx: !include common-rx.yaml diff --git a/tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml b/tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml deleted file mode 100644 index 7162f15b2d..0000000000 --- a/tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - common: !include common.yaml - tx: !include common-tx.yaml diff --git a/tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml deleted file mode 100644 index 7162f15b2d..0000000000 --- a/tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - common: !include common.yaml - tx: !include common-tx.yaml diff --git a/tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml b/tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml deleted file mode 100644 index 7162f15b2d..0000000000 --- a/tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - common: !include common.yaml - tx: !include common-tx.yaml diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index 64406afda8..c1e58c673f 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -32,6 +32,7 @@ esphome: - lambda: |- const auto &effects = id(test_monochromatic_light).get_effects(); auto &same = id(test_monochromatic_light).get_effects(); + const auto copy = id(test_monochromatic_light).get_effects(); // the view is copyable uint32_t total = effects.size(); for (auto *effect : effects) { ESP_LOGD("test", "Effect %s", effect->get_name().c_str()); @@ -39,7 +40,7 @@ esphome: if (total > 0) { ESP_LOGD("test", "First %s", effects.at(0)->get_name().c_str()); // Raw pointer iterators on purpose: external code binds std::find's result to `const auto *` - auto *it = std::find(same.begin(), same.end(), effects[0]); + auto *it = std::find(same.begin(), same.end(), copy[0]); ESP_LOGD("test", "Index %d", (int) (it - same.begin())); } diff --git a/tests/components/light/test_light_state.cpp b/tests/components/light/test_light_state.cpp index 85bfbd09ef..076438c7d2 100644 --- a/tests/components/light/test_light_state.cpp +++ b/tests/components/light/test_light_state.cpp @@ -1,6 +1,5 @@ #include -#include "esphome/components/light/light_effect.h" #include "esphome/components/light/light_output.h" #include "esphome/components/light/light_state.h" @@ -18,36 +17,8 @@ class BrightnessOutput : public LightOutput { void write_state(LightState *state) override {} }; -class NoopEffect : public LightEffect { - public: - using LightEffect::LightEffect; - void apply() override {} -}; - -// start_effect_() is where the uint32_t index is narrowed to the stored uint16_t. -class TestableLightState : public LightState { - public: - using LightState::LightState; - using LightState::start_effect_; -}; - } // namespace -// add_effects() is public, so an external component can exceed the codegen cap on effect count; -// an index the uint16_t can't hold must be ignored rather than wrap onto another effect. -TEST(LightStateEffect, IndexAboveUint16IsIgnoredAndKeepsTheActiveEffect) { - BrightnessOutput output; - TestableLightState state(&output); - NoopEffect effect("Noop"); - state.add_effects({&effect}); - - state.start_effect_(1); - ASSERT_EQ(state.get_current_effect_index(), 1u); - - state.start_effect_(0x10000u); // unchecked narrowing wraps this to 0, which stops the effect - EXPECT_EQ(state.get_current_effect_index(), 1u); -} - // get_gamma_correct() reads the gamma codegen stores after the lookup table, rounded to two decimals. TEST(LightStateGamma, ReadsTheGammaStoredWithTheTable) { static constexpr GammaTable TABLE{{}, 280}; diff --git a/tests/components/logger/test-custom_buffer_size.esp32-idf.yaml b/tests/components/logger/test-custom_buffer_size.esp32-idf.yaml deleted file mode 100644 index 9a396ca023..0000000000 --- a/tests/components/logger/test-custom_buffer_size.esp32-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common-default_uart.yaml - -logger: - id: logger_id - task_log_buffer_size: 1024B # Set a custom buffer size diff --git a/tests/components/logger/test-disable_log_buffer.esp32-idf.yaml b/tests/components/logger/test-disable_log_buffer.esp32-idf.yaml deleted file mode 100644 index 4260f178f9..0000000000 --- a/tests/components/logger/test-disable_log_buffer.esp32-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common-default_uart.yaml - -logger: - id: logger_id - task_log_buffer_size: 0 diff --git a/tests/components/logger/test-uart0.ln882x-ard.yaml b/tests/components/logger/test-uart0.ln882x-ard.yaml deleted file mode 100644 index 864d2b96de..0000000000 --- a/tests/components/logger/test-uart0.ln882x-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -logger: - hardware_uart: UART0 diff --git a/tests/components/logger/test-usb_cdc.esp32-c3-idf.yaml b/tests/components/logger/test-usb_cdc.esp32-c3-idf.yaml deleted file mode 100644 index cfdaec9771..0000000000 --- a/tests/components/logger/test-usb_cdc.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_cdc.yaml diff --git a/tests/components/logger/test-usb_cdc.esp32-s2-idf.yaml b/tests/components/logger/test-usb_cdc.esp32-s2-idf.yaml deleted file mode 100644 index cfdaec9771..0000000000 --- a/tests/components/logger/test-usb_cdc.esp32-s2-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_cdc.yaml diff --git a/tests/components/logger/test-usb_serial_jtag.esp32-c3-idf.yaml b/tests/components/logger/test-usb_serial_jtag.esp32-c3-idf.yaml deleted file mode 100644 index 46a927ff10..0000000000 --- a/tests/components/logger/test-usb_serial_jtag.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_serial_jtag.yaml diff --git a/tests/components/logger/test-usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/logger/test-usb_serial_jtag.esp32-s3-idf.yaml deleted file mode 100644 index 46a927ff10..0000000000 --- a/tests/components/logger/test-usb_serial_jtag.esp32-s3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_serial_jtag.yaml diff --git a/tests/components/logger/test.esp32-c3-idf.yaml b/tests/components/logger/test.esp32-c3-idf.yaml index 3fe04e18a3..4260f178f9 100644 --- a/tests/components/logger/test.esp32-c3-idf.yaml +++ b/tests/components/logger/test.esp32-c3-idf.yaml @@ -1 +1,5 @@ <<: !include common-default_uart.yaml + +logger: + id: logger_id + task_log_buffer_size: 0 diff --git a/tests/components/logger/test-on_message.host.yaml b/tests/components/logger/test.host.yaml similarity index 100% rename from tests/components/logger/test-on_message.host.yaml rename to tests/components/logger/test.host.yaml diff --git a/tests/components/logger/test.ln882x-ard.yaml b/tests/components/logger/test.ln882x-ard.yaml index c5bc9de6e8..d5c62d298a 100644 --- a/tests/components/logger/test.ln882x-ard.yaml +++ b/tests/components/logger/test.ln882x-ard.yaml @@ -2,4 +2,5 @@ logger: id: logger_id + hardware_uart: UART0 task_log_buffer_size: 1024B diff --git a/tests/components/logger/test-max_buffer_size.esp32-idf.yaml b/tests/components/logger/validate-max_buffer_size.esp32-idf.yaml similarity index 100% rename from tests/components/logger/test-max_buffer_size.esp32-idf.yaml rename to tests/components/logger/validate-max_buffer_size.esp32-idf.yaml diff --git a/tests/components/logger/test-min_buffer_size.esp32-idf.yaml b/tests/components/logger/validate-min_buffer_size.esp32-idf.yaml similarity index 100% rename from tests/components/logger/test-min_buffer_size.esp32-idf.yaml rename to tests/components/logger/validate-min_buffer_size.esp32-idf.yaml diff --git a/tests/components/lsm6ds/common.yaml b/tests/components/lsm6ds/common.yaml index aeacd31448..40bfbfc2a1 100644 --- a/tests/components/lsm6ds/common.yaml +++ b/tests/components/lsm6ds/common.yaml @@ -61,6 +61,7 @@ sensor: motion: - platform: lsm6ds id: lsm6ds_motion + i2c_id: i2c_bus # Accelerometer full-scale range: 2G | 4G | 8G | 16G accelerometer_range: 4G diff --git a/tests/components/ltr501/common.yaml b/tests/components/ltr501/common.yaml index 77c6f13739..406450f8c5 100644 --- a/tests/components/ltr501/common.yaml +++ b/tests/components/ltr501/common.yaml @@ -1,5 +1,6 @@ sensor: - platform: ltr501 + i2c_id: i2c_bus address: 0x23 type: ALS_PS gain: 1X diff --git a/tests/components/ltr_als_ps/common.yaml b/tests/components/ltr_als_ps/common.yaml index edad4b2e4f..6c555eeb74 100644 --- a/tests/components/ltr_als_ps/common.yaml +++ b/tests/components/ltr_als_ps/common.yaml @@ -1,5 +1,6 @@ sensor: - platform: ltr_als_ps + i2c_id: i2c_bus address: 0x23 gain: 1x integration_time: 100ms diff --git a/tests/components/mdns/common-enabled-ethernet.yaml b/tests/components/mdns/common-ethernet.yaml similarity index 95% rename from tests/components/mdns/common-enabled-ethernet.yaml rename to tests/components/mdns/common-ethernet.yaml index bfa9321d43..95a5d0edc8 100644 --- a/tests/components/mdns/common-enabled-ethernet.yaml +++ b/tests/components/mdns/common-ethernet.yaml @@ -14,7 +14,6 @@ ethernet: mac_address: "02:AA:BB:CC:DD:01" mdns: - disabled: false services: - service: _test_service protocol: _tcp diff --git a/tests/components/mdns/common-enabled.yaml b/tests/components/mdns/common.yaml similarity index 92% rename from tests/components/mdns/common-enabled.yaml rename to tests/components/mdns/common.yaml index 8b3d81cf69..8d0154fe9d 100644 --- a/tests/components/mdns/common-enabled.yaml +++ b/tests/components/mdns/common.yaml @@ -3,7 +3,6 @@ wifi: password: password1 mdns: - disabled: false services: - service: _test_service protocol: _tcp diff --git a/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml b/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml deleted file mode 100644 index f84a0bc276..0000000000 --- a/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled-ethernet.yaml diff --git a/tests/components/mdns/test-enabled.esp32-idf.yaml b/tests/components/mdns/test-enabled.esp32-idf.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/mdns/test-enabled.esp32-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.esp8266-ard.yaml b/tests/components/mdns/test-enabled.esp8266-ard.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/mdns/test-enabled.esp8266-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.rp2040-ard.yaml b/tests/components/mdns/test-enabled.rp2040-ard.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/mdns/test-enabled.rp2040-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-ethernet.rp2040-ard.yaml b/tests/components/mdns/test-ethernet.rp2040-ard.yaml new file mode 100644 index 0000000000..4c2dfce1e3 --- /dev/null +++ b/tests/components/mdns/test-ethernet.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + mdns: !include common-ethernet.yaml diff --git a/tests/components/mdns/test.esp32-idf.yaml b/tests/components/mdns/test.esp32-idf.yaml new file mode 100644 index 0000000000..c5e8cf7aea --- /dev/null +++ b/tests/components/mdns/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + mdns: !include common.yaml diff --git a/tests/components/mdns/test.esp8266-ard.yaml b/tests/components/mdns/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c5e8cf7aea --- /dev/null +++ b/tests/components/mdns/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + mdns: !include common.yaml diff --git a/tests/components/mdns/test.rp2040-ard.yaml b/tests/components/mdns/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c5e8cf7aea --- /dev/null +++ b/tests/components/mdns/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + mdns: !include common.yaml diff --git a/tests/components/mdns/validate-disabled.esp32-idf.yaml b/tests/components/mdns/validate-disabled.esp32-idf.yaml new file mode 100644 index 0000000000..7d76bf4de9 --- /dev/null +++ b/tests/components/mdns/validate-disabled.esp32-idf.yaml @@ -0,0 +1,7 @@ +# Disabling mdns compiles no mdns code, so config validation is enough. +wifi: + ssid: MySSID + password: password1 + +mdns: + disabled: true diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml index fb5fb39eb8..1ae9ea4a4d 100644 --- a/tests/components/opentherm/common.yaml +++ b/tests/components/opentherm/common.yaml @@ -25,6 +25,8 @@ opentherm: then: - lambda: |- ESP_LOGW("OT", ">> Sending message %d", x.id); + if (x.id == 0) + x.valueHB |= 0x01; before_process_response: then: - lambda: |- diff --git a/tests/components/ota/common.yaml b/tests/components/ota/common.yaml index 1433dada1f..af8cf66708 100644 --- a/tests/components/ota/common.yaml +++ b/tests/components/ota/common.yaml @@ -1,11 +1,6 @@ -wifi: - ssid: MySSID - password: password1 - ota: - platform: esphome password: "superlongpasswordthatnoonewillknow" - port: 3286 on_begin: then: - logger.log: "OTA start" diff --git a/tests/components/ota/test-api_key_offer.esp8266-ard.yaml b/tests/components/ota/test-api_key_offer.esp8266-ard.yaml deleted file mode 100644 index ecda625521..0000000000 --- a/tests/components/ota/test-api_key_offer.esp8266-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - ota: !include api_key_offer.yaml diff --git a/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml b/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml deleted file mode 100644 index 4709a9e45c..0000000000 --- a/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - ota: !include api_runtime_key.yaml diff --git a/tests/components/ota/test-partition_access.esp32-idf.yaml b/tests/components/ota/test-partition_access.esp32-idf.yaml deleted file mode 100644 index 0cbf854952..0000000000 --- a/tests/components/ota/test-partition_access.esp32-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -ota: - - platform: esphome - allow_partition_access: true - -<<: !include common.yaml diff --git a/tests/components/ota/test.bk72xx-ard.yaml b/tests/components/ota/test.bk72xx-ard.yaml index dade44d145..3b32457cf4 100644 --- a/tests/components/ota/test.bk72xx-ard.yaml +++ b/tests/components/ota/test.bk72xx-ard.yaml @@ -1 +1,3 @@ -<<: !include common.yaml +packages: + wifi: !include wifi.yaml + ota: !include common.yaml diff --git a/tests/components/ota/test.esp32-ard.yaml b/tests/components/ota/test.esp32-ard.yaml index dade44d145..3b32457cf4 100644 --- a/tests/components/ota/test.esp32-ard.yaml +++ b/tests/components/ota/test.esp32-ard.yaml @@ -1 +1,3 @@ -<<: !include common.yaml +packages: + wifi: !include wifi.yaml + ota: !include common.yaml diff --git a/tests/components/ota/test.esp32-idf.yaml b/tests/components/ota/test.esp32-idf.yaml index dade44d145..7f25fafadb 100644 --- a/tests/components/ota/test.esp32-idf.yaml +++ b/tests/components/ota/test.esp32-idf.yaml @@ -1 +1,8 @@ -<<: !include common.yaml +packages: + wifi: !include wifi.yaml + ota: !include common.yaml + +# Final validation merges this into the common block +ota: + - platform: esphome + allow_partition_access: true diff --git a/tests/components/ota/test.esp8266-ard.yaml b/tests/components/ota/test.esp8266-ard.yaml index dade44d145..3b32457cf4 100644 --- a/tests/components/ota/test.esp8266-ard.yaml +++ b/tests/components/ota/test.esp8266-ard.yaml @@ -1 +1,3 @@ -<<: !include common.yaml +packages: + wifi: !include wifi.yaml + ota: !include common.yaml diff --git a/tests/components/ota/test.host.yaml b/tests/components/ota/test.host.yaml index ae7c4d0add..fdfbf47139 100644 --- a/tests/components/ota/test.host.yaml +++ b/tests/components/ota/test.host.yaml @@ -1,4 +1,2 @@ -<<: !include common.yaml - -#host platform does not support wifi / network is automatically included -wifi: !remove +packages: + ota: !include common.yaml diff --git a/tests/components/ota/test.rp2040-ard.yaml b/tests/components/ota/test.rp2040-ard.yaml index dade44d145..3b32457cf4 100644 --- a/tests/components/ota/test.rp2040-ard.yaml +++ b/tests/components/ota/test.rp2040-ard.yaml @@ -1 +1,3 @@ -<<: !include common.yaml +packages: + wifi: !include wifi.yaml + ota: !include common.yaml diff --git a/tests/components/ota/test-empty_password.esp8266-ard.yaml b/tests/components/ota/validate-empty_password.esp8266-ard.yaml similarity index 100% rename from tests/components/ota/test-empty_password.esp8266-ard.yaml rename to tests/components/ota/validate-empty_password.esp8266-ard.yaml diff --git a/tests/components/ota/test-encryption_inherit.esp8266-ard.yaml b/tests/components/ota/validate-encryption_inherit.esp8266-ard.yaml similarity index 100% rename from tests/components/ota/test-encryption_inherit.esp8266-ard.yaml rename to tests/components/ota/validate-encryption_inherit.esp8266-ard.yaml diff --git a/tests/components/mdns/test-disabled.esp32-idf.yaml b/tests/components/ota/wifi.yaml similarity index 64% rename from tests/components/mdns/test-disabled.esp32-idf.yaml rename to tests/components/ota/wifi.yaml index 07c70bf248..ac662302fc 100644 --- a/tests/components/mdns/test-disabled.esp32-idf.yaml +++ b/tests/components/ota/wifi.yaml @@ -1,6 +1,3 @@ wifi: ssid: MySSID password: password1 - -mdns: - disabled: true diff --git a/tests/components/power_supply/test_setup_priority.cpp b/tests/components/power_supply/test_setup_priority.cpp index 401fc72654..736fdf99b0 100644 --- a/tests/components/power_supply/test_setup_priority.cpp +++ b/tests/components/power_supply/test_setup_priority.cpp @@ -1,3 +1,4 @@ +#ifdef USE_HOST #include #include "esphome/components/power_supply/power_supply.h" @@ -6,28 +7,9 @@ namespace esphome::power_supply::testing { -// Minimal dummy internal GPIO pin implementation for testing -class DummyInternalPin : public InternalGPIOPin { - public: - DummyInternalPin() = default; - void setup() override {} - void pin_mode(esphome::gpio::Flags) override {} - esphome::gpio::Flags get_flags() const override { return esphome::gpio::FLAG_NONE; } - bool digital_read() override { return false; } - void digital_write(bool) override {} - void detach_interrupt() const override {} - ISRInternalGPIOPin to_isr() const override { return ISRInternalGPIOPin(); } - uint8_t get_pin() const override { return 0; } - bool is_inverted() const override { return false; } - - protected: - // Implement protected attach_interrupt required by InternalGPIOPin - void attach_interrupt(void (*func)(void *), void *arg, esphome::gpio::InterruptType type) const override {} -}; - TEST(PowerSupply, HasHigherPriorityThanBusWhenInternalAndEnableOnBoot) { power_supply::PowerSupply ps; - DummyInternalPin pin; + InternalGPIOPin pin; ps.set_pin(&pin); ps.set_enable_on_boot(true); @@ -37,7 +19,7 @@ TEST(PowerSupply, HasHigherPriorityThanBusWhenInternalAndEnableOnBoot) { TEST(PowerSupply, FallsBackToIOWhenNotEnableOnBoot) { power_supply::PowerSupply ps; - DummyInternalPin pin; + InternalGPIOPin pin; ps.set_pin(&pin); ps.set_enable_on_boot(false); @@ -45,3 +27,4 @@ TEST(PowerSupply, FallsBackToIOWhenNotEnableOnBoot) { } } // namespace esphome::power_supply::testing +#endif // USE_HOST diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index 5d12dc68d4..4dadec6a01 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -103,6 +103,11 @@ on_nexa: - logger.log: format: "on_nexa: %lu %u %u %u %u" args: ["long(x.device)", "x.group", "x.state", "x.channel", "x.level"] +on_onkyori: + then: + - logger.log: + format: "on_onkyori: 0x%03X" + args: ["x.data"] on_panasonic: then: - logger.log: diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index bdb5f2eea2..776a03e680 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -252,6 +252,16 @@ button: channel: 1 button: 1 check: 1 + - platform: template + name: OnkyoRI + on_press: + remote_transmitter.transmit_onkyori: + data: 0xD9 + - platform: template + name: OnkyoRI 12-bit + on_press: + remote_transmitter.transmit_onkyori: + data: 0x170 - platform: template name: Toto on_press: diff --git a/tests/components/sendspin/common-pairing.yaml b/tests/components/sendspin/common-pairing.yaml index cd9cd82bec..500f888ac7 100644 --- a/tests/components/sendspin/common-pairing.yaml +++ b/tests/components/sendspin/common-pairing.yaml @@ -10,5 +10,10 @@ wifi: sendspin: id: sendspin_hub_id static_pairing_code: "01234567" - # Covers the hub's unpaired access option. + # Covers the hub's unpaired access option, which is only valid without an unpaired access switch. unpaired_access: false + +# Builds the API request for the Sendspin pairing token. +api: + encryption: + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= diff --git a/tests/components/sendspin/common-switch.yaml b/tests/components/sendspin/common-switch.yaml index 01e2f0b02b..dbf260286b 100644 --- a/tests/components/sendspin/common-switch.yaml +++ b/tests/components/sendspin/common-switch.yaml @@ -5,3 +5,6 @@ switch: - platform: sendspin name: "Sendspin Enabled" type: enabled + - platform: sendspin + name: "Sendspin Unpaired Access" + type: unpaired_access diff --git a/tests/components/spd2010/common.yaml b/tests/components/spd2010/common.yaml new file mode 100644 index 0000000000..0692137638 --- /dev/null +++ b/tests/components/spd2010/common.yaml @@ -0,0 +1,25 @@ +display: + - platform: ssd1306_i2c + id: spd2010_display + model: SSD1306_128X64 + +touchscreen: + - platform: spd2010 + id: spd2010_touchscreen + display: spd2010_display + interrupt_pin: ${interrupt_pin} + reset_pin: ${reset_pin} + - platform: spd2010 + id: spd2010_polling_touchscreen + display: spd2010_display + address: 0x54 + update_interval: 100ms + calibration: + x_min: 0 + x_max: 479 + y_min: 0 + y_max: 479 + transform: + swap_xy: true + mirror_x: true + mirror_y: false diff --git a/tests/components/spd2010/test.esp32-idf.yaml b/tests/components/spd2010/test.esp32-idf.yaml new file mode 100644 index 0000000000..ca2be87d29 --- /dev/null +++ b/tests/components/spd2010/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + interrupt_pin: "20" + reset_pin: "21" + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + spd2010: !include common.yaml diff --git a/tests/components/time/test-sync.esp8266-ard.yaml b/tests/components/time/test-sync.esp8266-ard.yaml new file mode 100644 index 0000000000..6240fcd73e --- /dev/null +++ b/tests/components/time/test-sync.esp8266-ard.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + on_time_sync: + - logger.log: Time synchronized diff --git a/tests/components/tuya/common.yaml b/tests/components/tuya/common.yaml index f52d47e7a0..1d0fc473ac 100644 --- a/tests/components/tuya/common.yaml +++ b/tests/components/tuya/common.yaml @@ -10,7 +10,50 @@ tuya: - sensor_datapoint: 6 datapoint_type: raw then: - - logger.log: Datapoint 6 updated + - logger.log: + format: "Datapoint 6 updated, %u bytes" + args: [x.size()] + - sensor_datapoint: 1 + then: + - logger.log: + format: "Datapoint %u updated" + args: [x.id] + - sensor_datapoint: 1 + datapoint_type: bool + then: + - logger.log: + format: "Datapoint 1 is %s" + args: ['x ? "on" : "off"'] + - sensor_datapoint: 3 + datapoint_type: int + then: + - logger.log: + format: "Datapoint 3 is %d" + args: [x] + - sensor_datapoint: 3 + datapoint_type: uint + then: + - logger.log: + format: "Datapoint 3 is %u" + args: [x] + - sensor_datapoint: 7 + datapoint_type: string + then: + - logger.log: + format: "Datapoint 7 is %s" + args: [x.c_str()] + - sensor_datapoint: 8 + datapoint_type: enum + then: + - logger.log: + format: "Datapoint 8 is %u" + args: [x] + - sensor_datapoint: 9 + datapoint_type: bitmask + then: + - logger.log: + format: "Datapoint 9 is %u" + args: [x] binary_sensor: - platform: tuya diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 9550de911b..3bb87aa3cc 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -14,7 +14,12 @@ esphome: uart: - id: uart_id - tx_pin: 17 + tx_pin: + number: 17 + mode: + pullup: true + output: true + hold_state: true rx_pin: 16 flow_control_pin: 4 baud_rate: 9600 diff --git a/tests/components/uart/uart_paced_write_room.cpp b/tests/components/uart/uart_paced_write_room.cpp new file mode 100644 index 0000000000..ed64695dd0 --- /dev/null +++ b/tests/components/uart/uart_paced_write_room.cpp @@ -0,0 +1,62 @@ +#include + +#include "common.h" +#include "esphome/core/application.h" + +namespace esphome::uart::testing { + +class RoomUART : public MockUARTComponent { + public: + size_t available_for_write() override { return this->room; } + size_t room{SIZE_MAX}; +}; + +class PacedWriteRoom : public ::testing::Test { + protected: + void SetUp() override { + App.set_loop_interval(16); + this->uart_.set_baud_rate(9600); + } + void TearDown() override { + App.set_loop_interval(16); + this->at_(0); + } + // Publishes now_ms as the loop start time, as Application::loop() does. + void at_(uint32_t now_ms) { LoopBlockingGuard dispatch{nullptr, nullptr, now_ms}; } + + RoomUART uart_; +}; + +TEST_F(PacedWriteRoom, KnownRoomIsReturned) { + this->uart_.room = 7; + this->at_(1000); + EXPECT_EQ(this->uart_.paced_write_room(0), 7u); + this->uart_.room = 0; + EXPECT_EQ(this->uart_.paced_write_room(0), 0u); +} + +TEST_F(PacedWriteRoom, UnknownRoomTakesOneLoopInterval) { + this->at_(1000); + // 9600 baud at 10 bits per byte for 16 ms. + EXPECT_EQ(this->uart_.paced_write_room(0), 15u); +} + +TEST_F(PacedWriteRoom, EarlyPassTakesTheTimeSinceTheLastWrite) { + this->at_(1005); + EXPECT_EQ(this->uart_.paced_write_room(1000), 4u); +} + +TEST_F(PacedWriteRoom, AtLeastOneByte) { + this->at_(1000); + EXPECT_EQ(this->uart_.paced_write_room(1000), 1u); +} + +TEST_F(PacedWriteRoom, SpanCapsAtFourSeconds) { + App.set_loop_interval(10000); + this->uart_.set_baud_rate(300); + this->at_(6000); + // 30 bytes/s for 4 s. + EXPECT_EQ(this->uart_.paced_write_room(0), 120u); +} + +} // namespace esphome::uart::testing diff --git a/tests/components/valve/common-triggers.yaml b/tests/components/valve/common-triggers.yaml new file mode 100644 index 0000000000..452cc2eac8 --- /dev/null +++ b/tests/components/valve/common-triggers.yaml @@ -0,0 +1,9 @@ +valve: + - platform: template + name: Trigger Test Valve + id: trigger_test_valve + optimistic: true + on_open: + - logger.log: Valve fully open + on_closed: + - logger.log: Valve fully closed diff --git a/tests/components/valve/test-triggers.esp8266-ard.yaml b/tests/components/valve/test-triggers.esp8266-ard.yaml new file mode 100644 index 0000000000..63ca1042de --- /dev/null +++ b/tests/components/valve/test-triggers.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + valve: !include common.yaml + triggers: !include common-triggers.yaml diff --git a/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.cpp b/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.cpp index b58f1c9adc..71182a46ad 100644 --- a/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.cpp +++ b/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.cpp @@ -16,4 +16,16 @@ void WakeTestComponent::start_async_wake() { }).detach(); } +void WakeTestComponent::start_async_timeout(uint32_t delay_ms) { + const uint32_t start_time = millis(); + std::thread([this, start_time, delay_ms] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + const int start_loop_count = this->get_loop_count(); + this->set_timeout(delay_ms, [this, start_time, start_loop_count, delay_ms] { + ESP_LOGI(TAG, "SCHEDULER_WAKE_RESULT delay=%u elapsed=%u loop_delta=%d", static_cast(delay_ms), + static_cast(millis() - start_time), this->get_loop_count() - start_loop_count); + }); + }).detach(); +} + } // namespace esphome::wake_test_component diff --git a/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.h b/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.h index c8e4e0a89f..a1f06f8998 100644 --- a/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.h +++ b/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.h @@ -18,6 +18,10 @@ class WakeTestComponent : public Component { // loop_interval_ has been raised high enough to gate it off otherwise. void start_async_wake(); + // Spawn a detached thread that inserts a timeout (or a defer when delay_ms + // is 0) while the main loop sleeps, and report how soon it ran. + void start_async_timeout(uint32_t delay_ms); + float get_setup_priority() const override { return setup_priority::DATA; } protected: diff --git a/tests/integration/fixtures/scheduler_background_wake.yaml b/tests/integration/fixtures/scheduler_background_wake.yaml new file mode 100644 index 0000000000..78e6864eb2 --- /dev/null +++ b/tests/integration/fixtures/scheduler_background_wake.yaml @@ -0,0 +1,31 @@ +esphome: + name: scheduler-background-wake + on_boot: + priority: -100 + then: + - lambda: |- + App.set_loop_interval(5000); + +host: +api: +logger: + level: INFO + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [wake_test_component] + +wake_test_component: + id: wake_test + +button: + - platform: template + name: Start Scheduler Timeout + on_press: + - lambda: id(wake_test)->start_async_timeout(100); + - platform: template + name: Start Scheduler Defer + on_press: + - lambda: id(wake_test)->start_async_timeout(0); diff --git a/tests/integration/fixtures/sensor_timeout_throttle_filter.yaml b/tests/integration/fixtures/sensor_timeout_throttle_filter.yaml new file mode 100644 index 0000000000..5f1f9556e0 --- /dev/null +++ b/tests/integration/fixtures/sensor_timeout_throttle_filter.yaml @@ -0,0 +1,67 @@ +esphome: + name: test-timeout-throttle + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +sensor: + - platform: template + name: "Source" + id: source_sensor + accuracy_decimals: 2 + + # The default radar pair; codegen merges it into one TimeoutThrottleFilter + - platform: copy + source_id: source_sensor + name: "Merged" + filters: + - timeout: + timeout: 200ms + value: last + - throttle_with_priority: 200ms + + # Same chain with an identity filter in between, which keeps the two filter objects + - platform: copy + source_id: source_sensor + name: "Reference" + filters: + - timeout: + timeout: 200ms + value: last + - multiply: 1.0 + - throttle_with_priority: 200ms + +script: + - id: drive + then: + # Burst faster than the period: first passes, the rest are dropped, the timeout republishes the last + - sensor.template.publish: {id: source_sensor, state: 1.0} + - delay: 30ms + - sensor.template.publish: {id: source_sensor, state: 2.0} + - delay: 30ms + - sensor.template.publish: {id: source_sensor, state: 3.0} + - delay: 450ms + # NaN inside a burst always passes + - sensor.template.publish: {id: source_sensor, state: 4.0} + - delay: 30ms + - sensor.template.publish: {id: source_sensor, state: !lambda "return NAN;"} + - delay: 30ms + - sensor.template.publish: {id: source_sensor, state: 5.0} + - delay: 450ms + # Values right around the period + - sensor.template.publish: {id: source_sensor, state: 6.0} + - delay: 250ms + - sensor.template.publish: {id: source_sensor, state: 7.0} + - delay: 190ms + - sensor.template.publish: {id: source_sensor, state: 8.0} + - delay: 600ms + - sensor.template.publish: {id: source_sensor, state: 99.0} + +button: + - platform: template + name: "Drive" + on_press: + - script.execute: drive diff --git a/tests/integration/fixtures/status_momentary.yaml b/tests/integration/fixtures/status_momentary.yaml new file mode 100644 index 0000000000..424a73ce3b --- /dev/null +++ b/tests/integration/fixtures/status_momentary.yaml @@ -0,0 +1,40 @@ +esphome: + name: status-momentary-test + +host: +api: + actions: + - action: momentary_warning + variables: + length: int + then: + - lambda: "id(probe)->status_momentary_warning(length);" + - action: momentary_error + variables: + length: int + then: + - lambda: "id(probe)->status_momentary_error(length);" + - action: momentary_warning_message + then: + - lambda: 'id(probe)->status_momentary_warning(LOG_STR("probe warning reason"), 300);' + - action: momentary_error_message + then: + - lambda: 'id(probe)->status_momentary_error(LOG_STR("probe error reason"), 300);' + +logger: + +sensor: + - platform: template + id: probe + update_interval: 24h + lambda: return 1.0; + +binary_sensor: + - platform: template + name: Probe Warning + id: probe_warning + lambda: return id(probe)->status_has_warning(); + - platform: template + name: Probe Error + id: probe_error + lambda: return id(probe)->status_has_error(); diff --git a/tests/integration/test_scheduler_background_wake.py b/tests/integration/test_scheduler_background_wake.py new file mode 100644 index 0000000000..fa7d34f494 --- /dev/null +++ b/tests/integration/test_scheduler_background_wake.py @@ -0,0 +1,84 @@ +"""A background scheduler insertion must interrupt the loop's current sleep. + +Covers both set_timeout and defer: a zero-delay insert from another thread +takes the separate defer queue on multi-threaded builds. +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import ButtonInfo +import pytest + +from .state_utils import require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_background_wake( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + results: dict[int, asyncio.Future[tuple[int, int]]] = { + 100: loop.create_future(), + 0: loop.create_future(), + } + + def on_log_line(line: str) -> None: + match = re.search( + r"SCHEDULER_WAKE_RESULT delay=(\d+) elapsed=(\d+) loop_delta=(-?\d+)", + line, + ) + if match is None: + return + result = results[int(match.group(1))] + if not result.done(): + result.set_result((int(match.group(2)), int(match.group(3)))) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-background-wake" + entities, _ = await client.list_entities_services() + for object_id, delay in ( + ("start_scheduler_timeout", 100), + ("start_scheduler_defer", 0), + ): + start_button = require_entity( + entities, + object_id, + ButtonInfo, + description=f"{object_id} button", + ) + client.button_command(start_button.key) + + try: + elapsed, loop_delta = await asyncio.wait_for( + results[delay], timeout=10.0 + ) + except TimeoutError: + pytest.fail(f"background insert with delay={delay} did not fire") + + assert elapsed < 1000, ( + f"background insert with delay={delay} should interrupt the " + f"five-second sleep; it fired after {elapsed}ms" + ) + assert loop_delta == 0, ( + f"scheduler-only wake must not run component loops; observed " + f"loop_delta={loop_delta} for delay={delay}" + ) diff --git a/tests/integration/test_sensor_timeout_throttle_filter.py b/tests/integration/test_sensor_timeout_throttle_filter.py new file mode 100644 index 0000000000..739421ca3a --- /dev/null +++ b/tests/integration/test_sensor_timeout_throttle_filter.py @@ -0,0 +1,61 @@ +"""The merged timeout + throttle_with_priority filter publishes exactly what the two filter chain does.""" + +from __future__ import annotations + +import asyncio +import math + +from aioesphomeapi import ButtonInfo, EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_timeout_throttle_filter( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + loop = asyncio.get_running_loop() + values: dict[str, list[float]] = {"merged": [], "reference": []} + done = {name: loop.create_future() for name in values} + + def on_state(state: EntityState) -> None: + if not isinstance(state, SensorState): + return + name = key_to_sensor.get(state.key) + if name not in values: + return + values[name].append(math.nan if state.missing_state else state.state) + # 99 is published last and republished once by the timeout + if values[name].count(99.0) == 2 and not done[name].done(): + done[name].set_result(True) + + async with run_compiled(yaml_config), api_client_connected() as client: + entities, _ = await client.list_entities_services() + key_to_sensor = build_key_to_entity_mapping( + entities, {"merged": "Merged", "reference": "Reference"} + ) + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + button = next( + e.key for e in entities if isinstance(e, ButtonInfo) and e.name == "Drive" + ) + client.button_command(button) + try: + await asyncio.wait_for(asyncio.gather(*done.values()), timeout=5.0) + except TimeoutError: + pytest.fail(f"Timed out: {values}") + + merged, reference = values["merged"], values["reference"] + assert len(merged) == len(reference), (merged, reference) + for got, want in zip(merged, reference, strict=True): + assert (math.isnan(got) and math.isnan(want)) or got == want, ( + merged, + reference, + ) + assert any(math.isnan(v) for v in merged) diff --git a/tests/integration/test_status_momentary.py b/tests/integration/test_status_momentary.py new file mode 100644 index 0000000000..0bf3a820be --- /dev/null +++ b/tests/integration/test_status_momentary.py @@ -0,0 +1,87 @@ +"""Integration test for status_momentary_warning/error without a name.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import BinarySensorState, EntityState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_status_momentary( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + loop = asyncio.get_running_loop() + warning_logged = loop.create_future() + error_logged = loop.create_future() + + def on_log_line(line: str) -> None: + if ( + "set Warning flag: probe warning reason" in line + and not warning_logged.done() + ): + warning_logged.set_result(True) + if "set Error flag: probe error reason" in line and not error_logged.done(): + error_logged.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, services = await client.list_entities_services() + svc = {s.name: s for s in services} + keys = {e.object_id: e.key for e in entities} + warning_key = keys["probe_warning"] + error_key = keys["probe_error"] + + changes: asyncio.Queue[tuple[int, bool, float]] = asyncio.Queue() + + def on_state(state: EntityState) -> None: + if isinstance(state, BinarySensorState): + changes.put_nowait((state.key, state.state, loop.time())) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + assert initial_state_helper.initial_states[warning_key].state is False + assert initial_state_helper.initial_states[error_key].state is False + + async def next_change(key: int) -> tuple[bool, float]: + while True: + k, value, when = await asyncio.wait_for(changes.get(), timeout=5) + if k == key: + return value, when + + # The flag sets at once and clears after the length + for service, key in ( + ("momentary_warning", warning_key), + ("momentary_error", error_key), + ): + start = loop.time() + await client.execute_service(svc[service], {"length": 300}) + assert (await next_change(key))[0] is True + value, cleared = await next_change(key) + assert value is False + assert cleared - start >= 0.25 + + # A second call restarts the timeout instead of adding a second one + start = loop.time() + await client.execute_service(svc["momentary_warning"], {"length": 1000}) + assert (await next_change(warning_key))[0] is True + await asyncio.sleep(0.6) + await client.execute_service(svc["momentary_warning"], {"length": 1000}) + value, cleared = await next_change(warning_key) + assert value is False + assert cleared - start >= 1.4 + + # A message replaces "unspecified" in the log line + await client.execute_service(svc["momentary_warning_message"], {}) + await asyncio.wait_for(warning_logged, timeout=5) + await client.execute_service(svc["momentary_error_message"], {}) + await asyncio.wait_for(error_logged, timeout=5) diff --git a/tests/script/test_ci_custom.py b/tests/script/test_ci_custom.py index 4459631293..f303d19bb0 100644 --- a/tests/script/test_ci_custom.py +++ b/tests/script/test_ci_custom.py @@ -442,3 +442,97 @@ def test_log_tag_as_string_flags_string_uses(line: str) -> None: ) def test_log_tag_as_string_ignores_logging(line: str) -> None: assert not ci_custom.lint_log_tag_as_string("test.cpp", line + "\n") + + +@pytest.mark.parametrize( + ("content", "flagged"), + [ + ("auto s = format_hex_pretty(data, len);\n", True), + # Explicit template arguments must not hide the call + ("auto s = format_hex_pretty(v, '\\0', false);\n", True), + ("auto s = format_hex>(arr);\n", True), + ( + "auto s = format_hex, uint64_t>>(v);\n", + True, + ), + ("auto s = format_hex < 3;\n", False), + ("format_hex_pretty_to(buf, sizeof(buf), data, len);\n", False), + ], +) +def test_heap_allocating_helpers_detection(content: str, flagged: bool) -> None: + errs = ci_custom.lint_no_heap_allocating_helpers( + Path("esphome/components/x/x.cpp"), content + ) + assert bool(errs) is flagged + + +# --- rule: every component __init__.py defines DOMAIN as its own name --- + + +def _domain_check() -> dict: + return next( + c + for c in ci_custom.LINT_CONTENT_CHECKS + if c["func"] is ci_custom.lint_component_domain + ) + + +def _lint_domain(fname: str, content: str) -> str | None: + return ci_custom.run_check(_domain_check(), fname, Path(fname), content) + + +@pytest.mark.parametrize( + "content", + [ + 'CODEOWNERS = ["@x"]\nDOMAIN = "uart"\n', + 'DOMAIN: str = "uart"\n', + ], +) +def test_domain_matching_component_name_passes(content: str) -> None: + assert _lint_domain("esphome/components/uart/__init__.py", content) is None + + +def test_domain_leaves_syntax_errors_to_the_python_linters() -> None: + assert _lint_domain("esphome/components/uart/__init__.py", "def (\n") is None + + +@pytest.mark.parametrize( + "content", + [ + 'CODEOWNERS = ["@x"]\n', + 'DOMAIN = "spi"\n', + "DOMAIN = CONF_UART\n", + '"""Docstring.\n\nDOMAIN = "uart"\n"""\n', + 'def f() -> None:\n DOMAIN = "uart"\n', + 'OTHER = "uart"\n', + "DOMAIN: str\n", + ], +) +def test_domain_missing_or_wrong_is_flagged(content: str) -> None: + err = _lint_domain("esphome/components/uart/__init__.py", content) + assert err is not None + assert 'DOMAIN = "uart"' in err + + +@pytest.mark.parametrize( + "fname", + [ + "esphome/components/uart/sensor/__init__.py", + "esphome/components/uart/sensor.py", + "esphome/core/__init__.py", + ], +) +def test_domain_ignores_files_other_than_component_init(fname: str) -> None: + assert _lint_domain(fname, "") is None + + +def test_every_component_defines_its_domain() -> None: + components = SCRIPT_DIR.parent / "esphome" / "components" + missing = [ + init.parent.name + for init in sorted(components.glob("*/__init__.py")) + if ci_custom.lint_component_domain( + init.relative_to(SCRIPT_DIR.parent), init.read_text(encoding="utf-8") + ) + ] + assert missing == [] diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 7e9b09b2b9..e63e0849de 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -2613,8 +2613,8 @@ def test_component_batching_beta_branch_groups_evenly( For beta/release branches, all components should be groupable (not isolated), so they share one grouped build per runner and spread evenly. """ - # Create 120 test components with test files - component_names = [f"comp_{i:03d}" for i in range(120)] + # Create 200 test components with test files + component_names = [f"comp_{i:03d}" for i in range(200)] tests_dir = tmp_path / "tests" / "components" for comp in component_names: @@ -2675,15 +2675,15 @@ def test_component_batching_beta_branch_groups_evenly( assert "component_test_batches" in output batches = output["component_test_batches"] - # One grouped esp32-idf build plus 5 s per extra component is 640 s, - # so two runners of 60 components each - assert [len(batch["components"].split()) for batch in batches] == [60, 60] + # One grouped esp32-idf build plus 3 s per extra component is 652 s, + # so two runners of 100 components each + assert [len(batch["components"].split()) for batch in batches] == [100, 100] - # Verify all 120 components are in batches + # Verify all 200 components are in batches all_components = [] for batch in batches: all_components.extend(batch["components"].split()) - assert len(all_components) == 120 + assert len(all_components) == 200 assert set(all_components) == set(component_names) diff --git a/tests/script/test_split_components_for_ci.py b/tests/script/test_split_components_for_ci.py index 88a1a17b33..718d754f01 100644 --- a/tests/script/test_split_components_for_ci.py +++ b/tests/script/test_split_components_for_ci.py @@ -35,13 +35,46 @@ def test_build_seconds() -> None: """Known platforms use the table; nrf52 boards share one estimate.""" build_seconds = split_components_for_ci.build_seconds assert build_seconds("host") == 10 - assert build_seconds("esp32-idf") == 45 + assert build_seconds("esp32-idf") == 55 assert ( build_seconds("nrf52-xiao-ble") == split_components_for_ci.NRF52_BUILD_SECONDS ) assert ( - build_seconds("esp32-s3-idf") == split_components_for_ci.DEFAULT_BUILD_SECONDS + build_seconds("esp32-c5-idf") == split_components_for_ci.DEFAULT_BUILD_SECONDS ) + assert ( + build_seconds("esp32-c3-ard") + == split_components_for_ci.ESP32_ARDUINO_BUILD_SECONDS + ) + + +def test_heavy_components(tests_dir: Path) -> None: + """A heavy component, or a test that enables one, costs its library time.""" + heavy_components = split_components_for_ci.heavy_components + heavy_seconds = split_components_for_ci.heavy_seconds + table = split_components_for_ci.HEAVY_COMPONENT_SECONDS + _add_component(tests_dir, "dht", ["test.esp32-idf.yaml"]) + _add_component(tests_dir, "micro_wake_word", ["test.esp32-idf.yaml"]) + assert heavy_components("dht", tests_dir / "dht/test.esp32-idf.yaml") == set() + assert heavy_components( + "micro_wake_word", tests_dir / "micro_wake_word/test.esp32-idf.yaml" + ) == {"micro_wake_word"} + assert heavy_seconds(frozenset({"micro_wake_word"})) == table["micro_wake_word"] + + display = tests_dir / "mipi_spi" + display.mkdir() + (display / "common.yaml").write_text("web_server:\n port: 80\n") + (display / "test-lvgl.esp32-s3-idf.yaml").write_text( + "packages:\n" + " mipi_spi: !include common.yaml\n" + "lvgl:\n pages: []\n" + "light:\n - platform: fastled_spi\n num_leds: 1\n" + ) + assert heavy_components("mipi_spi", display / "test-lvgl.esp32-s3-idf.yaml") == { + "lvgl", + "web_server", + "fastled_spi", + } def test_make_item_isolated_and_grouped(tests_dir: Path) -> None: @@ -61,29 +94,93 @@ def test_make_item_isolated_and_grouped(tests_dir: Path) -> None: isolated = split_components_for_ci._make_item( tests_dir, "comp", "isolated_comp", True ) - assert isolated.own_seconds == 45 + 10 + 45 + 50 + assert isolated.own_seconds == 55 + 10 + 55 + 45 assert isolated.grouped_builds == {} # The rp2040 variant has no base test to group with, so it always runs grouped = split_components_for_ci._make_item(tests_dir, "comp", "i2c", False) - assert grouped.own_seconds == 50 - assert grouped.grouped_builds == {("i2c", "esp32-idf"): 90, ("i2c", "host"): 10} + assert grouped.own_seconds == 45 + assert grouped.grouped_builds == { + ("i2c", "esp32-idf"): (110, frozenset()), + ("i2c", "host"): (10, frozenset()), + } + + +def test_make_item_heavy_component(tests_dir: Path) -> None: + """A heavy component's library cost follows it into every build.""" + _add_component(tests_dir, "lvgl", ["test.esp32-idf.yaml", "test-a.host.yaml"]) + extra = split_components_for_ci.HEAVY_COMPONENT_SECONDS["lvgl"] + + item = split_components_for_ci._make_item(tests_dir, "lvgl", "spi", False) + assert item.own_seconds == 10 + extra + assert item.grouped_builds == { + ("spi", "esp32-idf"): (55 + extra, frozenset({"lvgl"})) + } + + +def test_heavy_cost_is_per_platform(tests_dir: Path) -> None: + """A base test that enables a heavy component only charges its own platform.""" + comp = tests_dir / "web_server_idf" + comp.mkdir() + (comp / "test.esp32-idf.yaml").write_text("web_server:\n port: 80\n") + (comp / "test.host.yaml").write_text("logger:\n") + extra = split_components_for_ci.HEAVY_COMPONENT_SECONDS["web_server"] + + item = split_components_for_ci._make_item( + tests_dir, "web_server_idf", "none", False + ) + assert item.grouped_builds == { + ("none", "esp32-idf"): (55 + extra, frozenset({"web_server"})), + ("none", "host"): (10, frozenset()), + } def test_grouped_component_joins_existing_build() -> None: """A second member turns a lone build into a group that skips variants.""" item = split_components_for_ci._BatchItem + share = split_components_for_ci._GroupedShare batch = split_components_for_ci._Batch() - batch.add(item("a", 0, {("i2c", "esp32-idf"): 90})) - assert batch.seconds == 90 + batch.add(item("a", 0, {("i2c", "esp32-idf"): share(110, frozenset())})) + assert batch.seconds == 110 - other = item("b", 0, {("i2c", "esp32-idf"): 45, ("i2c", "host"): 10}) - grouped = 45 + split_components_for_ci.GROUPED_COMPONENT_SECONDS - assert batch.added_seconds(other) == grouped - 90 + 10 + other = item( + "b", + 0, + { + ("i2c", "esp32-idf"): share(55, frozenset()), + ("i2c", "host"): share(10, frozenset()), + }, + ) + grouped = 55 + split_components_for_ci.GROUPED_COMPONENT_SECONDS + assert batch.added_seconds(other) == grouped - 110 + 10 batch.add(other) assert batch.seconds == grouped + 10 +def test_heavy_component_charges_the_shared_build() -> None: + """A heavy member makes the whole grouped build slower, once per library.""" + item = split_components_for_ci._BatchItem + share = split_components_for_ci._GroupedShare + batch = split_components_for_ci._Batch() + extra = split_components_for_ci.HEAVY_COMPONENT_SECONDS["lvgl"] + per_member = split_components_for_ci.GROUPED_COMPONENT_SECONDS + lvgl = frozenset({"lvgl"}) + batch.add(item("a", 0, {("spi", "esp32-idf"): share(55, frozenset())})) + + heavy = item("lvgl", 0, {("spi", "esp32-idf"): share(55 + extra, lvgl)}) + grouped = 55 + per_member + extra + assert batch.added_seconds(heavy) == grouped - 55 + batch.add(heavy) + assert batch.seconds == grouped + + light = item("b", 0, {("spi", "esp32-idf"): share(55, frozenset())}) + assert batch.added_seconds(light) == per_member + + # A second member that also draws with lvgl compiles the library once + also_heavy = item("c", 0, {("spi", "esp32-idf"): share(55 + extra, lvgl)}) + assert batch.added_seconds(also_heavy) == per_member + + def test_isolated_components_balance_across_runners(tests_dir: Path) -> None: """Heavy isolated components spread out and light ones fill the gaps.""" platforms = ["esp32-idf", "esp8266-ard", "rp2040-ard", "bk72xx-ard"] @@ -138,7 +235,7 @@ def test_groupable_components_split_evenly(tests_dir: Path) -> None: components=names, tests_dir=tests_dir, target_seconds=100 ) - assert sorted(len(batch) for batch in batches) == [2, 2, 2, 3, 3] + assert sorted(len(batch) for batch in batches) == [3, 3, 3, 3] assert sorted(c for batch in batches for c in batch) == names @@ -149,7 +246,25 @@ def test_make_item_reads_tests_dir(tmp_path: Path) -> None: _add_component(other, "comp", ["test.esp32-idf.yaml"]) item = split_components_for_ci._make_item(other, "comp", "isolated_comp", True) - assert item.own_seconds == 45 + assert item.own_seconds == 55 + + +def test_balance_batches_keeps_partners_together() -> None: + """Two components sharing a build land on one runner even when the + lightest runner is the other one.""" + share = { + ("i2c", "esp32-idf"): split_components_for_ci._GroupedShare(55, frozenset()), + ("i2c", "esp8266-ard"): split_components_for_ci._GroupedShare(30, frozenset()), + } + items = [ + split_components_for_ci._BatchItem("partner_a", 0, dict(share)), + split_components_for_ci._BatchItem("partner_b", 0, dict(share)), + split_components_for_ci._BatchItem("alone", 80), + ] + + batches = split_components_for_ci.balance_batches(items, target_seconds=120) + + assert sorted(batches) == [["alone"], ["partner_a", "partner_b"]] def test_balance_batches_empty() -> None: diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index 2c95c61b37..9d92988da0 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -4,6 +4,7 @@ from __future__ import annotations import json import logging +import os from pathlib import Path from unittest.mock import patch @@ -241,9 +242,18 @@ def test_get_project_cmakelists_drops_empty_lwip_sources() -> None: def test_get_project_cmakelists_declares_map_as_link_byproduct() -> None: - """The link declares the map so size can build in the same ninja run.""" + """The map is a link byproduct only when IDF has not declared it itself.""" content = _render() - assert "BYPRODUCTS ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map" in content + assert ( + "get_source_file_property(esphome_map_declared\n" + " ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map GENERATED)" + ) in content + assert ( + "set(esphome_map_byproducts BYPRODUCTS " + "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map)" + ) in content + assert " ${esphome_map_byproducts}\n WORKING_DIRECTORY" in content + assert "BYPRODUCTS ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map\n" not in content def test_get_project_cmakelists_uses_supplied_builtin_components() -> None: @@ -350,17 +360,54 @@ def test_get_project_cmakelists_emits_exclude_components(tmp_path: Path) -> None assert "ESPHOME_PROJECT_BUILTIN_COMPONENTS esp_lcd" not in content -def test_get_project_cmakelists_minimal_emits_exclude_components() -> None: - """The discovery (minimal) write also excludes components so they never - register in project_description.json.""" +def test_get_project_cmakelists_minimal_excludes_only_managed_components( + tmp_path: Path, +) -> None: + """The discovery (minimal) write lets built-in components register so + the cached list serves every exclusion set; managed ones stay excluded.""" + (tmp_path / "components" / "unity").mkdir(parents=True) + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity", "espressif__esp_hosted"} + register_exclude_components_cmake_arg() + + with patch("esphome.espidf.toolchain._get_idf_path", return_value=tmp_path): + minimal = _render(minimal=True) + full = _render() + + assert 'set(EXCLUDE_COMPONENTS "espressif__esp_hosted")' in minimal + assert 'set(EXCLUDE_COMPONENTS "espressif__esp_hosted;unity")' in full + + +def test_get_project_cmakelists_minimal_keeps_exclusions_for_custom_idf_path( + tmp_path: Path, +) -> None: + """Nothing is cached for a user-supplied IDF_PATH, so its discovery write + keeps every exclusion.""" + (tmp_path / "components" / "unity").mkdir(parents=True) CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity"} register_exclude_components_cmake_arg() - content = _render(minimal=True) + with ( + patch("esphome.espidf.toolchain._get_idf_path", return_value=tmp_path), + patch.dict(os.environ, {"IDF_PATH": str(tmp_path)}), + ): + content = _render(minimal=True) assert 'set(EXCLUDE_COMPONENTS "unity")' in content +def test_get_project_cmakelists_minimal_drops_exclude_line_when_all_builtin( + tmp_path: Path, +) -> None: + (tmp_path / "components" / "unity").mkdir(parents=True) + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity"} + register_exclude_components_cmake_arg() + + with patch("esphome.espidf.toolchain._get_idf_path", return_value=tmp_path): + content = _render(minimal=True) + + assert "EXCLUDE_COMPONENTS" not in content + + def test_get_project_cmakelists_no_exclude_components_line_when_empty() -> None: """No EXCLUDE_COMPONENTS line at all when nothing is excluded.""" register_exclude_components_cmake_arg() diff --git a/tests/unit_tests/components/api/test_api_proto.py b/tests/unit_tests/components/api/test_api_proto.py index 0f25de7c08..ca3c0ce7b5 100644 --- a/tests/unit_tests/components/api/test_api_proto.py +++ b/tests/unit_tests/components/api/test_api_proto.py @@ -68,8 +68,10 @@ NEW_CAPABILITY_FIELDS: dict[str, dict[str, int]] = { "zwave_proxy": 3, "serial_proxies": 4, "wizard": 5, + "sendspin": 6, }, "WizardCapabilities": {"configured": 1}, + "SendspinCapabilities": {"feature_flags": 1}, "BluetoothProxyCapabilities": { "feature_flags": 1, "mac_address": 2, @@ -446,6 +448,39 @@ def test_device_wizard_rpc_requires_authentication() -> None: assert "needs_authentication" not in body +def test_sendspin_pairing_token_messages_keep_their_wire_ids() -> None: + """Message ids are part of the wire protocol and must not change.""" + for message, expected in ( + ("SendspinPairingTokenRequest", 159), + ("SendspinPairingTokenResponse", 160), + ): + body = _extract_proto_message(PROTO_TEXT, message) + match = re.search(r"option \(id\) = (\d+);", body) + assert match is not None, f"{message} is missing `option (id)`" + assert int(match.group(1)) == expected, ( + f"{message} has id {match.group(1)}, expected {expected}" + ) + + +def test_sendspin_pairing_token_response_fields_keep_their_wire_numbers() -> None: + """Clients decode the response purely by field number.""" + body = _extract_proto_message(PROTO_TEXT, "SendspinPairingTokenResponse") + for field_name, number in (("status", 1), ("token", 2)): + line = _field_declaration_line(body, field_name) + assert re.search(rf"\b{field_name}\s*=\s*{number}\b", line), ( + f"{field_name} in SendspinPairingTokenResponse is no longer field " + f"number {number}" + ) + + +def test_sendspin_pairing_token_rpc_requires_authentication() -> None: + """The pairing token is a long-lived secret, so the rpc must not set + `needs_authentication` and inherits the default of true. + """ + body = _extract_rpc_body(PROTO_TEXT, "sendspin_pairing_token") + assert "needs_authentication" not in body + + # ==================== Group C: advertised API version ==================== diff --git a/tests/unit_tests/components/nrf52/test_testing_mode_flash.py b/tests/unit_tests/components/nrf52/test_testing_mode_flash.py new file mode 100644 index 0000000000..7c66cd3361 --- /dev/null +++ b/tests/unit_tests/components/nrf52/test_testing_mode_flash.py @@ -0,0 +1,30 @@ +"""Testing mode fakes a larger flash so grouped component tests link.""" + +from esphome.components import nrf52 +from esphome.components.nrf52.boards import BOOTLOADER_CONFIG +from esphome.components.nrf52.const import BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 +from esphome.components.zephyr import Section + + +def test_testing_mode_sections_move_only_the_bootloader() -> None: + """The bootloader pinned to the end of flash follows the faked end; the + SoftDevice at the start stays.""" + sections = nrf52._testing_mode_sections( + BOOTLOADER_CONFIG[BOOTLOADER_ADAFRUIT_NRF52_SD140_V7] + ) + + assert [(s.name, s.address, s.size) for s in sections] == [ + ("SoftDevice", 0x0, 0x27000), + ( + "Adafruit_nRF52_Bootloader", + nrf52.TESTING_FLASH_SIZE - 0xC000, + 0xC000, + ), + ] + assert sections[1].end_address == nrf52.TESTING_FLASH_SIZE + + +def test_testing_mode_sections_leave_unpinned_sections() -> None: + section = Section("other", 0x50000, 0x1000, "flash_primary") + + assert nrf52._testing_mode_sections([section]) == [section] diff --git a/tests/unit_tests/components/test_uart.py b/tests/unit_tests/components/test_uart.py new file mode 100644 index 0000000000..4b062b4729 --- /dev/null +++ b/tests/unit_tests/components/test_uart.py @@ -0,0 +1,24 @@ +"""Tests for the uart helpers shared by components that own a UART.""" + +from esphome.components.uart import subtree_references_uart +from esphome.core import ID + + +def test_finds_a_nested_uart_id() -> None: + config = {"modbus": [{"id": ID("hub"), "uart_id": ID("bus")}]} + assert subtree_references_uart(config, "bus") + + +def test_ignores_other_uarts_and_other_keys() -> None: + config = {"modbus": [{"uart_id": ID("other")}], "sensor": [{"id": ID("bus")}]} + assert not subtree_references_uart(config, "bus") + + +def test_finds_another_key() -> None: + config = {"modbus_tcp_uart": [{"tcp_uart_id": ID("bus")}]} + assert subtree_references_uart(config, "bus", "tcp_uart_id") + assert not subtree_references_uart(config, "bus") + + +def test_handles_scalars() -> None: + assert not subtree_references_uart("bus", "bus") diff --git a/tests/unit_tests/components/test_usb_host.py b/tests/unit_tests/components/test_usb_host.py index 1a1df68f4d..8d572dba96 100644 --- a/tests/unit_tests/components/test_usb_host.py +++ b/tests/unit_tests/components/test_usb_host.py @@ -1,9 +1,12 @@ """Tests for usb_host device matching validation.""" +import logging + import pytest -from esphome.components.usb_host import validate_usb_clients +from esphome.components.usb_host import _final_validate, validate_usb_clients import esphome.config_validation as cv +import esphome.final_validate as fv from esphome.types import ConfigType @@ -109,3 +112,73 @@ def test_every_pair_is_compared_not_just_neighbours() -> None: ] with pytest.raises(cv.Invalid, match="a, c"): validate_usb_clients(configs) + + +def _run_final_validate( + devices: list[ConfigType], uarts: list[ConfigType] +) -> ConfigType: + config = {"id": "usb_host", "devices": devices} + token = fv.full_config.set({"usb_host": config, "usb_uart": uarts}) + try: + return _final_validate(config) + finally: + fv.full_config.reset(token) + + +def test_device_duplicating_usb_uart_warns(caplog: pytest.LogCaptureFixture) -> None: + devices = [{"id": "device_0", "vid": 0x303A, "pid": 0x4001}] + uarts = [{"id": "uart_0", "vid": 0x303A, "pid": 0x4001}] + with caplog.at_level(logging.WARNING): + _run_final_validate(devices, uarts) + assert "'device_0'" in caplog.text + assert "'uart_0'" in caplog.text + assert "remove it from usb_host devices" in caplog.text + + +def test_wildcard_device_covering_usb_uart_suggests_narrowing( + caplog: pytest.LogCaptureFixture, +) -> None: + devices = [{"id": "device_0", "vid": 0x303A, "pid": 0}] + uarts = [{"id": "uart_0", "vid": 0x303A, "pid": 0x4001}] + with caplog.at_level(logging.WARNING): + _run_final_validate(devices, uarts) + assert "narrow its filter" in caplog.text + assert "remove it" not in caplog.text + + +def test_disjoint_device_and_usb_uart_do_not_warn( + caplog: pytest.LogCaptureFixture, +) -> None: + devices = [{"id": "device_0", "vid": 0x303A, "pid": 0x4002}] + uarts = [{"id": "uart_0", "vid": 0x303A, "pid": 0x4001}] + with caplog.at_level(logging.WARNING): + _run_final_validate(devices, uarts) + assert caplog.text == "" + + +_DUPLICATE_CLIENTS = [ + {"id": "a", "vid": 0x303A, "pid": 0x4001}, + {"id": "b", "vid": 0x303A, "pid": 0x4001}, +] + + +@pytest.mark.parametrize( + ("devices", "uarts"), + [([*_DUPLICATE_CLIENTS], []), ([], [*_DUPLICATE_CLIENTS])], + ids=["devices", "uarts"], +) +def test_overlap_within_one_component_is_rejected( + devices: list[ConfigType], uarts: list[ConfigType] +) -> None: + with pytest.raises(cv.Invalid, match="a, b"): + _run_final_validate(devices, uarts) + + +def test_device_inside_broader_usb_uart_suggests_removing( + caplog: pytest.LogCaptureFixture, +) -> None: + devices = [{"id": "device_0", "vid": 0x303A, "pid": 0x4001}] + uarts = [{"id": "uart_0", "vid": 0x303A, "pid": 0}] + with caplog.at_level(logging.WARNING): + _run_final_validate(devices, uarts) + assert "remove it from usb_host devices" in caplog.text diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index ff5564e815..7835156b01 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -18,6 +18,7 @@ from unittest.mock import Mock, patch import pytest from esphome.core import CORE +from esphome.espidf import toolchain here = Path(__file__).parent @@ -26,6 +27,21 @@ package_root = here.parent.parent sys.path.insert(0, package_root.as_posix()) +@pytest.fixture(autouse=True) +def _no_idf_install(monkeypatch: pytest.MonkeyPatch) -> None: + """Fail any test that would install ESP-IDF for real. + + An unmocked ``_get_idf_env`` reaches ``check_esp_idf_install``, which + downloads the framework on a bare CI runner (minutes on Windows). The + install path's own tests call it through ``esphome.espidf.framework``. + """ + + def refuse(*args: object, **kwargs: object) -> None: + raise AssertionError("test would install ESP-IDF; mock _get_idf_env") + + monkeypatch.setattr(toolchain, "check_esp_idf_install", refuse) + + @pytest.fixture(autouse=True) def reset_core(): """Reset CORE after each test.""" diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py index 2bcd32375e..3cd70c082d 100644 --- a/tests/unit_tests/test_automation.py +++ b/tests/unit_tests/test_automation.py @@ -16,10 +16,15 @@ from esphome.automation import ( TriggerForwarder, TriggerOnFalseForwarder, TriggerOnTrueForwarder, + build_callback_automation, build_callback_automations, + build_parent_callback_automation, + build_trigger_automations, + build_trigger_callback, has_non_synchronous_actions, literal_with_length, maybe_simple_id, + parent_ref, progmem_bytes, register_apply_action, register_apply_condition, @@ -29,11 +34,12 @@ from esphome.automation import ( register_parented_condition, register_simple_action, register_simple_condition, + string_ref_literal, templatable_bytes, ) import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_AUTOMATION_ID, CONF_ID, CONF_THEN from esphome.core import CORE, ID, KEY_CORE, KEY_TARGET_PLATFORM, EsphomeError, Lambda from esphome.cpp_generator import MockObj, RawExpression from esphome.util import Registry, RegistryEntry @@ -327,7 +333,14 @@ async def test_build_callback_automations_single_entry( (CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]),), ) mock_build_callback.assert_called_once_with( - parent, "add_on_state_callback", [(bool, "x")], conf, forwarder=None + parent, + "add_on_state_callback", + [(bool, "x")], + conf, + forwarder=None, + params=None, + forward=None, + when=None, ) @@ -347,10 +360,24 @@ async def test_build_callback_automations_multiple_configs( ) assert mock_build_callback.call_count == 2 mock_build_callback.assert_any_call( - parent, "add_on_state_callback", [(bool, "x")], conf1, forwarder=None + parent, + "add_on_state_callback", + [(bool, "x")], + conf1, + forwarder=None, + params=None, + forward=None, + when=None, ) mock_build_callback.assert_any_call( - parent, "add_on_state_callback", [(bool, "x")], conf2, forwarder=None + parent, + "add_on_state_callback", + [(bool, "x")], + conf2, + forwarder=None, + params=None, + forward=None, + when=None, ) @@ -378,9 +405,25 @@ async def test_build_callback_automations_multiple_entries( ) assert mock_build_callback.call_count == 2 assert mock_build_callback.call_args_list == [ - call(parent, "add_on_value_callback", [(float, "x")], conf_a, forwarder=None), call( - parent, "add_on_raw_value_callback", [(float, "x")], conf_b, forwarder=None + parent, + "add_on_value_callback", + [(float, "x")], + conf_a, + forwarder=None, + params=None, + forward=None, + when=None, + ), + call( + parent, + "add_on_raw_value_callback", + [(float, "x")], + conf_b, + forwarder=None, + params=None, + forward=None, + when=None, ), ] @@ -403,7 +446,14 @@ async def test_build_callback_automations_with_forwarder( ), ) mock_build_callback.assert_called_once_with( - parent, "add_on_state_callback", [], conf, forwarder=TriggerOnTrueForwarder + parent, + "add_on_state_callback", + [], + conf, + forwarder=TriggerOnTrueForwarder, + params=None, + forward=None, + when=None, ) @@ -437,7 +487,14 @@ async def test_build_callback_automations_mixed_entries( assert mock_build_callback.call_count == 3 assert mock_build_callback.call_args_list == [ call( - parent, "add_on_state_callback", [(bool, "x")], conf_state, forwarder=None + parent, + "add_on_state_callback", + [(bool, "x")], + conf_state, + forwarder=None, + params=None, + forward=None, + when=None, ), call( parent, @@ -445,6 +502,9 @@ async def test_build_callback_automations_mixed_entries( [], conf_press, forwarder=TriggerOnTrueForwarder, + params=None, + forward=None, + when=None, ), call( parent, @@ -452,6 +512,9 @@ async def test_build_callback_automations_mixed_entries( [], conf_release, forwarder=TriggerOnFalseForwarder, + params=None, + forward=None, + when=None, ), ] @@ -477,7 +540,14 @@ async def test_build_callback_automations_skips_missing_keys( ), ) mock_build_callback.assert_called_once_with( - parent, "add_on_state_callback", [], conf, forwarder=TriggerOnTrueForwarder + parent, + "add_on_state_callback", + [], + conf, + forwarder=TriggerOnTrueForwarder, + params=None, + forward=None, + when=None, ) @@ -495,7 +565,14 @@ async def test_build_callback_automations_defaults( (CallbackAutomation("on_press", "add_on_press_callback"),), ) mock_build_callback.assert_called_once_with( - parent, "add_on_press_callback", [], conf, forwarder=None + parent, + "add_on_press_callback", + [], + conf, + forwarder=None, + params=None, + forward=None, + when=None, ) @@ -512,9 +589,59 @@ class MockCodegen(NamedTuple): new_pvariable: MagicMock register_parented: AsyncMock add_global: MagicMock + add: MagicMock calls: MagicMock # new_pvariable and add_global attached, to check their order +@pytest.fixture +def mock_build_automation() -> Generator[AsyncMock]: + with patch("esphome.automation.build_automation", new_callable=AsyncMock) as mock: + yield mock + + +@pytest.mark.asyncio +async def test_build_trigger_automations_with_parent( + mock_build_automation: AsyncMock, +) -> None: + """Each entry's Trigger class is instantiated with the parent and built with its args.""" + parent = MockObj("var", "->") + on_conf = {"trigger_id": ID("trig_1"), "then": []} + set_conf = {"trigger_id": ID("trig_2"), "then": []} + config = {"on_turn_on": [on_conf], "on_speed_set": [set_conf]} + with patch("esphome.codegen.new_Pvariable") as new_pvariable: + new_pvariable.side_effect = lambda id_, *args: MockObj(str(id_), "->") + await build_trigger_automations( + parent, + config, + ( + ("on_turn_on", []), + ("on_turn_off", []), + ("on_speed_set", [(cg.int_, "x")]), + ), + ) + assert [c.args for c in new_pvariable.call_args_list] == [ + (ID("trig_1"), parent), + (ID("trig_2"), parent), + ] + calls = mock_build_automation.call_args_list + assert [(str(c.args[0]), c.args[1], c.args[2]) for c in calls] == [ + ("trig_1", [], on_conf), + ("trig_2", [(cg.int_, "x")], set_conf), + ] + + +@pytest.mark.asyncio +async def test_build_trigger_automations_without_parent( + mock_build_automation: AsyncMock, +) -> None: + """A None parent instantiates the Trigger class with no constructor arguments.""" + conf = {"trigger_id": ID("trig_1"), "then": []} + with patch("esphome.codegen.new_Pvariable") as new_pvariable: + await build_trigger_automations(None, {"on_boot": [conf]}, (("on_boot", []),)) + new_pvariable.assert_called_once_with(ID("trig_1")) + mock_build_automation.assert_awaited_once() + + @pytest.fixture def mock_cg() -> Generator[MockCodegen]: """Patch the codegen calls the shared builders make.""" @@ -525,6 +652,7 @@ def mock_cg() -> Generator[MockCodegen]: "esphome.codegen.register_parented", new_callable=AsyncMock ) as register_parented, patch("esphome.cpp_generator.add_global") as add_global, + patch("esphome.codegen.add") as add, ): get_variable.return_value = PARENT_OBJ new_pvariable.return_value = NEW_OBJ @@ -532,7 +660,7 @@ def mock_cg() -> Generator[MockCodegen]: calls.attach_mock(new_pvariable, "new_pvariable") calls.attach_mock(add_global, "add_global") yield MockCodegen( - get_variable, new_pvariable, register_parented, add_global, calls + get_variable, new_pvariable, register_parented, add_global, add, calls ) @@ -815,6 +943,30 @@ async def test_apply_action_call_shape( assert positions == sorted(positions) +@pytest.mark.asyncio +async def test_apply_target_names_parent( + registries: tuple[Registry, Registry], mock_cg: MockCodegen +) -> None: + """{parent} in a target is the global-scoped parent, also beside a call object.""" + fields = (ApplyCall("set_peer({parent}->id(), {})", (("level", cg.float_),)),) + await _run_apply_action(registries, fields, {"level": 0.5}) + assert ( + f"::{PARENT_OBJ}->set_peer(::{PARENT_OBJ}->id(), 0.5f);" + in _apply_definition(mock_cg) + ) + mock_cg.new_pvariable.reset_mock() + await _run_apply_action(registries, fields, {"level": 0.5}, call="make_call") + assert f"apply_call.set_peer(::{PARENT_OBJ}->id(), 0.5f);" in _apply_definition( + mock_cg + ) + mock_cg.new_pvariable.reset_mock() + await _run_apply_condition(registries, "is_peer({parent}->id())", {}) + assert ( + f"return ::{PARENT_OBJ}->is_peer(::{PARENT_OBJ}->id());" + in _apply_definition(mock_cg) + ) + + @pytest.mark.asyncio async def test_apply_field_nested_key_const_fn_and_type_string( registries: tuple[Registry, Registry], mock_cg: MockCodegen @@ -851,8 +1003,10 @@ async def test_apply_field_nested_key_const_fn_and_type_string( def test_apply_registration_checks(registries: tuple[Registry, Registry]) -> None: with pytest.raises(ValueError, match="2 placeholder"): ApplyCall("set_range({}, {})", (("low", cg.float_),)) - with pytest.raises(ValueError, match="only bare"): - ApplyCall("if ({}) {parent}->reset()", (("reset", cg.bool_),)) + with pytest.raises(ValueError, match="only {} and {parent}"): + ApplyCall("if ({}) {other}->reset()", (("reset", cg.bool_),)) + assert ApplyCall("if ({}) {parent}->reset()", (("reset", cg.bool_),)).names_parent + assert not ApplyCall("set_flags({{parent}})").names_parent ApplyCall("set_flags({{{}}})", (("flags", cg.int_),)) with pytest.raises(ValueError, match="each arg is"): ApplyCall("set_kp({})", (("kp", cg.float_, None, "extra"),)) @@ -927,6 +1081,26 @@ async def test_apply_literal_with_length_is_plain_on_every_platform( assert "progmem_string" not in text +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("platform", "rendered"), + [("esp32", 'StringRef("ON", 2)'), ("esp8266", 'ESPHOME_F("ON")')], +) +async def test_apply_string_ref_literal_stays_in_flash_on_esp8266( + registries: tuple[Registry, Registry], + mock_cg: MockCodegen, + platform: str, + rendered: str, +) -> None: + fields = ( + ApplyField( + "payload", "set_payload", cg.std_string, const_fn=string_ref_literal + ), + ) + await _run_apply_action(registries, fields, {"payload": "ON"}, platform=platform) + assert f"::{PARENT_OBJ}->set_payload({rendered});" in _apply_definition(mock_cg) + + @pytest.mark.asyncio async def test_register_apply_condition_predicate( registries: tuple[Registry, Registry], mock_cg: MockCodegen @@ -1036,3 +1210,165 @@ async def test_templatable_bytes_rejects_oversized_payload() -> None: await templatable_bytes( [0] * 0x10000, [], var.set_code_template, var.set_code_static, "payload" ) + + +TRIGGER_CONF = {CONF_AUTOMATION_ID: ID("automation_1"), CONF_THEN: []} + + +def _squash(expr: object) -> str: + """Generated text with its whitespace folded, for one-line assertions.""" + return " ".join(str(expr).split()) + + +def test_parent_ref_is_global_scoped() -> None: + """A parent named through parent_ref cannot be shadowed by a trigger argument.""" + assert ( + str(parent_ref(MockObj("sel", "->")).option_at(RawExpression("i"))) + == "::sel->option_at(i)" + ) + + +@pytest.mark.asyncio +async def test_trigger_callback_forwards_params(mock_cg: MockCodegen) -> None: + """Without forward or when the callback passes its parameters straight to trigger().""" + text = str( + await build_trigger_callback( + [(cg.bool_, "state")], TRIGGER_CONF, [(cg.bool_, "state")] + ) + ) + assert text.startswith("[](const std::remove_cvref_t & state) -> void {") + assert f"::{NEW_OBJ}->trigger(state);" in text + assert "if (" not in text + + +@pytest.mark.asyncio +async def test_trigger_callback_reshapes_and_filters(mock_cg: MockCodegen) -> None: + """A filter returns early, forward picks the trigger args, a string constant is a plain literal.""" + when = ApplyCall("payload == {}", (("payload", cg.std_string),)) + params = [(cg.std_string, "topic"), (cg.std_string, "payload")] + text = _squash( + await build_trigger_callback( + [(cg.std_string, "x")], + {**TRIGGER_CONF, "payload": "hi"}, + params, + forward=["payload"], + when=when, + ) + ) + assert "& topic, const std::remove_cvref_t & payload) -> void" in text + assert f'if (!(payload == "hi")) return; ::{NEW_OBJ}->trigger(payload);' in text + # An absent optional key skips the filter, as for any ApplyCall. + text = str( + await build_trigger_callback( + [(cg.std_string, "x")], TRIGGER_CONF, params, forward=["payload"], when=when + ) + ) + assert "if (" not in text + + +@pytest.mark.asyncio +async def test_trigger_callback_filter_has_no_parent(mock_cg: MockCodegen) -> None: + """A filter that names {parent} is rejected up front when no parent is given.""" + for when in ( + ApplyCall("mode == {}", (("mode", "{parent}::Mode"),)), + "{parent}->is_failed() == false", + ): + with pytest.raises(ValueError, match="names {parent}"): + await build_trigger_callback( + [], + {**TRIGGER_CONF, "mode": 1}, + [(cg.int_, "mode")], + forward=[], + when=when, + ) + + +@pytest.mark.asyncio +async def test_trigger_callback_filter_names_parent(mock_cg: MockCodegen) -> None: + """With the parent given, {parent} in a filter is the global-scoped parent.""" + text = _squash( + await build_trigger_callback( + [], + TRIGGER_CONF, + [(cg.int_, "state")], + forward=[], + when="state == 1 && {parent}->is_failed() == false", + parent=MockObj("improv", "->"), + ) + ) + assert ( + "if (!(state == 1 && ::improv->is_failed() == false)) return; " + f"::{NEW_OBJ}->trigger();" + ) in text + + +@pytest.mark.asyncio +async def test_build_callback_automations_when_names_parent( + mock_cg: MockCodegen, +) -> None: + """A table entry names the parent through {parent}, so it can live at module level.""" + entries = ( + CallbackAutomation( + "on_open", "add_on_state_callback", when="{parent}->is_fully_open()" + ), + ) + await build_callback_automations( + MockObj("valve", "->"), {"on_open": [TRIGGER_CONF]}, entries + ) + assert _squash(mock_cg.add.call_args.args[0]) == ( + "valve->add_on_state_callback([]() -> void { " + f"if (!(::valve->is_fully_open())) return; ::{NEW_OBJ}->trigger(); }})" + ) + + +@pytest.mark.asyncio +async def test_build_parent_callback_automation(mock_cg: MockCodegen) -> None: + """A no-argument callback registers a lambda that hands the parent to the automation.""" + parent = MockObj("fan", "->") + await build_parent_callback_automation( + parent, + "add_on_state_callback", + (cg.RawExpression("Fan *"), "x"), + TRIGGER_CONF, + ) + assert _squash(mock_cg.add.call_args.args[0]) == ( + f"fan->add_on_state_callback([]() -> void {{ ::{NEW_OBJ}->trigger(::fan); }})" + ) + + +@pytest.mark.asyncio +async def test_build_callback_automation_lambda(mock_cg: MockCodegen) -> None: + """Reshaping keywords switch the registration to the lambda; forwarder cannot join them.""" + parent = MockObj("sel", "->") + await build_callback_automation( + parent, + "add_cb", + [(cg.std_string, "x"), (cg.size_t, "i")], + TRIGGER_CONF, + params=[(cg.size_t, "index")], + forward=[parent_ref(parent).option_at(RawExpression("index")), "index"], + ) + assert _squash(mock_cg.add.call_args.args[0]) == ( + "sel->add_cb([](const std::remove_cvref_t & index) -> void { " + f"::{NEW_OBJ}->trigger(::sel->option_at(index), index); }})" + ) + with pytest.raises(ValueError, match="forwarder"): + await build_callback_automation( + parent, + "add_cb", + [], + TRIGGER_CONF, + forwarder=TriggerOnTrueForwarder, + when="x", + ) + + +@pytest.mark.asyncio +async def test_build_callback_automation_forwarder(mock_cg: MockCodegen) -> None: + """The forwarder path registers a pointer-sized TriggerForwarder, not a lambda.""" + await build_callback_automation( + MockObj("parent", "->"), "add_cb", [(cg.bool_, "x")], TRIGGER_CONF + ) + assert str(mock_cg.add.call_args.args[0]) == ( + f"parent->add_cb(TriggerForwarder{{{NEW_OBJ}}})" + ) diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index b6f8fd1ec1..6fdd197c95 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -4,7 +4,7 @@ import math import pytest from esphome import cpp_generator as cg, cpp_types as ct -from esphome.core import CORE, ID +from esphome.core import CORE, ID, EsphomeError class TestExpressions: @@ -829,6 +829,46 @@ class TestSharedProgmemArray: assert str(a) != str(b) assert sum("PROGMEM" in str(st) for st in CORE.global_statements) == 2 + def test_is_static_pointer_only_for_placement_new(self) -> None: + CORE.config = {} + static = cg.new_Pvariable(ID("static_obj", is_declaration=True, type=ct.uint8)) + dynamic = cg.Pvariable( + ID("dynamic_obj", is_declaration=True, type=ct.uint8), + cg.RawExpression("nullptr"), + ) + assert cg.is_static_pointer(static) + assert not cg.is_static_pointer(dynamic) + + def test_constexpr_and_const_arrays_are_not_shared(self) -> None: + CORE.config = {} + a = cg.shared_progmem_array("table", ct.uint8, [1], constexpr=False) + b = cg.shared_progmem_array("table", ct.uint8, [1]) + assert str(a) != str(b) + + def test_constexpr_false_rejects_dynamic_pointers(self) -> None: + CORE.config = {} + dynamic = cg.Pvariable( + ID("dynamic_obj", is_declaration=True, type=ct.uint8), + cg.RawExpression("nullptr"), + ) + with pytest.raises(EsphomeError, match="dynamic_obj"): + cg.shared_progmem_array( + "table", + ct.uint8.operator("ptr"), + cg.ArrayInitializer(dynamic), + constexpr=False, + ) + + def test_constexpr_false_emits_a_const_array(self) -> None: + CORE.config = {} + cg.shared_progmem_array("table", ct.uint8, [1], constexpr=False) + (statement,) = ( + str(st) for st in CORE.global_statements if "PROGMEM" in str(st) + ) + assert statement.startswith( + "ESPHOME_FLASH_CONSTINIT static uint8_t const table[] PROGMEM = {1}" + ) + def test_same_contents_different_type_are_separate(self) -> None: CORE.config = {} a = cg.shared_progmem_array("table", ct.uint8, [1]) diff --git a/tests/unit_tests/test_espidf_toolchain.py b/tests/unit_tests/test_espidf_toolchain.py index 620d08fe3b..2a799a4d84 100644 --- a/tests/unit_tests/test_espidf_toolchain.py +++ b/tests/unit_tests/test_espidf_toolchain.py @@ -647,7 +647,7 @@ def test_run_compile_cache_hit_skips_discovery(setup_core: Path) -> None: @contextmanager -def _cache_env(tmp_path: Path, excluded: str) -> Iterator[Path]: +def _cache_env(tmp_path: Path) -> Iterator[Path]: """Patch everything the cache key derives from onto a temp IDF tree and yield that tree's path.""" idf_path = tmp_path / "idf" @@ -655,7 +655,6 @@ def _cache_env(tmp_path: Path, excluded: str) -> Iterator[Path]: with ( patch.object(toolchain, "_get_idf_path", return_value=idf_path), patch.dict(CORE.data, {KEY_ESP32: {KEY_VARIANT: "ESP32"}}), - patch.dict(CORE.cmake_args, {"EXCLUDE_COMPONENTS": excluded}), ): yield idf_path @@ -663,7 +662,7 @@ def _cache_env(tmp_path: Path, excluded: str) -> Iterator[Path]: def test_component_cache_round_trip(setup_core: Path, tmp_path: Path) -> None: """A saved list is read back until it is dropped.""" _setup_build(setup_core) - with _cache_env(tmp_path, "fatfs") as idf_path: + with _cache_env(tmp_path) as idf_path: for name in ("lwip", "esp_timer"): (idf_path / "components" / name).mkdir() assert toolchain.load_cached_builtin_components() is None @@ -673,25 +672,28 @@ def test_component_cache_round_trip(setup_core: Path, tmp_path: Path) -> None: assert toolchain.load_cached_builtin_components() is None -def test_component_cache_misses_on_key_change_or_missing_component( +def test_component_cache_is_per_target_and_drops_missing_component( setup_core: Path, tmp_path: Path ) -> None: - """A different exclusion set uses another entry, an entry naming a + """One entry per target serves every exclusion set, an entry naming a component that no longer exists is ignored, and a custom IDF_PATH is never cached.""" _setup_build(setup_core) - with _cache_env(tmp_path, "fatfs") as idf_path: + with _cache_env(tmp_path) as idf_path: (idf_path / "components" / "lwip").mkdir() toolchain.save_cached_builtin_components(["lwip"]) path = toolchain._builtin_component_cache_path() assert path.parent == idf_path / ".esphome_component_lists" - assert path.name.startswith("esp32-") + assert path.name == "esp32.json" assert toolchain.load_cached_builtin_components() == ["lwip"] with patch.dict(os.environ, {"IDF_PATH": str(idf_path)}): assert toolchain.load_cached_builtin_components() is None - with _cache_env(tmp_path, "fatfs;unity"): - assert toolchain.load_cached_builtin_components() is None - with _cache_env(tmp_path, "fatfs") as idf_path: + with ( + _cache_env(tmp_path), + patch.dict(CORE.cmake_args, {"EXCLUDE_COMPONENTS": "fatfs;unity"}), + ): + assert toolchain.load_cached_builtin_components() == ["lwip"] + with _cache_env(tmp_path) as idf_path: path.write_text(json.dumps(["lwip", "gone"])) assert toolchain.load_cached_builtin_components() is None # A plain file with the right name is not a component directory. @@ -703,7 +705,7 @@ def test_component_cache_save_skips_empty_list_or_custom_idf_path( setup_core: Path, tmp_path: Path ) -> None: _setup_build(setup_core) - with _cache_env(tmp_path, "") as idf_path: + with _cache_env(tmp_path) as idf_path: toolchain.save_cached_builtin_components([]) with patch.dict(os.environ, {"IDF_PATH": str(idf_path)}): toolchain.save_cached_builtin_components(["lwip"]) @@ -715,7 +717,7 @@ def test_component_cache_write_failure_is_logged( ) -> None: _setup_build(setup_core) with ( - _cache_env(tmp_path, ""), + _cache_env(tmp_path), patch.object(toolchain, "write_file", side_effect=EsphomeError("disk full")), ): toolchain.save_cached_builtin_components(["lwip"]) @@ -725,7 +727,7 @@ def test_component_cache_write_failure_is_logged( def test_component_cache_ignores_corrupt_file(setup_core: Path, tmp_path: Path) -> None: _setup_build(setup_core) - with _cache_env(tmp_path, ""): + with _cache_env(tmp_path): path = toolchain._builtin_component_cache_path() path.parent.mkdir(parents=True) path.write_text("{not json") @@ -1519,6 +1521,7 @@ def test_run_reconfigure_flip_into_skip_mode_cleans_up(setup_core: Path) -> None with ( patch.object(toolchain, "_skip_bootloader", return_value=True), patch.object(toolchain, "_get_idf_tool", side_effect=lambda n: f"/tools/{n}"), + patch.object(toolchain, "_get_idf_env", return_value={}), patch.object(toolchain, "_tool_env", return_value={}), patch.object(toolchain, "run_build_tool", return_value=0), patch.object(toolchain, "_idf_py") as mock_idf_py, @@ -1542,6 +1545,7 @@ def test_run_reconfigure_skip_steady_state_cleans_nothing(setup_core: Path) -> N with ( patch.object(toolchain, "_skip_bootloader", return_value=True), patch.object(toolchain, "_get_idf_tool", side_effect=lambda n: f"/tools/{n}"), + patch.object(toolchain, "_get_idf_env", return_value={}), patch.object(toolchain, "_tool_env", return_value={}), patch.object(toolchain, "run_build_tool", return_value=0), patch.object(toolchain, "_idf_py") as mock_idf_py,