Add unit tests for sensors Electric Kiwi (#97723)

* add unit tests for sensors

* newline long strings

* unit test check and move time

* rename entry to entity

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* add types to test

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* fix newlined f strings

* remove if statement

* add some more explaination

* Update datetime

Co-authored-by: Robert Resch <robert@resch.dev>

* Simpler time update

Co-authored-by: Robert Resch <robert@resch.dev>

* add missing datetime import

* Update docustring - grammar

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* address comments and issues raised

* address docstrings too long

* Fix docstring

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Michael Arthur 2023-09-11 11:30:25 +12:00 committed by GitHub
parent 6c45f43c5d
commit 8beace265b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 442 additions and 21 deletions

View File

@ -277,7 +277,6 @@ omit =
homeassistant/components/electric_kiwi/__init__.py homeassistant/components/electric_kiwi/__init__.py
homeassistant/components/electric_kiwi/api.py homeassistant/components/electric_kiwi/api.py
homeassistant/components/electric_kiwi/oauth2.py homeassistant/components/electric_kiwi/oauth2.py
homeassistant/components/electric_kiwi/sensor.py
homeassistant/components/electric_kiwi/coordinator.py homeassistant/components/electric_kiwi/coordinator.py
homeassistant/components/electric_kiwi/select.py homeassistant/components/electric_kiwi/select.py
homeassistant/components/eliqonline/sensor.py homeassistant/components/eliqonline/sensor.py

View File

@ -50,7 +50,10 @@ class ElectricKiwiSelectHOPEntity(
) -> None: ) -> None:
"""Initialise the HOP selection entity.""" """Initialise the HOP selection entity."""
super().__init__(coordinator) 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.entity_description = description
self.values_dict = coordinator.get_hop_options() self.values_dict = coordinator.get_hop_options()
self._attr_options = list(self.values_dict) self._attr_options = list(self.values_dict)
@ -58,7 +61,10 @@ class ElectricKiwiSelectHOPEntity(
@property @property
def current_option(self) -> str | None: def current_option(self) -> str | None:
"""Return the currently selected option.""" """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: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""

View File

@ -62,7 +62,7 @@ def _check_and_move_time(hop: Hop, time: str) -> datetime:
return date_time return date_time
HOP_SENSOR_TYPE: tuple[ElectricKiwiHOPSensorEntityDescription, ...] = ( HOP_SENSOR_TYPES: tuple[ElectricKiwiHOPSensorEntityDescription, ...] = (
ElectricKiwiHOPSensorEntityDescription( ElectricKiwiHOPSensorEntityDescription(
key=ATTR_EK_HOP_START, key=ATTR_EK_HOP_START,
translation_key="hopfreepowerstart", translation_key="hopfreepowerstart",
@ -85,7 +85,7 @@ async def async_setup_entry(
hop_coordinator: ElectricKiwiHOPDataCoordinator = hass.data[DOMAIN][entry.entry_id] hop_coordinator: ElectricKiwiHOPDataCoordinator = hass.data[DOMAIN][entry.entry_id]
hop_entities = [ hop_entities = [
ElectricKiwiHOPEntity(hop_coordinator, description) ElectricKiwiHOPEntity(hop_coordinator, description)
for description in HOP_SENSOR_TYPE for description in HOP_SENSOR_TYPES
] ]
async_add_entities(hop_entities) async_add_entities(hop_entities)
@ -107,7 +107,10 @@ class ElectricKiwiHOPEntity(
"""Entity object for Electric Kiwi sensor.""" """Entity object for Electric Kiwi sensor."""
super().__init__(coordinator) 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.entity_description = description
@property @property

View File

@ -1,9 +1,12 @@
"""Define fixtures for electric kiwi tests.""" """Define fixtures for electric kiwi tests."""
from __future__ import annotations 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 from unittest.mock import AsyncMock, patch
import zoneinfo
from electrickiwi_api.model import Hop, HopIntervals
import pytest import pytest
from homeassistant.components.application_credentials import ( 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.core import HomeAssistant
from homeassistant.setup import async_setup_component 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_ID = "1234"
CLIENT_SECRET = "5678" CLIENT_SECRET = "5678"
REDIRECT_URI = "https://example.com/auth/external/callback" 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) @pytest.fixture(autouse=True)
async def request_setup(current_request_with_host) -> None: async def request_setup(current_request_with_host) -> None:
@ -28,14 +36,23 @@ async def request_setup(current_request_with_host) -> None:
@pytest.fixture @pytest.fixture
async def setup_credentials(hass: HomeAssistant) -> None: def component_setup(
"""Fixture to setup credentials.""" hass: HomeAssistant, config_entry: MockConfigEntry
assert await async_setup_component(hass, "application_credentials", {}) ) -> ComponentSetup:
await async_import_client_credential( """Fixture for setting up the integration."""
hass,
DOMAIN, async def _setup_func() -> bool:
ClientCredential(CLIENT_ID, CLIENT_SECRET), 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") @pytest.fixture(name="config_entry")
@ -45,12 +62,18 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
title="Electric Kiwi", title="Electric Kiwi",
domain=DOMAIN, domain=DOMAIN,
data={ data={
"id": "mock_user", "id": "12345",
"auth_implementation": DOMAIN, "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, unique_id=DOMAIN,
) )
entry.add_to_hass(hass)
return entry return entry
@ -61,3 +84,33 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"homeassistant.components.electric_kiwi.async_setup_entry", return_value=True "homeassistant.components.electric_kiwi.async_setup_entry", return_value=True
) as mock_setup: ) as mock_setup:
yield 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

View File

@ -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
}

View File

@ -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
}

View File

@ -21,6 +21,7 @@ from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.setup import async_setup_component
from .conftest import CLIENT_ID, CLIENT_SECRET, REDIRECT_URI 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") 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: async def test_config_flow_no_credentials(hass: HomeAssistant) -> None:
"""Test config flow base case with no credentials registered.""" """Test config flow base case with no credentials registered."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -45,12 +57,12 @@ async def test_full_flow(
hass_client_no_auth: ClientSessionGenerator, hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
current_request_with_host: None, current_request_with_host: None,
setup_credentials, setup_credentials: None,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
) -> None: ) -> None:
"""Check full flow.""" """Check full flow."""
await async_import_client_credential( 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( result = await hass.config_entries.flow.async_init(
@ -103,7 +115,7 @@ async def test_existing_entry(
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Check existing entry.""" """Check existing entry."""
config_entry.add_to_hass(hass)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View File

@ -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"