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/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

View file

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

View file

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

View file

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

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.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(

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"