from __future__ import annotations from datetime import datetime, timezone import pytest from app.ha.exceptions import HaUnexpectedPayloadError from app.ha.history import ( normalize_history_payload, normalize_logbook_payload, normalize_state_history_payload, ) def test_normalize_history_payload_groups_and_sorts_numeric_states() -> None: payload = [ [ { "entity_id": "sensor.temperature", "state": "22.5", "last_changed": "2026-06-01T12:15:00+00:00", }, { "state": "21.0", "last_changed": "2026-06-01T12:00:00Z", }, ], [ { "entity_id": "sensor.humidity", "state": 45, "last_updated": "2026-06-01T12:00:00+00:00", } ], ] result = normalize_history_payload(payload) assert [series.entity_id for series in result] == [ "sensor.humidity", "sensor.temperature", ] temperature = result[1] assert [point.value for point in temperature.points] == [21.0, 22.5] assert temperature.points[0].timestamp == datetime( 2026, 6, 1, 12, 0, tzinfo=timezone.utc ) def test_normalize_history_payload_skips_non_numeric_and_non_finite_states() -> None: payload = [ [ { "entity_id": "sensor.temperature", "state": state, "last_changed": "2026-06-01T12:00:00+00:00", } for state in ("unknown", "unavailable", "nan", "inf", "-inf", True, None) ] ] assert normalize_history_payload(payload) == [] @pytest.mark.parametrize( "payload", [ {}, [{}], [["invalid"]], [[{"entity_id": "invalid", "state": "21", "last_changed": "2026-06-01"}]], [[{"entity_id": "sensor.a", "state": "21", "last_changed": "invalid"}]], [[{"state": "21", "last_changed": "2026-06-01T12:00:00+00:00"}]], [ [ { "entity_id": "sensor.a", "state": "21", "last_changed": "2026-06-01T12:00:00+00:00", }, { "entity_id": "sensor.b", "state": "22", "last_changed": "2026-06-01T12:01:00+00:00", }, ] ], ], ) def test_normalize_history_payload_rejects_malformed_structure(payload: object) -> None: with pytest.raises(HaUnexpectedPayloadError): normalize_history_payload(payload) def test_normalize_history_payload_accepts_empty_series() -> None: assert normalize_history_payload([[]]) == [] def test_normalize_state_history_keeps_categorical_changes() -> None: result = normalize_state_history_payload( [ [ { "entity_id": "light.office", "state": "off", "last_changed": "2026-06-01T08:00:00+00:00", }, { "state": "on", "last_changed": "2026-06-01T08:05:00+00:00", }, { "state": "on", "last_changed": "2026-06-01T08:06:00+00:00", }, ] ] ) assert [point.state for point in result[0].points] == ["off", "on"] def test_normalize_logbook_preserves_action_origin() -> None: result = normalize_logbook_payload( [ { "entity_id": "light.office", "when": "2026-06-01T08:05:00+00:00", "message": "turned on", "context_user_id": "user-1", "context_domain": "light", "context_service": "turn_on", } ], "light.office", ) assert result[0].context_user_id == "user-1" assert result[0].context_service == "turn_on"