Compare commits

..
7 changed files with 28 additions and 70 deletions

No files matched your search

+1 -3
View File
@@ -296,9 +296,7 @@ def _external_manifests() -> list[dict[str, Any]]:
found[component_dir] = source
break
else:
# Built in means anywhere in the package: the esphome domain itself
# is esphome/core, not under esphome/components
if not component_dir.is_relative_to(loader.CORE_COMPONENTS_PATH.parent):
if not component_dir.is_relative_to(loader.CORE_COMPONENTS_PATH):
_LOGGER.warning(
"SBOM: component '%s' in %s matches no external component "
"source and is listed as part of ESPHome",
-17
View File
@@ -72,23 +72,6 @@ button:
id(ota_update).set_source_url(std::string("http://my.ha.net:8123/local/esphome/manifest.json"));
ESP_LOGI("test", "Manifest is now %s", id(ota_update).get_source_url());
- component.update: ota_update
# The OTA component's public setters and getters as lambdas use them; every shape must compile.
- platform: template
name: Flash from lambda
on_press:
then:
- lambda: |-
std::string url = "http://my.ha.net:8123/local/esphome/";
url += "firmware.bin";
id(http_request_ota).set_md5_url("http://my.ha.net:8123/local/esphome/firmware.md5");
id(http_request_ota).set_md5(std::string("0123456789abcdef0123456789abcdef"));
id(http_request_ota).set_username("user");
id(http_request_ota).set_password(std::string("pass"));
id(http_request_ota).set_url(url);
id(http_request_ota).set_url(url.c_str());
ESP_LOGI("test", "MD5 %s / %s", id(http_request_ota).md5_expected().c_str(),
id(http_request_ota).md5_computed().c_str());
id(http_request_ota).flash();
update:
- platform: http_request
+13 -7
View File
@@ -595,8 +595,8 @@ class SensorStateCollector:
class SensorTracker:
"""Data-driven sensor state tracker with expected-value futures.
Tracks sensor and binary sensor state updates and resolves futures when
they report specific expected values. Eliminates per-sensor future
Tracks sensor, binary sensor and text sensor state updates and resolves
futures when they report specific expected values. Eliminates per-sensor future
boilerplate.
Usage::
@@ -608,12 +608,20 @@ class SensorTracker:
"""
def __init__(self, sensor_names: list[str]) -> None:
self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names}
self.sensor_states: dict[str, list[float | bool | str]] = {
name: [] for name in sensor_names
}
self.key_to_sensor: dict[int, str] = {}
self._expectations: dict[str, list[tuple[object, asyncio.Future]]] = {}
_ANY = object() # Sentinel: match any value
def map_entities(self, entities: list[EntityInfo]) -> None:
"""Map the tracked names to the keys of ``entities``."""
self.key_to_sensor.update(
build_key_to_entity_mapping(entities, list(self.sensor_states))
)
def expect(self, name: str, value: object) -> asyncio.Future:
"""Register an expected value for *name* and return a future for it."""
future: asyncio.Future = asyncio.get_running_loop().create_future()
@@ -638,7 +646,7 @@ class SensorTracker:
connect-time states so they cannot satisfy a later phase.
"""
if (
not isinstance(state, (SensorState, BinarySensorState))
not isinstance(state, (SensorState, BinarySensorState, TextSensorState))
or state.missing_state
):
return
@@ -708,9 +716,7 @@ class SensorTracker:
satisfy a later phase.
"""
entities, _ = await client.list_entities_services()
self.key_to_sensor.update(
build_key_to_entity_mapping(entities, list(self.sensor_states.keys()))
)
self.map_entities(entities)
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(initial_state_helper.on_state_wrapper(self.on_state))
await initial_state_helper.wait_for_initial_states()
@@ -6,7 +6,7 @@ from aioesphomeapi import connection as api_connection
from aioesphomeapi.api_pb2 import GetTimeResponse
import pytest
from .state_utils import SensorTracker, build_key_to_entity_mapping
from .state_utils import SensorTracker
from .types import APIClientConnectedFactory, RunCompiledFunction
# 2024-01-01 00:00:00 UTC
@@ -32,7 +32,7 @@ async def test_api_get_time_response_timezone(
async with run_compiled(yaml_config), api_client_connected() as client:
entities, _ = await client.list_entities_services()
tracker = SensorTracker(["tz_offset"])
tracker.key_to_sensor = build_key_to_entity_mapping(entities, ["tz_offset"])
tracker.map_entities(entities)
client.subscribe_states(tracker.on_state)
await tracker.await_change(tracker.expect_any("tz_offset"), "tz_offset")
+2 -8
View File
@@ -19,11 +19,7 @@ import asyncio
import pytest
from .state_utils import (
SensorTracker,
build_key_to_entity_mapping,
subscribe_after_initial_states,
)
from .state_utils import SensorTracker, subscribe_after_initial_states
from .types import APIClientConnectedFactory, RunCompiledFunction
# Time to let the host-mode main loop run so status_led_light.loop() can
@@ -67,9 +63,7 @@ async def test_status_flags(
"status_led_last_state",
]
)
tracker.key_to_sensor.update(
build_key_to_entity_mapping(entities, list(tracker.sensor_states.keys()))
)
tracker.map_entities(entities)
# Swallow initial state broadcasts so the test only reacts to
# state changes triggered by our service calls.
+8 -20
View File
@@ -2,14 +2,10 @@
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState, TextSensorState
import pytest
from .state_utils import build_key_to_entity_mapping, subscribe_after_initial_states
from .state_utils import SensorTracker, subscribe_after_initial_states
from .types import APIClientConnectedFactory, RunCompiledFunction
from .wait_utils import wait_or_fail
# Timestamps are exposed as text sensors (rather than 32-bit float sensor states,
# which cannot represent a UNIX epoch exactly) so the exact value can be checked.
@@ -30,18 +26,10 @@ async def test_template_time(
async with run_compiled(yaml_config), api_client_connected() as client:
entities, _ = await client.list_entities_services()
key_to_name = build_key_to_entity_mapping(entities, list(EXPECTED_STATES))
events = {name: asyncio.Event() for name in EXPECTED_STATES}
def on_state(state: EntityState) -> None:
if isinstance(state, TextSensorState) and not state.missing_state:
name = key_to_name.get(state.key)
if name is not None and state.state == EXPECTED_STATES[name]:
events[name].set()
await subscribe_after_initial_states(client, entities, on_state=on_state)
for name, event in events.items():
await wait_or_fail(
event, 3.0, f"Timeout waiting for {name} to report its expected value"
)
tracker = SensorTracker(list(EXPECTED_STATES))
tracker.map_entities(entities)
futures = tracker.expect_all(EXPECTED_STATES)
await subscribe_after_initial_states(
client, entities, on_state=tracker.on_state
)
await tracker.await_all(futures, timeout=3.0)
+2 -13
View File
@@ -386,7 +386,7 @@ def test_external_manifests_skip_other_components(
components_dir = tmp_path / "components"
get_sources().append(ExternalSource(components_dir=components_dir))
_add_external_component(components_dir, "widget")
_add_external_component(tmp_path / "pkg" / "components", "wifi")
_add_external_component(tmp_path / "builtin", "wifi")
CORE.loaded_integrations.update({"missing", "namespace"})
sbom.loader._COMPONENT_CACHE["namespace"] = SimpleNamespace(
module=SimpleNamespace()
@@ -399,9 +399,7 @@ def test_external_manifests_skip_other_components(
side_effect=sbom.loader._COMPONENT_CACHE.get,
),
patch.object(
sbom.loader,
"CORE_COMPONENTS_PATH",
(tmp_path / "pkg" / "components").resolve(),
sbom.loader, "CORE_COMPONENTS_PATH", (tmp_path / "builtin").resolve()
),
):
manifests = sbom._external_manifests()
@@ -705,12 +703,3 @@ def test_checkout_remote_detached(tmp_path: Path) -> None:
"https://user:token@github.com/someone/esphome.git",
)
assert not any(call.args[0][1] == "config" for call in run.call_args_list)
def test_external_manifests_esphome_core_is_built_in(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The esphome domain resolves to esphome/core, which is still part of ESPHome."""
CORE.loaded_integrations.add("esphome")
assert sbom._external_manifests() == []
assert "SBOM" not in caplog.text