mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
Merge remote-tracking branch 'origin/dev' into HEAD
This commit is contained in:
+25
-23
@@ -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.<platform>.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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.22.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+178
-88
@@ -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<Ts...>. 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,
|
||||
)
|
||||
|
||||
+51
-15
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@MrSuicideParrot"]
|
||||
DOMAIN = "a01nyub"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@TH-Braemer"]
|
||||
DOMAIN = "a02yyuw"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "a4988"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@DAVe3283"]
|
||||
DOMAIN = "absolute_humidity"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "ac_dimmer"
|
||||
|
||||
@@ -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_(
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
DOMAIN = "adc128s102"
|
||||
MULTI_CONF = True
|
||||
CODEOWNERS = ["@DeerMaximum"]
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "addressable_light"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "ade7880"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@angelnu"]
|
||||
DOMAIN = "ade7953"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@angelnu"]
|
||||
DOMAIN = "ade7953_i2c"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@angelnu"]
|
||||
DOMAIN = "ade7953_spi"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@solomondg1"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
DOMAIN = "ads1118"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_ADS1118_ID = "ads1118_id"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@mak-42"]
|
||||
DOMAIN = "ags10"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "aht10"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "aic3204"
|
||||
|
||||
@@ -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_(
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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_();
|
||||
});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@ncareau"]
|
||||
DOMAIN = "airthings_wave_mini"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@jeromelaban", "@precurse"]
|
||||
DOMAIN = "airthings_wave_plus"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@jan-hofmeier"]
|
||||
DOMAIN = "alpha3"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
DOMAIN = "am2315c"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "am2320"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@buxtronix"]
|
||||
DOMAIN = "am43"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@ianchi"]
|
||||
DOMAIN = "analog_threshold"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "anova"
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
|
||||
|
||||
CODEOWNERS = ["@aodrenah"]
|
||||
DOMAIN = "apds9306"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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<const char *>"];
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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<const char *> 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);
|
||||
}
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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<const SendspinCapabilities *>(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<const SendspinCapabilities *>(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<const DeviceCapabilitiesResponse *>(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<const SendspinPairingTokenResponse *>(self);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(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<const SendspinPairingTokenResponse *>(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<HelloRequest>, "decodable messages carry no vtable");
|
||||
static_assert(!std::is_polymorphic_v<DisconnectRequest>, "decodable messages carry no vtable");
|
||||
|
||||
@@ -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<const char *> *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
|
||||
|
||||
@@ -928,6 +928,25 @@ template<> const char *proto_enum_to_string<enums::SerialProxyIdentityFlag>(enum
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#ifdef USE_SENDSPIN
|
||||
template<>
|
||||
const char *proto_enum_to_string<enums::SendspinPairingTokenStatus>(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<enums::SendspinPairingTokenStatus>(this->status));
|
||||
dump_field(out, ESPHOME_PSTR("token"), this->token);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
#include "esphome/components/light/light_effect_names.h"
|
||||
#include "esphome/components/light/light_traits.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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_(); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "as7341"
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
DOMAIN = "async_tcp"
|
||||
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
|
||||
@@ -8,6 +8,7 @@ from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@X-Ryl669"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "at581x"
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "atc_mithermometer"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@danieltwagner"]
|
||||
DOMAIN = "atm90e26"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "audio_http"
|
||||
|
||||
@@ -2,5 +2,6 @@ import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "axs15231"
|
||||
|
||||
axs15231_ns = cg.esphome_ns.namespace("axs15231")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "b_parasite"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "ballu"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
DOMAIN = "bang_bang"
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@jhansche"]
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
DOMAIN = "bedjet"
|
||||
MULTI_CONF = True
|
||||
CONF_BEDJET_ID = "bedjet_id"
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "beken_spi_led_strip"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "bh1750"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "bh1900nux"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
DOMAIN = "binary"
|
||||
|
||||
binary_ns = cg.esphome_ns.namespace("binary")
|
||||
|
||||
@@ -106,6 +106,7 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_VIBRATION,
|
||||
DEVICE_CLASS_WINDOW,
|
||||
]
|
||||
DOMAIN = "binary_sensor"
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "binary_sensor_map"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -38,6 +38,7 @@ from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["bk72xx"]
|
||||
CODEOWNERS = ["@Bl00d-B0b"]
|
||||
DOMAIN = "bk72xx_ble"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@athom-tech", "@tarontop", "@jesserockz"]
|
||||
DOMAIN = "bl0906"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@ziceva"]
|
||||
DOMAIN = "bl0939"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@dbuezas", "@dwmw2"]
|
||||
DOMAIN = "bl0942"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "ble_presence"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "ble_rssi"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "ble_scanner"
|
||||
|
||||
@@ -58,6 +58,7 @@ _HUB_PLATFORMS = (PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RP2)
|
||||
|
||||
DEPENDENCIES = ["api"]
|
||||
CODEOWNERS = ["@jesserockz", "@bdraco"]
|
||||
DOMAIN = "bluetooth_proxy"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CODEOWNERS = ["@abmantis"]
|
||||
DOMAIN = "bm8563"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "bme280_i2c"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "bme280_spi"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "bme680"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user