from __future__ import annotations from datetime import datetime, timezone from unittest.mock import Mock import pytest import requests from app.ha.client import HaClient, HaClientSettings from app.ha.exceptions import ( HaAuthError, HaHttpError, HaTimeoutError, HaUnexpectedPayloadError, ) def _client_with_response(response: Mock) -> HaClient: client = HaClient(HaClientSettings(url="http://ha.local", token="test-token")) client._session.get = Mock(return_value=response) # type: ignore[method-assign] return client def _response(status_code: int = 200, payload: object | None = None) -> Mock: response = Mock() response.status_code = status_code response.json.return_value = [] if payload is None else payload if status_code >= 400: response.raise_for_status.side_effect = requests.HTTPError("upstream failed") return response def test_list_entities_returns_home_assistant_payload() -> None: payload = [{"entity_id": "sensor.temperature", "state": "21"}] client = _client_with_response(_response(payload=payload)) assert client.list_entities() == payload def test_list_entities_maps_timeout() -> None: client = HaClient(HaClientSettings(url="http://ha.local", token="test-token")) client._session.get = Mock(side_effect=requests.Timeout("timed out")) # type: ignore[method-assign] with pytest.raises(HaTimeoutError): client.list_entities() @pytest.mark.parametrize("status_code", [401, 403]) def test_list_entities_maps_auth_errors(status_code: int) -> None: client = _client_with_response(_response(status_code=status_code)) with pytest.raises(HaAuthError) as exc_info: client.list_entities() assert exc_info.value.status_code == status_code def test_list_entities_maps_http_errors() -> None: client = _client_with_response(_response(status_code=500)) with pytest.raises(HaHttpError) as exc_info: client.list_entities() assert exc_info.value.status_code == 500 def test_list_entities_rejects_invalid_json() -> None: response = _response() response.json.side_effect = ValueError("not json") client = _client_with_response(response) with pytest.raises(HaUnexpectedPayloadError): client.list_entities() def test_list_entities_rejects_non_list_payload() -> None: client = _client_with_response(_response(payload={"entity_id": "sensor.temperature"})) with pytest.raises(HaUnexpectedPayloadError): client.list_entities() def test_get_history_calls_home_assistant_history_api() -> None: response = _response(payload=[[{"entity_id": "sensor.temperature", "state": "21.0"}]]) client = _client_with_response(response) start = datetime(2026, 6, 1, tzinfo=timezone.utc) end = datetime(2026, 6, 2, tzinfo=timezone.utc) payload = client.get_history(["sensor.temperature"], start, end) assert payload == [[{"entity_id": "sensor.temperature", "state": "21.0"}]] client._session.get.assert_called_once() # type: ignore[attr-defined] call = client._session.get.call_args # type: ignore[attr-defined] assert "/api/history/period/2026-06-01T00:00:00+00:00" in call.args[0] assert call.kwargs["params"]["filter_entity_id"] == "sensor.temperature" assert call.kwargs["params"]["end_time"] == "2026-06-02T00:00:00+00:00" def test_list_entity_metadata_calls_template_api() -> None: response = _response() response.text = ( '[{"entity_id":"sensor.temperature","area_name":"Kueche","device_name":"Thermometer"}]' ) client = HaClient(HaClientSettings(url="http://ha.local", token="test-token")) client._session.post = Mock(return_value=response) # type: ignore[method-assign] metadata = client.list_entity_metadata(["sensor.temperature"]) assert metadata == { "sensor.temperature": { "area_id": None, "area_name": "Kueche", "device_id": None, "device_name": "Thermometer", } } @pytest.mark.parametrize( ("entity_ids", "start", "end"), [ ( [], datetime(2026, 6, 1, tzinfo=timezone.utc), datetime(2026, 6, 2, tzinfo=timezone.utc), ), ( ["sensor.temperature"], datetime(2026, 6, 1), datetime(2026, 6, 2, tzinfo=timezone.utc), ), ( ["sensor.temperature"], datetime(2026, 6, 2, tzinfo=timezone.utc), datetime(2026, 6, 1, tzinfo=timezone.utc), ), ( ["invalid entity"], datetime(2026, 6, 1, tzinfo=timezone.utc), datetime(2026, 6, 2, tzinfo=timezone.utc), ), ( ["sensor.temperature"], datetime(2026, 5, 1, tzinfo=timezone.utc), datetime(2026, 6, 2, tzinfo=timezone.utc), ), ], ) def test_get_history_validates_request( entity_ids: list[str], start: datetime, end: datetime, ) -> None: client = HaClient(HaClientSettings(url="http://ha.local", token="test-token")) with pytest.raises(ValueError): client.get_history(entity_ids, start, end)