174 lines
5.2 KiB
Python
174 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from app.ha.client import HaClient, HaClientSettings
|
|
from app.ha.exceptions import HaHttpError
|
|
from app.ha.reader import HaReader
|
|
|
|
|
|
class FakeHaClient(HaClient):
|
|
def __init__(self) -> None:
|
|
super().__init__(HaClientSettings(url="http://test", token="token"))
|
|
|
|
def list_entities(self) -> list[dict[str, object]]:
|
|
return [
|
|
{
|
|
"entity_id": "sensor.temperature",
|
|
"state": "21.5",
|
|
"last_changed": "2026-06-14T12:00:00+00:00",
|
|
"attributes": {
|
|
"state_class": "measurement",
|
|
"device_class": "temperature",
|
|
"unit_of_measurement": "°C",
|
|
},
|
|
},
|
|
{
|
|
"entity_id": "light.living_room",
|
|
"state": "on",
|
|
"attributes": {},
|
|
},
|
|
]
|
|
|
|
def get_history(
|
|
self,
|
|
entity_ids: list[str],
|
|
start_time: datetime,
|
|
end_time: datetime,
|
|
) -> list[object]:
|
|
return [
|
|
[
|
|
{
|
|
"entity_id": entity_ids[0],
|
|
"state": "21.5",
|
|
"last_changed": start_time.isoformat(),
|
|
}
|
|
]
|
|
]
|
|
|
|
def list_entity_metadata(self, entity_ids: list[str]) -> dict[str, dict[str, str | None]]:
|
|
return {
|
|
"sensor.temperature": {
|
|
"area_id": "kitchen",
|
|
"area_name": "Kueche",
|
|
"device_id": "device-1",
|
|
"device_name": "Thermometer",
|
|
}
|
|
}
|
|
|
|
def get_logbook(
|
|
self,
|
|
entity_id: str,
|
|
start_time: datetime,
|
|
end_time: datetime,
|
|
) -> list[object]:
|
|
return [
|
|
{
|
|
"entity_id": entity_id,
|
|
"when": start_time.isoformat(),
|
|
"message": "turned on",
|
|
"context_user_id": "user-1",
|
|
}
|
|
]
|
|
|
|
def call_service(
|
|
self,
|
|
domain: str,
|
|
service: str,
|
|
service_data: dict[str, object],
|
|
) -> list[object]:
|
|
return []
|
|
|
|
|
|
def test_ha_reader_returns_summaries() -> None:
|
|
reader = HaReader(FakeHaClient())
|
|
summaries = reader.read_entities()
|
|
assert len(summaries) == 2
|
|
domains = {summary.domain for summary in summaries}
|
|
assert domains == {"sensor", "light"}
|
|
sensor = next(item for item in summaries if item.entity_id == "sensor.temperature")
|
|
assert sensor.unit_of_measurement == "°C"
|
|
assert sensor.state == "21.5"
|
|
assert sensor.last_changed == datetime(2026, 6, 14, 12, 0, tzinfo=timezone.utc)
|
|
assert sensor.area_name == "Kueche"
|
|
assert sensor.device_name == "Thermometer"
|
|
|
|
|
|
def test_ha_reader_discovers_learnable_sensors() -> None:
|
|
reader = HaReader(FakeHaClient())
|
|
|
|
discovered = reader.discover(learnable=True)
|
|
|
|
assert [entity.entity_id for entity in discovered] == ["sensor.temperature"]
|
|
|
|
|
|
def test_ha_reader_normalizes_history() -> None:
|
|
reader = HaReader(FakeHaClient())
|
|
start = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
|
|
|
history = reader.read_history(
|
|
["sensor.temperature"],
|
|
start,
|
|
datetime(2026, 6, 2, tzinfo=timezone.utc),
|
|
)
|
|
|
|
assert history[0].entity_id == "sensor.temperature"
|
|
assert history[0].points[0].value == 21.5
|
|
|
|
|
|
def test_ha_reader_normalizes_state_history_and_logbook() -> None:
|
|
reader = HaReader(FakeHaClient())
|
|
start = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
|
end = datetime(2026, 6, 2, tzinfo=timezone.utc)
|
|
|
|
history = reader.read_state_history(["light.living_room"], start, end)
|
|
logbook = reader.read_logbook("light.living_room", start, end)
|
|
|
|
assert history[0].points[0].state == "21.5"
|
|
assert logbook[0].context_user_id == "user-1"
|
|
|
|
|
|
def test_ha_reader_finds_automation_that_targets_entity() -> None:
|
|
client = FakeHaClient()
|
|
client.list_entities = lambda: [ # type: ignore[method-assign]
|
|
{
|
|
"entity_id": "automation.storage_light",
|
|
"state": "on",
|
|
"attributes": {
|
|
"id": "123",
|
|
"friendly_name": "Storage light",
|
|
},
|
|
}
|
|
]
|
|
client.get_automation_config = lambda automation_id: { # type: ignore[method-assign]
|
|
"id": automation_id,
|
|
"target": {"entity_id": "light.storage"},
|
|
}
|
|
reader = HaReader(client)
|
|
|
|
matches = reader.find_automations_for_entity("light.storage")
|
|
|
|
assert len(matches) == 1
|
|
assert matches[0].entity_id == "automation.storage_light"
|
|
assert matches[0].enabled is True
|
|
|
|
|
|
def test_ha_reader_ignores_automation_configs_not_exposed_by_ha() -> None:
|
|
client = FakeHaClient()
|
|
client.list_entities = lambda: [ # type: ignore[method-assign]
|
|
{
|
|
"entity_id": "automation.storage_light",
|
|
"state": "on",
|
|
"attributes": {
|
|
"id": "123",
|
|
"friendly_name": "Storage light",
|
|
},
|
|
}
|
|
]
|
|
client.get_automation_config = lambda automation_id: (_ for _ in ()).throw( # type: ignore[method-assign]
|
|
HaHttpError(404, "Resource not found")
|
|
)
|
|
reader = HaReader(client)
|
|
|
|
assert reader.find_automations_for_entity("light.storage") == []
|