diff --git a/.coveragerc b/.coveragerc index e932be670cb..4df91b250ed 100644 --- a/.coveragerc +++ b/.coveragerc @@ -277,7 +277,6 @@ omit = homeassistant/components/electric_kiwi/__init__.py homeassistant/components/electric_kiwi/api.py homeassistant/components/electric_kiwi/oauth2.py - homeassistant/components/electric_kiwi/sensor.py homeassistant/components/electric_kiwi/coordinator.py homeassistant/components/electric_kiwi/select.py homeassistant/components/eliqonline/sensor.py diff --git a/homeassistant/components/electric_kiwi/select.py b/homeassistant/components/electric_kiwi/select.py index 9d883c72d1e..eb8aaac8c2f 100644 --- a/homeassistant/components/electric_kiwi/select.py +++ b/homeassistant/components/electric_kiwi/select.py @@ -50,7 +50,10 @@ class ElectricKiwiSelectHOPEntity( ) -> None: """Initialise the HOP selection entity.""" super().__init__(coordinator) - self._attr_unique_id = f"{coordinator._ek_api.customer_number}_{coordinator._ek_api.connection_id}_{description.key}" + self._attr_unique_id = ( + f"{coordinator._ek_api.customer_number}" + f"_{coordinator._ek_api.connection_id}_{description.key}" + ) self.entity_description = description self.values_dict = coordinator.get_hop_options() self._attr_options = list(self.values_dict) @@ -58,7 +61,10 @@ class ElectricKiwiSelectHOPEntity( @property def current_option(self) -> str | None: """Return the currently selected option.""" - return f"{self.coordinator.data.start.start_time} - {self.coordinator.data.end.end_time}" + return ( + f"{self.coordinator.data.start.start_time}" + f" - {self.coordinator.data.end.end_time}" + ) async def async_select_option(self, option: str) -> None: """Change the selected option.""" diff --git a/homeassistant/components/electric_kiwi/sensor.py b/homeassistant/components/electric_kiwi/sensor.py index 8c983b92dd5..8017bbf006e 100644 --- a/homeassistant/components/electric_kiwi/sensor.py +++ b/homeassistant/components/electric_kiwi/sensor.py @@ -62,7 +62,7 @@ def _check_and_move_time(hop: Hop, time: str) -> datetime: return date_time -HOP_SENSOR_TYPE: tuple[ElectricKiwiHOPSensorEntityDescription, ...] = ( +HOP_SENSOR_TYPES: tuple[ElectricKiwiHOPSensorEntityDescription, ...] = ( ElectricKiwiHOPSensorEntityDescription( key=ATTR_EK_HOP_START, translation_key="hopfreepowerstart", @@ -85,7 +85,7 @@ async def async_setup_entry( hop_coordinator: ElectricKiwiHOPDataCoordinator = hass.data[DOMAIN][entry.entry_id] hop_entities = [ ElectricKiwiHOPEntity(hop_coordinator, description) - for description in HOP_SENSOR_TYPE + for description in HOP_SENSOR_TYPES ] async_add_entities(hop_entities) @@ -107,7 +107,10 @@ class ElectricKiwiHOPEntity( """Entity object for Electric Kiwi sensor.""" super().__init__(coordinator) - self._attr_unique_id = f"{coordinator._ek_api.customer_number}_{coordinator._ek_api.connection_id}_{description.key}" + self._attr_unique_id = ( + f"{coordinator._ek_api.customer_number}" + f"_{coordinator._ek_api.connection_id}_{description.key}" + ) self.entity_description = description @property diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index 525f5742382..f7e60e975f8 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -1,9 +1,12 @@ """Define fixtures for electric kiwi tests.""" from __future__ import annotations -from collections.abc import Generator +from collections.abc import Awaitable, Callable, Generator +from time import time from unittest.mock import AsyncMock, patch +import zoneinfo +from electrickiwi_api.model import Hop, HopIntervals import pytest from homeassistant.components.application_credentials import ( @@ -14,12 +17,17 @@ from homeassistant.components.electric_kiwi.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_value_fixture CLIENT_ID = "1234" CLIENT_SECRET = "5678" REDIRECT_URI = "https://example.com/auth/external/callback" +TZ_NAME = "Pacific/Auckland" +TIMEZONE = zoneinfo.ZoneInfo(TZ_NAME) +YieldFixture = Generator[AsyncMock, None, None] +ComponentSetup = Callable[[], Awaitable[bool]] + @pytest.fixture(autouse=True) async def request_setup(current_request_with_host) -> None: @@ -28,14 +36,23 @@ async def request_setup(current_request_with_host) -> None: @pytest.fixture -async def setup_credentials(hass: HomeAssistant) -> None: - """Fixture to setup credentials.""" - assert await async_setup_component(hass, "application_credentials", {}) - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential(CLIENT_ID, CLIENT_SECRET), - ) +def component_setup( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> ComponentSetup: + """Fixture for setting up the integration.""" + + async def _setup_func() -> bool: + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + DOMAIN, + ) + config_entry.add_to_hass(hass) + return await hass.config_entries.async_setup(config_entry.entry_id) + + return _setup_func @pytest.fixture(name="config_entry") @@ -45,12 +62,18 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: title="Electric Kiwi", domain=DOMAIN, data={ - "id": "mock_user", + "id": "12345", "auth_implementation": DOMAIN, + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "expires_at": time() + 60, + }, }, unique_id=DOMAIN, ) - entry.add_to_hass(hass) return entry @@ -61,3 +84,33 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: "homeassistant.components.electric_kiwi.async_setup_entry", return_value=True ) as mock_setup: yield mock_setup + + +@pytest.fixture(name="ek_auth") +def electric_kiwi_auth() -> YieldFixture: + """Patch access to electric kiwi access token.""" + with patch( + "homeassistant.components.electric_kiwi.api.AsyncConfigEntryAuth" + ) as mock_auth: + mock_auth.return_value.async_get_access_token = AsyncMock("auth_token") + yield mock_auth + + +@pytest.fixture(name="ek_api") +def ek_api() -> YieldFixture: + """Mock ek api and return values.""" + with patch( + "homeassistant.components.electric_kiwi.ElectricKiwiApi", autospec=True + ) as mock_ek_api: + mock_ek_api.return_value.customer_number = 123456 + mock_ek_api.return_value.connection_id = 123456 + mock_ek_api.return_value.set_active_session.return_value = None + mock_ek_api.return_value.get_hop_intervals.return_value = ( + HopIntervals.from_dict( + load_json_value_fixture("hop_intervals.json", DOMAIN) + ) + ) + mock_ek_api.return_value.get_hop.return_value = Hop.from_dict( + load_json_value_fixture("get_hop.json", DOMAIN) + ) + yield mock_ek_api diff --git a/tests/components/electric_kiwi/fixtures/get_hop.json b/tests/components/electric_kiwi/fixtures/get_hop.json new file mode 100644 index 00000000000..d29825391e9 --- /dev/null +++ b/tests/components/electric_kiwi/fixtures/get_hop.json @@ -0,0 +1,16 @@ +{ + "data": { + "connection_id": "3", + "customer_number": 1000001, + "end": { + "end_time": "5:00 PM", + "interval": "34" + }, + "start": { + "start_time": "4:00 PM", + "interval": "33" + }, + "type": "hop_customer" + }, + "status": 1 +} diff --git a/tests/components/electric_kiwi/fixtures/hop_intervals.json b/tests/components/electric_kiwi/fixtures/hop_intervals.json new file mode 100644 index 00000000000..15ecc174f13 --- /dev/null +++ b/tests/components/electric_kiwi/fixtures/hop_intervals.json @@ -0,0 +1,249 @@ +{ + "data": { + "hop_duration": "60", + "type": "hop_intervals", + "intervals": { + "1": { + "active": 1, + "end_time": "1:00 AM", + "start_time": "12:00 AM" + }, + "2": { + "active": 1, + "end_time": "1:30 AM", + "start_time": "12:30 AM" + }, + "3": { + "active": 1, + "end_time": "2:00 AM", + "start_time": "1:00 AM" + }, + "4": { + "active": 1, + "end_time": "2:30 AM", + "start_time": "1:30 AM" + }, + "5": { + "active": 1, + "end_time": "3:00 AM", + "start_time": "2:00 AM" + }, + "6": { + "active": 1, + "end_time": "3:30 AM", + "start_time": "2:30 AM" + }, + "7": { + "active": 1, + "end_time": "4:00 AM", + "start_time": "3:00 AM" + }, + "8": { + "active": 1, + "end_time": "4:30 AM", + "start_time": "3:30 AM" + }, + "9": { + "active": 1, + "end_time": "5:00 AM", + "start_time": "4:00 AM" + }, + "10": { + "active": 1, + "end_time": "5:30 AM", + "start_time": "4:30 AM" + }, + "11": { + "active": 1, + "end_time": "6:00 AM", + "start_time": "5:00 AM" + }, + "12": { + "active": 1, + "end_time": "6:30 AM", + "start_time": "5:30 AM" + }, + "13": { + "active": 1, + "end_time": "7:00 AM", + "start_time": "6:00 AM" + }, + "14": { + "active": 1, + "end_time": "7:30 AM", + "start_time": "6:30 AM" + }, + "15": { + "active": 1, + "end_time": "8:00 AM", + "start_time": "7:00 AM" + }, + "16": { + "active": 1, + "end_time": "8:30 AM", + "start_time": "7:30 AM" + }, + "17": { + "active": 1, + "end_time": "9:00 AM", + "start_time": "8:00 AM" + }, + "18": { + "active": 1, + "end_time": "9:30 AM", + "start_time": "8:30 AM" + }, + "19": { + "active": 1, + "end_time": "10:00 AM", + "start_time": "9:00 AM" + }, + "20": { + "active": 1, + "end_time": "10:30 AM", + "start_time": "9:30 AM" + }, + "21": { + "active": 1, + "end_time": "11:00 AM", + "start_time": "10:00 AM" + }, + "22": { + "active": 1, + "end_time": "11:30 AM", + "start_time": "10:30 AM" + }, + "23": { + "active": 1, + "end_time": "12:00 PM", + "start_time": "11:00 AM" + }, + "24": { + "active": 1, + "end_time": "12:30 PM", + "start_time": "11:30 AM" + }, + "25": { + "active": 1, + "end_time": "1:00 PM", + "start_time": "12:00 PM" + }, + "26": { + "active": 1, + "end_time": "1:30 PM", + "start_time": "12:30 PM" + }, + "27": { + "active": 1, + "end_time": "2:00 PM", + "start_time": "1:00 PM" + }, + "28": { + "active": 1, + "end_time": "2:30 PM", + "start_time": "1:30 PM" + }, + "29": { + "active": 1, + "end_time": "3:00 PM", + "start_time": "2:00 PM" + }, + "30": { + "active": 1, + "end_time": "3:30 PM", + "start_time": "2:30 PM" + }, + "31": { + "active": 1, + "end_time": "4:00 PM", + "start_time": "3:00 PM" + }, + "32": { + "active": 1, + "end_time": "4:30 PM", + "start_time": "3:30 PM" + }, + "33": { + "active": 1, + "end_time": "5:00 PM", + "start_time": "4:00 PM" + }, + "34": { + "active": 1, + "end_time": "5:30 PM", + "start_time": "4:30 PM" + }, + "35": { + "active": 1, + "end_time": "6:00 PM", + "start_time": "5:00 PM" + }, + "36": { + "active": 1, + "end_time": "6:30 PM", + "start_time": "5:30 PM" + }, + "37": { + "active": 1, + "end_time": "7:00 PM", + "start_time": "6:00 PM" + }, + "38": { + "active": 1, + "end_time": "7:30 PM", + "start_time": "6:30 PM" + }, + "39": { + "active": 1, + "end_time": "8:00 PM", + "start_time": "7:00 PM" + }, + "40": { + "active": 1, + "end_time": "8:30 PM", + "start_time": "7:30 PM" + }, + "41": { + "active": 1, + "end_time": "9:00 PM", + "start_time": "8:00 PM" + }, + "42": { + "active": 1, + "end_time": "9:30 PM", + "start_time": "8:30 PM" + }, + "43": { + "active": 1, + "end_time": "10:00 PM", + "start_time": "9:00 PM" + }, + "44": { + "active": 1, + "end_time": "10:30 PM", + "start_time": "9:30 PM" + }, + "45": { + "active": 1, + "end_time": "11:00 AM", + "start_time": "10:00 PM" + }, + "46": { + "active": 1, + "end_time": "11:30 PM", + "start_time": "10:30 PM" + }, + "47": { + "active": 1, + "end_time": "12:00 AM", + "start_time": "11:00 PM" + }, + "48": { + "active": 1, + "end_time": "12:30 AM", + "start_time": "11:30 PM" + } + } + }, + "status": 1 +} diff --git a/tests/components/electric_kiwi/test_config_flow.py b/tests/components/electric_kiwi/test_config_flow.py index 51d00722341..1199c3e555a 100644 --- a/tests/components/electric_kiwi/test_config_flow.py +++ b/tests/components/electric_kiwi/test_config_flow.py @@ -21,6 +21,7 @@ from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from .conftest import CLIENT_ID, CLIENT_SECRET, REDIRECT_URI @@ -31,6 +32,17 @@ from tests.typing import ClientSessionGenerator pytestmark = pytest.mark.usefixtures("mock_setup_entry") +@pytest.fixture +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup application credentials component.""" + await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + + async def test_config_flow_no_credentials(hass: HomeAssistant) -> None: """Test config flow base case with no credentials registered.""" result = await hass.config_entries.flow.async_init( @@ -45,12 +57,12 @@ async def test_full_flow( hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, current_request_with_host: None, - setup_credentials, + setup_credentials: None, mock_setup_entry: AsyncMock, ) -> None: """Check full flow.""" await async_import_client_credential( - hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET) ) result = await hass.config_entries.flow.async_init( @@ -103,7 +115,7 @@ async def test_existing_entry( config_entry: MockConfigEntry, ) -> None: """Check existing entry.""" - + config_entry.add_to_hass(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 result = await hass.config_entries.flow.async_init( diff --git a/tests/components/electric_kiwi/test_sensor.py b/tests/components/electric_kiwi/test_sensor.py new file mode 100644 index 00000000000..ef268735334 --- /dev/null +++ b/tests/components/electric_kiwi/test_sensor.py @@ -0,0 +1,83 @@ +"""The tests for Electric Kiwi sensors.""" + + +from datetime import UTC, datetime +from unittest.mock import AsyncMock, Mock + +from freezegun import freeze_time +import pytest + +from homeassistant.components.electric_kiwi.const import ATTRIBUTION +from homeassistant.components.electric_kiwi.sensor import _check_and_move_time +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry +import homeassistant.util.dt as dt_util + +from .conftest import TIMEZONE, ComponentSetup, YieldFixture + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("sensor", "sensor_state"), + [ + ("sensor.hour_of_free_power_start", "4:00 PM"), + ("sensor.hour_of_free_power_end", "5:00 PM"), + ], +) +async def test_hop_sensors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + ek_api: YieldFixture, + ek_auth: YieldFixture, + entity_registry: EntityRegistry, + component_setup: ComponentSetup, + sensor: str, + sensor_state: str, +) -> None: + """Test HOP sensors for the Electric Kiwi integration. + + This time (note no day is given, it's only a time) is fed + from the Electric Kiwi API. if the API returns 4:00 PM, the + sensor state should be set to today at 4pm or if now is past 4pm, + then tomorrow at 4pm. + """ + assert await component_setup() + assert config_entry.state is ConfigEntryState.LOADED + + entity = entity_registry.async_get(sensor) + assert entity + + state = hass.states.get(sensor) + assert state + + api = ek_api(Mock()) + hop_data = await api.get_hop() + + value = _check_and_move_time(hop_data, sensor_state) + + value = value.astimezone(UTC) + assert state.state == value.isoformat(timespec="seconds") + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + + +async def test_check_and_move_time(ek_api: AsyncMock) -> None: + """Test correct time is returned depending on time of day.""" + hop = await ek_api(Mock()).get_hop() + + test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TIMEZONE) + dt_util.set_default_time_zone(TIMEZONE) + + with freeze_time(test_time): + value = _check_and_move_time(hop, "4:00 PM") + assert str(value) == "2023-06-22 16:00:00+12:00" + + test_time = test_time.replace(hour=10) + + with freeze_time(test_time): + value = _check_and_move_time(hop, "4:00 PM") + assert str(value) == "2023-06-21 16:00:00+12:00"