mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Change Electric Kiwi authentication (#135231)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
67c6a1d436
commit
8504162539
@ -4,12 +4,16 @@ from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
from electrickiwi_api import ElectricKiwiApi
|
||||
from electrickiwi_api.exceptions import ApiException
|
||||
from electrickiwi_api.exceptions import ApiException, AuthException
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
entity_registry as er,
|
||||
)
|
||||
|
||||
from . import api
|
||||
from .coordinator import (
|
||||
@ -44,7 +48,9 @@ async def async_setup_entry(
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
ek_api = ElectricKiwiApi(
|
||||
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
||||
api.ConfigEntryElectricKiwiAuth(
|
||||
aiohttp_client.async_get_clientsession(hass), session
|
||||
)
|
||||
)
|
||||
hop_coordinator = ElectricKiwiHOPDataCoordinator(hass, entry, ek_api)
|
||||
account_coordinator = ElectricKiwiAccountDataCoordinator(hass, entry, ek_api)
|
||||
@ -53,6 +59,8 @@ async def async_setup_entry(
|
||||
await ek_api.set_active_session()
|
||||
await hop_coordinator.async_config_entry_first_refresh()
|
||||
await account_coordinator.async_config_entry_first_refresh()
|
||||
except AuthException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except ApiException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
@ -70,3 +78,53 @@ async def async_unload_entry(
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: ElectricKiwiConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
if config_entry.version == 1 and config_entry.minor_version == 1:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, config_entry
|
||||
)
|
||||
)
|
||||
|
||||
session = config_entry_oauth2_flow.OAuth2Session(
|
||||
hass, config_entry, implementation
|
||||
)
|
||||
|
||||
ek_api = ElectricKiwiApi(
|
||||
api.ConfigEntryElectricKiwiAuth(
|
||||
aiohttp_client.async_get_clientsession(hass), session
|
||||
)
|
||||
)
|
||||
try:
|
||||
await ek_api.set_active_session()
|
||||
connection_details = await ek_api.get_connection_details()
|
||||
except AuthException:
|
||||
config_entry.async_start_reauth(hass)
|
||||
return False
|
||||
except ApiException:
|
||||
return False
|
||||
unique_id = str(ek_api.customer_number)
|
||||
identifier = ek_api.electricity.identifier
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=unique_id, minor_version=2
|
||||
)
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
for entity in entity_entries:
|
||||
assert entity.config_entry_id
|
||||
entity_registry.async_update_entity(
|
||||
entity.entity_id,
|
||||
new_unique_id=entity.unique_id.replace(
|
||||
f"{unique_id}_{connection_details.id}", f"{unique_id}_{identifier}"
|
||||
),
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -2,17 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from electrickiwi_api import AbstractAuth
|
||||
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
|
||||
from .const import API_BASE_URL
|
||||
|
||||
|
||||
class AsyncConfigEntryAuth(AbstractAuth):
|
||||
class ConfigEntryElectricKiwiAuth(AbstractAuth):
|
||||
"""Provide Electric Kiwi authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
@ -29,4 +28,21 @@ class AsyncConfigEntryAuth(AbstractAuth):
|
||||
"""Return a valid access token."""
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return cast(str, self._oauth_session.token["access_token"])
|
||||
return str(self._oauth_session.token["access_token"])
|
||||
|
||||
|
||||
class ConfigFlowElectricKiwiAuth(AbstractAuth):
|
||||
"""Provide Electric Kiwi authentication tied to an OAuth2 based config flow."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
token: str,
|
||||
) -> None:
|
||||
"""Initialize ConfigFlowFitbitApi."""
|
||||
super().__init__(aiohttp_client.async_get_clientsession(hass), API_BASE_URL)
|
||||
self._token = token
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return the token for the Electric Kiwi API."""
|
||||
return self._token
|
||||
|
@ -6,9 +6,14 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from electrickiwi_api import ElectricKiwiApi
|
||||
from electrickiwi_api.exceptions import ApiException
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN, SCOPE_VALUES
|
||||
|
||||
|
||||
@ -17,6 +22,8 @@ class ElectricKiwiOauth2FlowHandler(
|
||||
):
|
||||
"""Config flow to handle Electric Kiwi OAuth2 authentication."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
@property
|
||||
@ -40,12 +47,30 @@ class ElectricKiwiOauth2FlowHandler(
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={CONF_NAME: self._get_reauth_entry().title},
|
||||
)
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Create an entry for Electric Kiwi."""
|
||||
existing_entry = await self.async_set_unique_id(DOMAIN)
|
||||
if existing_entry:
|
||||
return self.async_update_reload_and_abort(existing_entry, data=data)
|
||||
return await super().async_oauth_create_entry(data)
|
||||
ek_api = ElectricKiwiApi(
|
||||
api.ConfigFlowElectricKiwiAuth(self.hass, data["token"]["access_token"])
|
||||
)
|
||||
|
||||
try:
|
||||
session = await ek_api.get_active_session()
|
||||
except ApiException:
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
||||
unique_id = str(session.data.customer_number)
|
||||
await self.async_set_unique_id(unique_id)
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=data
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=unique_id, data=data)
|
||||
|
@ -8,4 +8,4 @@ OAUTH2_AUTHORIZE = "https://welcome.electrickiwi.co.nz/oauth/authorize"
|
||||
OAUTH2_TOKEN = "https://welcome.electrickiwi.co.nz/oauth/token"
|
||||
API_BASE_URL = "https://api.electrickiwi.co.nz"
|
||||
|
||||
SCOPE_VALUES = "read_connection_detail read_billing_frequency read_account_running_balance read_consumption_summary read_consumption_averages read_hop_intervals_config read_hop_connection save_hop_connection read_session"
|
||||
SCOPE_VALUES = "read_customer_details read_connection_detail read_connection read_billing_address get_bill_address read_billing_frequency read_billing_details read_billing_bills read_billing_bill read_billing_bill_id read_billing_bill_file read_account_running_balance read_customer_account_summary read_consumption_summary download_consumption_file read_consumption_averages get_consumption_averages read_hop_intervals_config read_hop_intervals read_hop_connection read_hop_specific_connection save_hop_connection save_hop_specific_connection read_outage_contact get_outage_contact_info_for_icp read_session read_session_data_login"
|
||||
|
@ -10,7 +10,7 @@ import logging
|
||||
|
||||
from electrickiwi_api import ElectricKiwiApi
|
||||
from electrickiwi_api.exceptions import ApiException, AuthException
|
||||
from electrickiwi_api.model import AccountBalance, Hop, HopIntervals
|
||||
from electrickiwi_api.model import AccountSummary, Hop, HopIntervals
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -34,7 +34,7 @@ class ElectricKiwiRuntimeData:
|
||||
type ElectricKiwiConfigEntry = ConfigEntry[ElectricKiwiRuntimeData]
|
||||
|
||||
|
||||
class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountBalance]):
|
||||
class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountSummary]):
|
||||
"""ElectricKiwi Account Data object."""
|
||||
|
||||
def __init__(
|
||||
@ -51,13 +51,13 @@ class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountBalance]):
|
||||
name="Electric Kiwi Account Data",
|
||||
update_interval=ACCOUNT_SCAN_INTERVAL,
|
||||
)
|
||||
self._ek_api = ek_api
|
||||
self.ek_api = ek_api
|
||||
|
||||
async def _async_update_data(self) -> AccountBalance:
|
||||
async def _async_update_data(self) -> AccountSummary:
|
||||
"""Fetch data from Account balance API endpoint."""
|
||||
try:
|
||||
async with asyncio.timeout(60):
|
||||
return await self._ek_api.get_account_balance()
|
||||
return await self.ek_api.get_account_summary()
|
||||
except AuthException as auth_err:
|
||||
raise ConfigEntryAuthFailed from auth_err
|
||||
except ApiException as api_err:
|
||||
@ -85,7 +85,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=HOP_SCAN_INTERVAL,
|
||||
)
|
||||
self._ek_api = ek_api
|
||||
self.ek_api = ek_api
|
||||
self.hop_intervals: HopIntervals | None = None
|
||||
|
||||
def get_hop_options(self) -> dict[str, int]:
|
||||
@ -100,7 +100,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
||||
async def async_update_hop(self, hop_interval: int) -> Hop:
|
||||
"""Update selected hop and data."""
|
||||
try:
|
||||
self.async_set_updated_data(await self._ek_api.post_hop(hop_interval))
|
||||
self.async_set_updated_data(await self.ek_api.post_hop(hop_interval))
|
||||
except AuthException as auth_err:
|
||||
raise ConfigEntryAuthFailed from auth_err
|
||||
except ApiException as api_err:
|
||||
@ -118,7 +118,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
||||
try:
|
||||
async with asyncio.timeout(60):
|
||||
if self.hop_intervals is None:
|
||||
hop_intervals: HopIntervals = await self._ek_api.get_hop_intervals()
|
||||
hop_intervals: HopIntervals = await self.ek_api.get_hop_intervals()
|
||||
hop_intervals.intervals = OrderedDict(
|
||||
filter(
|
||||
lambda pair: pair[1].active == 1,
|
||||
@ -127,7 +127,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
||||
)
|
||||
|
||||
self.hop_intervals = hop_intervals
|
||||
return await self._ek_api.get_hop()
|
||||
return await self.ek_api.get_hop()
|
||||
except AuthException as auth_err:
|
||||
raise ConfigEntryAuthFailed from auth_err
|
||||
except ApiException as api_err:
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/electric_kiwi",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["electrickiwi-api==0.8.5"]
|
||||
"requirements": ["electrickiwi-api==0.9.12"]
|
||||
}
|
||||
|
@ -53,8 +53,8 @@ class ElectricKiwiSelectHOPEntity(
|
||||
"""Initialise the HOP selection entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
||||
f"{coordinator.ek_api.customer_number}"
|
||||
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||
)
|
||||
self.entity_description = description
|
||||
self.values_dict = coordinator.get_hop_options()
|
||||
|
@ -6,7 +6,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from electrickiwi_api.model import AccountBalance, Hop
|
||||
from electrickiwi_api.model import AccountSummary, Hop
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@ -39,7 +39,15 @@ ATTR_HOP_PERCENTAGE = "hop_percentage"
|
||||
class ElectricKiwiAccountSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Electric Kiwi sensor entity."""
|
||||
|
||||
value_func: Callable[[AccountBalance], float | datetime]
|
||||
value_func: Callable[[AccountSummary], float | datetime]
|
||||
|
||||
|
||||
def _get_hop_percentage(account_balance: AccountSummary) -> float:
|
||||
"""Return the hop percentage from account summary."""
|
||||
if power := account_balance.services.get("power"):
|
||||
if connection := power.connections[0]:
|
||||
return float(connection.hop_percentage)
|
||||
return 0.0
|
||||
|
||||
|
||||
ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = (
|
||||
@ -72,9 +80,7 @@ ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = (
|
||||
translation_key="hop_power_savings",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_func=lambda account_balance: float(
|
||||
account_balance.connections[0].hop_percentage
|
||||
),
|
||||
value_func=_get_hop_percentage,
|
||||
),
|
||||
)
|
||||
|
||||
@ -165,8 +171,8 @@ class ElectricKiwiAccountEntity(
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
||||
f"{coordinator.ek_api.customer_number}"
|
||||
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||
)
|
||||
self.entity_description = description
|
||||
|
||||
@ -194,8 +200,8 @@ class ElectricKiwiHOPEntity(
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
||||
f"{coordinator.ek_api.customer_number}"
|
||||
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||
)
|
||||
self.entity_description = description
|
||||
|
||||
|
@ -21,7 +21,8 @@
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"connection_error": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -821,7 +821,7 @@ ecoaliface==0.4.0
|
||||
eheimdigital==1.0.5
|
||||
|
||||
# homeassistant.components.electric_kiwi
|
||||
electrickiwi-api==0.8.5
|
||||
electrickiwi-api==0.9.12
|
||||
|
||||
# homeassistant.components.elevenlabs
|
||||
elevenlabs==1.9.0
|
||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -699,7 +699,7 @@ easyenergy==2.1.2
|
||||
eheimdigital==1.0.5
|
||||
|
||||
# homeassistant.components.electric_kiwi
|
||||
electrickiwi-api==0.8.5
|
||||
electrickiwi-api==0.9.12
|
||||
|
||||
# homeassistant.components.elevenlabs
|
||||
elevenlabs==1.9.0
|
||||
|
@ -1 +1,13 @@
|
||||
"""Tests for the Electric Kiwi integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def init_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the integration with args."""
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -2,11 +2,18 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from collections.abc import Generator
|
||||
from time import time
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from electrickiwi_api.model import AccountBalance, Hop, HopIntervals
|
||||
from electrickiwi_api.model import (
|
||||
AccountSummary,
|
||||
CustomerConnection,
|
||||
Hop,
|
||||
HopIntervals,
|
||||
Service,
|
||||
Session,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
@ -23,37 +30,55 @@ CLIENT_ID = "1234"
|
||||
CLIENT_SECRET = "5678"
|
||||
REDIRECT_URI = "https://example.com/auth/external/callback"
|
||||
|
||||
type YieldFixture = Generator[AsyncMock]
|
||||
type ComponentSetup = Callable[[], Awaitable[bool]]
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def request_setup(current_request_with_host: None) -> None:
|
||||
"""Request setup."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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 hass.async_block_till_done()
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential(CLIENT_ID, CLIENT_SECRET),
|
||||
DOMAIN,
|
||||
def electrickiwi_api() -> Generator[AsyncMock]:
|
||||
"""Mock ek api and return values."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.electric_kiwi.ElectricKiwiApi",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.electric_kiwi.config_flow.ElectricKiwiApi",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.customer_number = 123456
|
||||
client.electricity = Service(
|
||||
identifier="00000000DDA",
|
||||
service="electricity",
|
||||
service_status="Y",
|
||||
is_primary_service=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
config_entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return result
|
||||
|
||||
return _setup_func
|
||||
client.get_active_session.return_value = Session.from_dict(
|
||||
load_json_value_fixture("session.json", DOMAIN)
|
||||
)
|
||||
client.get_hop_intervals.return_value = HopIntervals.from_dict(
|
||||
load_json_value_fixture("hop_intervals.json", DOMAIN)
|
||||
)
|
||||
client.get_hop.return_value = Hop.from_dict(
|
||||
load_json_value_fixture("get_hop.json", DOMAIN)
|
||||
)
|
||||
client.get_account_summary.return_value = AccountSummary.from_dict(
|
||||
load_json_value_fixture("account_summary.json", DOMAIN)
|
||||
)
|
||||
client.get_connection_details.return_value = CustomerConnection.from_dict(
|
||||
load_json_value_fixture("connection_details.json", DOMAIN)
|
||||
)
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
@ -63,7 +88,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
title="Electric Kiwi",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"id": "12345",
|
||||
"id": "123456",
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"refresh_token": "mock-refresh-token",
|
||||
@ -74,6 +99,54 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
},
|
||||
},
|
||||
unique_id=DOMAIN,
|
||||
version=1,
|
||||
minor_version=1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry2")
|
||||
def mock_config_entry2(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Create mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title="Electric Kiwi",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"id": "123457",
|
||||
"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="1234567",
|
||||
version=1,
|
||||
minor_version=1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="migrated_config_entry")
|
||||
def mock_migrated_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Create mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title="Electric Kiwi",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"id": "123456",
|
||||
"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="123456",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
|
||||
@ -87,35 +160,10 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
|
||||
|
||||
@pytest.fixture(name="ek_auth")
|
||||
def electric_kiwi_auth() -> YieldFixture:
|
||||
def electric_kiwi_auth() -> Generator[AsyncMock]:
|
||||
"""Patch access to electric kiwi access token."""
|
||||
with patch(
|
||||
"homeassistant.components.electric_kiwi.api.AsyncConfigEntryAuth"
|
||||
"homeassistant.components.electric_kiwi.api.ConfigEntryElectricKiwiAuth"
|
||||
) 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)
|
||||
)
|
||||
mock_ek_api.return_value.get_account_balance.return_value = (
|
||||
AccountBalance.from_dict(
|
||||
load_json_value_fixture("account_balance.json", DOMAIN)
|
||||
)
|
||||
)
|
||||
yield mock_ek_api
|
||||
|
@ -1,28 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"connections": [
|
||||
{
|
||||
"hop_percentage": "3.5",
|
||||
"id": 3,
|
||||
"running_balance": "184.09",
|
||||
"start_date": "2020-10-04",
|
||||
"unbilled_days": 15
|
||||
}
|
||||
],
|
||||
"last_billed_amount": "-66.31",
|
||||
"last_billed_date": "2020-10-03",
|
||||
"next_billing_date": "2020-11-03",
|
||||
"is_prepay": "N",
|
||||
"summary": {
|
||||
"credits": "0.0",
|
||||
"electricity_used": "184.09",
|
||||
"other_charges": "0.00",
|
||||
"payments": "-220.0"
|
||||
},
|
||||
"total_account_balance": "-102.22",
|
||||
"total_billing_days": 30,
|
||||
"total_running_balance": "184.09",
|
||||
"type": "account_running_balance"
|
||||
},
|
||||
"status": 1
|
||||
}
|
43
tests/components/electric_kiwi/fixtures/account_summary.json
Normal file
43
tests/components/electric_kiwi/fixtures/account_summary.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "account_summary",
|
||||
"total_running_balance": "184.09",
|
||||
"total_account_balance": "-102.22",
|
||||
"total_billing_days": 31,
|
||||
"next_billing_date": "2025-02-19",
|
||||
"service_names": ["power"],
|
||||
"services": {
|
||||
"power": {
|
||||
"connections": [
|
||||
{
|
||||
"id": 515363,
|
||||
"running_balance": "12.98",
|
||||
"unbilled_days": 5,
|
||||
"hop_percentage": "11.2",
|
||||
"start_date": "2025-01-19",
|
||||
"service_label": "Power"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"date_to_pay": "",
|
||||
"invoice_id": "",
|
||||
"total_invoiced_charges": "",
|
||||
"default_to_pay": "",
|
||||
"invoice_exists": 1,
|
||||
"display_date": "2025-01-19",
|
||||
"last_billed_date": "2025-01-18",
|
||||
"last_billed_amount": "-21.02",
|
||||
"summary": {
|
||||
"electricity_used": "12.98",
|
||||
"other_charges": "0.00",
|
||||
"payments": "0.00",
|
||||
"credits": "0.00",
|
||||
"mobile_charges": "0.00",
|
||||
"broadband_charges": "0.00",
|
||||
"addon_unbilled_charges": {}
|
||||
},
|
||||
"is_prepay": "N"
|
||||
},
|
||||
"status": 1
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "connection",
|
||||
"id": 515363,
|
||||
"customer_id": 273941,
|
||||
"customer_number": 34030646,
|
||||
"icp_identifier": "00000000DDA",
|
||||
"address": "",
|
||||
"short_address": "",
|
||||
"physical_address_unit": "",
|
||||
"physical_address_number": "555",
|
||||
"physical_address_street": "RACECOURSE ROAD",
|
||||
"physical_address_suburb": "",
|
||||
"physical_address_town": "Blah",
|
||||
"physical_address_region": "Blah",
|
||||
"physical_address_postcode": "0000",
|
||||
"is_active": "Y",
|
||||
"pricing_plan": {
|
||||
"id": 51423,
|
||||
"usage": "0.0000",
|
||||
"fixed": "0.6000",
|
||||
"usage_rate_inc_gst": "0.0000",
|
||||
"supply_rate_inc_gst": "0.6900",
|
||||
"plan_description": "MoveMaster Anytime Residential (Low User)",
|
||||
"plan_type": "movemaster_tou",
|
||||
"signup_price_plan_blurb": "Better rates every day during off-peak, and all day on weekends. Plus half price nights (11pm-7am) and our best solar buyback.",
|
||||
"signup_price_plan_label": "MoveMaster",
|
||||
"app_price_plan_label": "Your MoveMaster rates are...",
|
||||
"solar_rate_excl_gst": "0.1250",
|
||||
"solar_rate_incl_gst": "0.1438",
|
||||
"pricing_type": "tou_plus",
|
||||
"tou_plus": {
|
||||
"fixed_rate_excl_gst": "0.6000",
|
||||
"fixed_rate_incl_gst": "0.6900",
|
||||
"interval_types": ["peak", "off_peak_shoulder", "off_peak_night"],
|
||||
"peak": {
|
||||
"price_excl_gst": "0.5390",
|
||||
"price_incl_gst": "0.6199",
|
||||
"display_text": {
|
||||
"Weekdays": "7am-9am, 5pm-9pm"
|
||||
},
|
||||
"tou_plus_label": "Peak"
|
||||
},
|
||||
"off_peak_shoulder": {
|
||||
"price_excl_gst": "0.3234",
|
||||
"price_incl_gst": "0.3719",
|
||||
"display_text": {
|
||||
"Weekdays": "9am-5pm, 9pm-11pm",
|
||||
"Weekends": "7am-11pm"
|
||||
},
|
||||
"tou_plus_label": "Off-peak shoulder"
|
||||
},
|
||||
"off_peak_night": {
|
||||
"price_excl_gst": "0.2695",
|
||||
"price_incl_gst": "0.3099",
|
||||
"display_text": {
|
||||
"Every day": "11pm-7am"
|
||||
},
|
||||
"tou_plus_label": "Off-peak night"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hop": {
|
||||
"start_time": "9:00 PM",
|
||||
"end_time": "10:00 PM",
|
||||
"interval_start": "43",
|
||||
"interval_end": "44"
|
||||
},
|
||||
"start_date": "2022-03-03",
|
||||
"end_date": "",
|
||||
"property_type": "residential"
|
||||
}
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
{
|
||||
"data": {
|
||||
"connection_id": "3",
|
||||
"customer_number": 1000001,
|
||||
"end": {
|
||||
"end_time": "5:00 PM",
|
||||
"interval": "34"
|
||||
},
|
||||
"type": "hop_customer",
|
||||
"customer_id": 123456,
|
||||
"service_type": "electricity",
|
||||
"connection_id": 515363,
|
||||
"billing_id": 1247975,
|
||||
"start": {
|
||||
"start_time": "4:00 PM",
|
||||
"interval": "33"
|
||||
"interval": "33",
|
||||
"start_time": "4:00 PM"
|
||||
},
|
||||
"type": "hop_customer"
|
||||
"end": {
|
||||
"interval": "34",
|
||||
"end_time": "5:00 PM"
|
||||
}
|
||||
},
|
||||
"status": 1
|
||||
}
|
||||
|
@ -1,249 +1,250 @@
|
||||
{
|
||||
"data": {
|
||||
"hop_duration": "60",
|
||||
"type": "hop_intervals",
|
||||
"hop_duration": "60",
|
||||
"intervals": {
|
||||
"1": {
|
||||
"active": 1,
|
||||
"start_time": "12:00 AM",
|
||||
"end_time": "1:00 AM",
|
||||
"start_time": "12:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"2": {
|
||||
"active": 1,
|
||||
"start_time": "12:30 AM",
|
||||
"end_time": "1:30 AM",
|
||||
"start_time": "12:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"3": {
|
||||
"active": 1,
|
||||
"start_time": "1:00 AM",
|
||||
"end_time": "2:00 AM",
|
||||
"start_time": "1:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"4": {
|
||||
"active": 1,
|
||||
"start_time": "1:30 AM",
|
||||
"end_time": "2:30 AM",
|
||||
"start_time": "1:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"5": {
|
||||
"active": 1,
|
||||
"start_time": "2:00 AM",
|
||||
"end_time": "3:00 AM",
|
||||
"start_time": "2:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"6": {
|
||||
"active": 1,
|
||||
"start_time": "2:30 AM",
|
||||
"end_time": "3:30 AM",
|
||||
"start_time": "2:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"7": {
|
||||
"active": 1,
|
||||
"start_time": "3:00 AM",
|
||||
"end_time": "4:00 AM",
|
||||
"start_time": "3:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"8": {
|
||||
"active": 1,
|
||||
"start_time": "3:30 AM",
|
||||
"end_time": "4:30 AM",
|
||||
"start_time": "3:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"9": {
|
||||
"active": 1,
|
||||
"start_time": "4:00 AM",
|
||||
"end_time": "5:00 AM",
|
||||
"start_time": "4:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"10": {
|
||||
"active": 1,
|
||||
"start_time": "4:30 AM",
|
||||
"end_time": "5:30 AM",
|
||||
"start_time": "4:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"11": {
|
||||
"active": 1,
|
||||
"start_time": "5:00 AM",
|
||||
"end_time": "6:00 AM",
|
||||
"start_time": "5:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"12": {
|
||||
"active": 1,
|
||||
"start_time": "5:30 AM",
|
||||
"end_time": "6:30 AM",
|
||||
"start_time": "5:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"13": {
|
||||
"active": 1,
|
||||
"start_time": "6:00 AM",
|
||||
"end_time": "7:00 AM",
|
||||
"start_time": "6:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"14": {
|
||||
"active": 1,
|
||||
"start_time": "6:30 AM",
|
||||
"end_time": "7:30 AM",
|
||||
"start_time": "6:30 AM"
|
||||
"active": 0
|
||||
},
|
||||
"15": {
|
||||
"active": 1,
|
||||
"start_time": "7:00 AM",
|
||||
"end_time": "8:00 AM",
|
||||
"start_time": "7:00 AM"
|
||||
"active": 0
|
||||
},
|
||||
"16": {
|
||||
"active": 1,
|
||||
"start_time": "7:30 AM",
|
||||
"end_time": "8:30 AM",
|
||||
"start_time": "7:30 AM"
|
||||
"active": 0
|
||||
},
|
||||
"17": {
|
||||
"active": 1,
|
||||
"start_time": "8:00 AM",
|
||||
"end_time": "9:00 AM",
|
||||
"start_time": "8:00 AM"
|
||||
"active": 0
|
||||
},
|
||||
"18": {
|
||||
"active": 1,
|
||||
"start_time": "8:30 AM",
|
||||
"end_time": "9:30 AM",
|
||||
"start_time": "8:30 AM"
|
||||
"active": 0
|
||||
},
|
||||
"19": {
|
||||
"active": 1,
|
||||
"start_time": "9:00 AM",
|
||||
"end_time": "10:00 AM",
|
||||
"start_time": "9:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"20": {
|
||||
"active": 1,
|
||||
"start_time": "9:30 AM",
|
||||
"end_time": "10:30 AM",
|
||||
"start_time": "9:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"21": {
|
||||
"active": 1,
|
||||
"start_time": "10:00 AM",
|
||||
"end_time": "11:00 AM",
|
||||
"start_time": "10:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"22": {
|
||||
"active": 1,
|
||||
"start_time": "10:30 AM",
|
||||
"end_time": "11:30 AM",
|
||||
"start_time": "10:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"23": {
|
||||
"active": 1,
|
||||
"start_time": "11:00 AM",
|
||||
"end_time": "12:00 PM",
|
||||
"start_time": "11:00 AM"
|
||||
"active": 1
|
||||
},
|
||||
"24": {
|
||||
"active": 1,
|
||||
"start_time": "11:30 AM",
|
||||
"end_time": "12:30 PM",
|
||||
"start_time": "11:30 AM"
|
||||
"active": 1
|
||||
},
|
||||
"25": {
|
||||
"active": 1,
|
||||
"start_time": "12:00 PM",
|
||||
"end_time": "1:00 PM",
|
||||
"start_time": "12:00 PM"
|
||||
"active": 1
|
||||
},
|
||||
"26": {
|
||||
"active": 1,
|
||||
"start_time": "12:30 PM",
|
||||
"end_time": "1:30 PM",
|
||||
"start_time": "12:30 PM"
|
||||
"active": 1
|
||||
},
|
||||
"27": {
|
||||
"active": 1,
|
||||
"start_time": "1:00 PM",
|
||||
"end_time": "2:00 PM",
|
||||
"start_time": "1:00 PM"
|
||||
"active": 1
|
||||
},
|
||||
"28": {
|
||||
"active": 1,
|
||||
"start_time": "1:30 PM",
|
||||
"end_time": "2:30 PM",
|
||||
"start_time": "1:30 PM"
|
||||
"active": 1
|
||||
},
|
||||
"29": {
|
||||
"active": 1,
|
||||
"start_time": "2:00 PM",
|
||||
"end_time": "3:00 PM",
|
||||
"start_time": "2:00 PM"
|
||||
"active": 1
|
||||
},
|
||||
"30": {
|
||||
"active": 1,
|
||||
"start_time": "2:30 PM",
|
||||
"end_time": "3:30 PM",
|
||||
"start_time": "2:30 PM"
|
||||
"active": 1
|
||||
},
|
||||
"31": {
|
||||
"active": 1,
|
||||
"start_time": "3:00 PM",
|
||||
"end_time": "4:00 PM",
|
||||
"start_time": "3:00 PM"
|
||||
"active": 1
|
||||
},
|
||||
"32": {
|
||||
"active": 1,
|
||||
"start_time": "3:30 PM",
|
||||
"end_time": "4:30 PM",
|
||||
"start_time": "3:30 PM"
|
||||
"active": 1
|
||||
},
|
||||
"33": {
|
||||
"active": 1,
|
||||
"start_time": "4:00 PM",
|
||||
"end_time": "5:00 PM",
|
||||
"start_time": "4:00 PM"
|
||||
"active": 1
|
||||
},
|
||||
"34": {
|
||||
"active": 1,
|
||||
"start_time": "4:30 PM",
|
||||
"end_time": "5:30 PM",
|
||||
"start_time": "4:30 PM"
|
||||
"active": 0
|
||||
},
|
||||
"35": {
|
||||
"active": 1,
|
||||
"start_time": "5:00 PM",
|
||||
"end_time": "6:00 PM",
|
||||
"start_time": "5:00 PM"
|
||||
"active": 0
|
||||
},
|
||||
"36": {
|
||||
"active": 1,
|
||||
"start_time": "5:30 PM",
|
||||
"end_time": "6:30 PM",
|
||||
"start_time": "5:30 PM"
|
||||
"active": 0
|
||||
},
|
||||
"37": {
|
||||
"active": 1,
|
||||
"start_time": "6:00 PM",
|
||||
"end_time": "7:00 PM",
|
||||
"start_time": "6:00 PM"
|
||||
"active": 0
|
||||
},
|
||||
"38": {
|
||||
"active": 1,
|
||||
"start_time": "6:30 PM",
|
||||
"end_time": "7:30 PM",
|
||||
"start_time": "6:30 PM"
|
||||
"active": 0
|
||||
},
|
||||
"39": {
|
||||
"active": 1,
|
||||
"start_time": "7:00 PM",
|
||||
"end_time": "8:00 PM",
|
||||
"start_time": "7:00 PM"
|
||||
"active": 0
|
||||
},
|
||||
"40": {
|
||||
"active": 1,
|
||||
"start_time": "7:30 PM",
|
||||
"end_time": "8:30 PM",
|
||||
"start_time": "7:30 PM"
|
||||
"active": 0
|
||||
},
|
||||
"41": {
|
||||
"active": 1,
|
||||
"start_time": "8:00 PM",
|
||||
"end_time": "9:00 PM",
|
||||
"start_time": "8:00 PM"
|
||||
"active": 0
|
||||
},
|
||||
"42": {
|
||||
"active": 1,
|
||||
"start_time": "8:30 PM",
|
||||
"end_time": "9:30 PM",
|
||||
"start_time": "8:30 PM"
|
||||
"active": 0
|
||||
},
|
||||
"43": {
|
||||
"active": 1,
|
||||
"start_time": "9:00 PM",
|
||||
"end_time": "10:00 PM",
|
||||
"start_time": "9:00 PM"
|
||||
"active": 1
|
||||
},
|
||||
"44": {
|
||||
"active": 1,
|
||||
"start_time": "9:30 PM",
|
||||
"end_time": "10:30 PM",
|
||||
"start_time": "9:30 PM"
|
||||
"active": 1
|
||||
},
|
||||
"45": {
|
||||
"active": 1,
|
||||
"end_time": "11:00 AM",
|
||||
"start_time": "10:00 PM"
|
||||
"start_time": "10:00 PM",
|
||||
"end_time": "11:00 PM",
|
||||
"active": 1
|
||||
},
|
||||
"46": {
|
||||
"active": 1,
|
||||
"start_time": "10:30 PM",
|
||||
"end_time": "11:30 PM",
|
||||
"start_time": "10:30 PM"
|
||||
"active": 1
|
||||
},
|
||||
"47": {
|
||||
"active": 1,
|
||||
"start_time": "11:00 PM",
|
||||
"end_time": "12:00 AM",
|
||||
"start_time": "11:00 PM"
|
||||
"active": 1
|
||||
},
|
||||
"48": {
|
||||
"active": 1,
|
||||
"start_time": "11:30 PM",
|
||||
"end_time": "12:30 AM",
|
||||
"start_time": "11:30 PM"
|
||||
"active": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"service_type": "electricity"
|
||||
},
|
||||
"status": 1
|
||||
}
|
||||
|
23
tests/components/electric_kiwi/fixtures/session.json
Normal file
23
tests/components/electric_kiwi/fixtures/session.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"data": {
|
||||
"data": {
|
||||
"type": "session",
|
||||
"avatar": [],
|
||||
"customer_number": 123456,
|
||||
"customer_name": "Joe Dirt",
|
||||
"email": "joe@dirt.kiwi",
|
||||
"customer_status": "Y",
|
||||
"services": [
|
||||
{
|
||||
"service": "Electricity",
|
||||
"identifier": "00000000DDA",
|
||||
"is_primary_service": true,
|
||||
"service_status": "Y"
|
||||
}
|
||||
],
|
||||
"res_partner_id": 285554,
|
||||
"nuid": "EK_GUID"
|
||||
}
|
||||
},
|
||||
"status": 1
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"data": {
|
||||
"data": {
|
||||
"type": "session",
|
||||
"avatar": [],
|
||||
"customer_number": 123456,
|
||||
"customer_name": "Joe Dirt",
|
||||
"email": "joe@dirt.kiwi",
|
||||
"customer_status": "Y",
|
||||
"services": [],
|
||||
"res_partner_id": 285554,
|
||||
"nuid": "EK_GUID"
|
||||
}
|
||||
},
|
||||
"status": 1
|
||||
}
|
@ -3,70 +3,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from electrickiwi_api.exceptions import ApiException
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.application_credentials import (
|
||||
ClientCredential,
|
||||
async_import_client_credential,
|
||||
)
|
||||
from homeassistant.components.electric_kiwi.const import (
|
||||
DOMAIN,
|
||||
OAUTH2_AUTHORIZE,
|
||||
OAUTH2_TOKEN,
|
||||
SCOPE_VALUES,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
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
|
||||
from .conftest import CLIENT_ID, REDIRECT_URI
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
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(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "missing_credentials"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.usefixtures("current_request_with_host", "electrickiwi_api")
|
||||
async def test_full_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_credentials: None,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Check full flow."""
|
||||
await async_import_client_credential(
|
||||
hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER, "entry_id": DOMAIN}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
@ -76,13 +46,13 @@ async def test_full_flow(
|
||||
},
|
||||
)
|
||||
|
||||
URL_SCOPE = SCOPE_VALUES.replace(" ", "+")
|
||||
url_scope = SCOPE_VALUES.replace(" ", "+")
|
||||
|
||||
assert result["url"] == (
|
||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||
f"&redirect_uri={REDIRECT_URI}"
|
||||
f"&state={state}"
|
||||
f"&scope={URL_SCOPE}"
|
||||
f"&scope={url_scope}"
|
||||
)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
@ -90,6 +60,7 @@ async def test_full_flow(
|
||||
assert resp.status == HTTPStatus.OK
|
||||
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
OAUTH2_TOKEN,
|
||||
json={
|
||||
@ -106,20 +77,73 @@ async def test_full_flow(
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_flow_failure(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
electrickiwi_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Check failure on creation of entry."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
"flow_id": result["flow_id"],
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
},
|
||||
)
|
||||
|
||||
url_scope = SCOPE_VALUES.replace(" ", "+")
|
||||
|
||||
assert result["url"] == (
|
||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||
f"&redirect_uri={REDIRECT_URI}"
|
||||
f"&state={state}"
|
||||
f"&scope={url_scope}"
|
||||
)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
assert resp.status == HTTPStatus.OK
|
||||
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
OAUTH2_TOKEN,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
},
|
||||
)
|
||||
|
||||
electrickiwi_api.get_active_session.side_effect = ApiException()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "connection_error"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_existing_entry(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_credentials: None,
|
||||
config_entry: MockConfigEntry,
|
||||
migrated_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Check existing entry."""
|
||||
config_entry.add_to_hass(hass)
|
||||
migrated_config_entry.add_to_hass(hass)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER, "entry_id": DOMAIN}
|
||||
DOMAIN, context={"source": SOURCE_USER, "entry_id": DOMAIN}
|
||||
)
|
||||
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
@ -145,7 +169,9 @@ async def test_existing_entry(
|
||||
},
|
||||
)
|
||||
|
||||
await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
|
||||
|
||||
@ -154,13 +180,13 @@ async def test_reauthentication(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
mock_setup_entry: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
setup_credentials: None,
|
||||
mock_setup_entry: AsyncMock,
|
||||
migrated_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test Electric Kiwi reauthentication."""
|
||||
config_entry.add_to_hass(hass)
|
||||
result = await config_entry.start_reauth_flow(hass)
|
||||
migrated_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await migrated_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
@ -189,8 +215,11 @@ async def test_reauthentication(
|
||||
},
|
||||
)
|
||||
|
||||
await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "reauth_successful"
|
||||
|
135
tests/components/electric_kiwi/test_init.py
Normal file
135
tests/components/electric_kiwi/test_init.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""Test the Electric Kiwi init."""
|
||||
|
||||
import http
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohttp import RequestInfo
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from electrickiwi_api.exceptions import ApiException, AuthException
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.electric_kiwi.const import DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test a successful setup entry and unload of entry."""
|
||||
await init_integration(hass, config_entry)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_async_setup_multiple_entries(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
config_entry2: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a successful setup and unload of multiple entries."""
|
||||
|
||||
for entry in (config_entry, config_entry2):
|
||||
await init_integration(hass, entry)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 2
|
||||
|
||||
for entry in (config_entry, config_entry2):
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status", "expected_state"),
|
||||
[
|
||||
(
|
||||
http.HTTPStatus.UNAUTHORIZED,
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
),
|
||||
(
|
||||
http.HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
),
|
||||
],
|
||||
ids=["failure_requires_reauth", "transient_failure"],
|
||||
)
|
||||
async def test_refresh_token_validity_failures(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
status: http.HTTPStatus,
|
||||
expected_state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test token refresh failure status."""
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=ClientResponseError(
|
||||
RequestInfo("", "POST", {}, ""), None, status=status
|
||||
),
|
||||
) as mock_async_ensure_token_valid:
|
||||
await init_integration(hass, config_entry)
|
||||
mock_async_ensure_token_valid.assert_called_once()
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert entries[0].state is expected_state
|
||||
|
||||
|
||||
async def test_unique_id_migration(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that the unique ID is migrated to the customer number."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
entity_registry.async_get_or_create(
|
||||
SENSOR_DOMAIN, DOMAIN, "123456_515363_sensor", config_entry=config_entry
|
||||
)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
new_entry = hass.config_entries.async_get_entry(config_entry.entry_id)
|
||||
assert new_entry.minor_version == 2
|
||||
assert new_entry.unique_id == "123456"
|
||||
entity_entry = entity_registry.async_get(
|
||||
"sensor.electric_kiwi_123456_515363_sensor"
|
||||
)
|
||||
assert entity_entry.unique_id == "123456_00000000DDA_sensor"
|
||||
|
||||
|
||||
async def test_unique_id_migration_failure(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, electrickiwi_api: AsyncMock
|
||||
) -> None:
|
||||
"""Test that the unique ID is migrated to the customer number."""
|
||||
electrickiwi_api.set_active_session.side_effect = ApiException()
|
||||
await init_integration(hass, config_entry)
|
||||
|
||||
assert config_entry.minor_version == 1
|
||||
assert config_entry.unique_id == DOMAIN
|
||||
assert config_entry.state is ConfigEntryState.MIGRATION_ERROR
|
||||
|
||||
|
||||
async def test_unique_id_migration_auth_failure(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, electrickiwi_api: AsyncMock
|
||||
) -> None:
|
||||
"""Test that the unique ID is migrated to the customer number."""
|
||||
electrickiwi_api.set_active_session.side_effect = AuthException()
|
||||
await init_integration(hass, config_entry)
|
||||
|
||||
assert config_entry.minor_version == 1
|
||||
assert config_entry.unique_id == DOMAIN
|
||||
assert config_entry.state is ConfigEntryState.MIGRATION_ERROR
|
@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import ComponentSetup, YieldFixture
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -47,10 +47,9 @@ def restore_timezone():
|
||||
async def test_hop_sensors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
ek_api: YieldFixture,
|
||||
ek_auth: YieldFixture,
|
||||
electrickiwi_api: Mock,
|
||||
ek_auth: AsyncMock,
|
||||
entity_registry: EntityRegistry,
|
||||
component_setup: ComponentSetup,
|
||||
sensor: str,
|
||||
sensor_state: str,
|
||||
) -> None:
|
||||
@ -61,7 +60,7 @@ async def test_hop_sensors(
|
||||
sensor state should be set to today at 4pm or if now is past 4pm,
|
||||
then tomorrow at 4pm.
|
||||
"""
|
||||
assert await component_setup()
|
||||
await init_integration(hass, config_entry)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity = entity_registry.async_get(sensor)
|
||||
@ -70,8 +69,7 @@ async def test_hop_sensors(
|
||||
state = hass.states.get(sensor)
|
||||
assert state
|
||||
|
||||
api = ek_api(Mock())
|
||||
hop_data = await api.get_hop()
|
||||
hop_data = await electrickiwi_api.get_hop()
|
||||
|
||||
value = _check_and_move_time(hop_data, sensor_state)
|
||||
|
||||
@ -98,20 +96,19 @@ async def test_hop_sensors(
|
||||
),
|
||||
(
|
||||
"sensor.next_billing_date",
|
||||
"2020-11-03T00:00:00",
|
||||
"2025-02-19T00:00:00",
|
||||
SensorDeviceClass.DATE,
|
||||
None,
|
||||
),
|
||||
("sensor.hour_of_power_savings", "3.5", None, SensorStateClass.MEASUREMENT),
|
||||
("sensor.hour_of_power_savings", "11.2", None, SensorStateClass.MEASUREMENT),
|
||||
],
|
||||
)
|
||||
async def test_account_sensors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
ek_api: YieldFixture,
|
||||
ek_auth: YieldFixture,
|
||||
electrickiwi_api: AsyncMock,
|
||||
ek_auth: AsyncMock,
|
||||
entity_registry: EntityRegistry,
|
||||
component_setup: ComponentSetup,
|
||||
sensor: str,
|
||||
sensor_state: str,
|
||||
device_class: str,
|
||||
@ -119,7 +116,7 @@ async def test_account_sensors(
|
||||
) -> None:
|
||||
"""Test Account sensors for the Electric Kiwi integration."""
|
||||
|
||||
assert await component_setup()
|
||||
await init_integration(hass, config_entry)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity = entity_registry.async_get(sensor)
|
||||
@ -133,9 +130,9 @@ async def test_account_sensors(
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == state_class
|
||||
|
||||
|
||||
async def test_check_and_move_time(ek_api: AsyncMock) -> None:
|
||||
async def test_check_and_move_time(electrickiwi_api: AsyncMock) -> None:
|
||||
"""Test correct time is returned depending on time of day."""
|
||||
hop = await ek_api(Mock()).get_hop()
|
||||
hop = await electrickiwi_api.get_hop()
|
||||
|
||||
test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TEST_TIMEZONE)
|
||||
dt_util.set_default_time_zone(TEST_TIMEZONE)
|
||||
|
Loading…
x
Reference in New Issue
Block a user