mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Change Electric Kiwi authentication (#135231)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
15bc29f8ca
commit
94614e0376
@ -4,12 +4,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from electrickiwi_api import ElectricKiwiApi
|
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.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
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 . import api
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
@ -44,7 +48,9 @@ async def async_setup_entry(
|
|||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
ek_api = ElectricKiwiApi(
|
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)
|
hop_coordinator = ElectricKiwiHOPDataCoordinator(hass, entry, ek_api)
|
||||||
account_coordinator = ElectricKiwiAccountDataCoordinator(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 ek_api.set_active_session()
|
||||||
await hop_coordinator.async_config_entry_first_refresh()
|
await hop_coordinator.async_config_entry_first_refresh()
|
||||||
await account_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:
|
except ApiException as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
@ -70,3 +78,53 @@ async def async_unload_entry(
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
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 __future__ import annotations
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from electrickiwi_api import AbstractAuth
|
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
|
from .const import API_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
class AsyncConfigEntryAuth(AbstractAuth):
|
class ConfigEntryElectricKiwiAuth(AbstractAuth):
|
||||||
"""Provide Electric Kiwi authentication tied to an OAuth2 based config entry."""
|
"""Provide Electric Kiwi authentication tied to an OAuth2 based config entry."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -29,4 +28,21 @@ class AsyncConfigEntryAuth(AbstractAuth):
|
|||||||
"""Return a valid access token."""
|
"""Return a valid access token."""
|
||||||
await self._oauth_session.async_ensure_token_valid()
|
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
|
import logging
|
||||||
from typing import Any
|
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 homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from . import api
|
||||||
from .const import DOMAIN, SCOPE_VALUES
|
from .const import DOMAIN, SCOPE_VALUES
|
||||||
|
|
||||||
|
|
||||||
@ -17,6 +22,8 @@ class ElectricKiwiOauth2FlowHandler(
|
|||||||
):
|
):
|
||||||
"""Config flow to handle Electric Kiwi OAuth2 authentication."""
|
"""Config flow to handle Electric Kiwi OAuth2 authentication."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 2
|
||||||
DOMAIN = DOMAIN
|
DOMAIN = DOMAIN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -40,12 +47,30 @@ class ElectricKiwiOauth2FlowHandler(
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Dialog that informs the user that reauth is required."""
|
"""Dialog that informs the user that reauth is required."""
|
||||||
if user_input is None:
|
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()
|
return await self.async_step_user()
|
||||||
|
|
||||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||||
"""Create an entry for Electric Kiwi."""
|
"""Create an entry for Electric Kiwi."""
|
||||||
existing_entry = await self.async_set_unique_id(DOMAIN)
|
ek_api = ElectricKiwiApi(
|
||||||
if existing_entry:
|
api.ConfigFlowElectricKiwiAuth(self.hass, data["token"]["access_token"])
|
||||||
return self.async_update_reload_and_abort(existing_entry, data=data)
|
)
|
||||||
return await super().async_oauth_create_entry(data)
|
|
||||||
|
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"
|
OAUTH2_TOKEN = "https://welcome.electrickiwi.co.nz/oauth/token"
|
||||||
API_BASE_URL = "https://api.electrickiwi.co.nz"
|
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 import ElectricKiwiApi
|
||||||
from electrickiwi_api.exceptions import ApiException, AuthException
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -34,7 +34,7 @@ class ElectricKiwiRuntimeData:
|
|||||||
type ElectricKiwiConfigEntry = ConfigEntry[ElectricKiwiRuntimeData]
|
type ElectricKiwiConfigEntry = ConfigEntry[ElectricKiwiRuntimeData]
|
||||||
|
|
||||||
|
|
||||||
class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountBalance]):
|
class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountSummary]):
|
||||||
"""ElectricKiwi Account Data object."""
|
"""ElectricKiwi Account Data object."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -51,13 +51,13 @@ class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountBalance]):
|
|||||||
name="Electric Kiwi Account Data",
|
name="Electric Kiwi Account Data",
|
||||||
update_interval=ACCOUNT_SCAN_INTERVAL,
|
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."""
|
"""Fetch data from Account balance API endpoint."""
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(60):
|
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:
|
except AuthException as auth_err:
|
||||||
raise ConfigEntryAuthFailed from auth_err
|
raise ConfigEntryAuthFailed from auth_err
|
||||||
except ApiException as api_err:
|
except ApiException as api_err:
|
||||||
@ -85,7 +85,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
|||||||
# Polling interval. Will only be polled if there are subscribers.
|
# Polling interval. Will only be polled if there are subscribers.
|
||||||
update_interval=HOP_SCAN_INTERVAL,
|
update_interval=HOP_SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
self._ek_api = ek_api
|
self.ek_api = ek_api
|
||||||
self.hop_intervals: HopIntervals | None = None
|
self.hop_intervals: HopIntervals | None = None
|
||||||
|
|
||||||
def get_hop_options(self) -> dict[str, int]:
|
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:
|
async def async_update_hop(self, hop_interval: int) -> Hop:
|
||||||
"""Update selected hop and data."""
|
"""Update selected hop and data."""
|
||||||
try:
|
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:
|
except AuthException as auth_err:
|
||||||
raise ConfigEntryAuthFailed from auth_err
|
raise ConfigEntryAuthFailed from auth_err
|
||||||
except ApiException as api_err:
|
except ApiException as api_err:
|
||||||
@ -118,7 +118,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
|||||||
try:
|
try:
|
||||||
async with asyncio.timeout(60):
|
async with asyncio.timeout(60):
|
||||||
if self.hop_intervals is None:
|
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(
|
hop_intervals.intervals = OrderedDict(
|
||||||
filter(
|
filter(
|
||||||
lambda pair: pair[1].active == 1,
|
lambda pair: pair[1].active == 1,
|
||||||
@ -127,7 +127,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.hop_intervals = hop_intervals
|
self.hop_intervals = hop_intervals
|
||||||
return await self._ek_api.get_hop()
|
return await self.ek_api.get_hop()
|
||||||
except AuthException as auth_err:
|
except AuthException as auth_err:
|
||||||
raise ConfigEntryAuthFailed from auth_err
|
raise ConfigEntryAuthFailed from auth_err
|
||||||
except ApiException as api_err:
|
except ApiException as api_err:
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/electric_kiwi",
|
"documentation": "https://www.home-assistant.io/integrations/electric_kiwi",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"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."""
|
"""Initialise the HOP selection entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
f"{coordinator.ek_api.customer_number}"
|
||||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self.values_dict = coordinator.get_hop_options()
|
self.values_dict = coordinator.get_hop_options()
|
||||||
|
@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from electrickiwi_api.model import AccountBalance, Hop
|
from electrickiwi_api.model import AccountSummary, Hop
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -39,7 +39,15 @@ ATTR_HOP_PERCENTAGE = "hop_percentage"
|
|||||||
class ElectricKiwiAccountSensorEntityDescription(SensorEntityDescription):
|
class ElectricKiwiAccountSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Describes Electric Kiwi sensor entity."""
|
"""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, ...] = (
|
ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = (
|
||||||
@ -72,9 +80,7 @@ ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = (
|
|||||||
translation_key="hop_power_savings",
|
translation_key="hop_power_savings",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_func=lambda account_balance: float(
|
value_func=_get_hop_percentage,
|
||||||
account_balance.connections[0].hop_percentage
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -165,8 +171,8 @@ class ElectricKiwiAccountEntity(
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
f"{coordinator.ek_api.customer_number}"
|
||||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
@ -194,8 +200,8 @@ class ElectricKiwiHOPEntity(
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
f"{coordinator.ek_api.customer_number}"
|
||||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
|
@ -21,7 +21,8 @@
|
|||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
"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": {
|
"create_entry": {
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -824,7 +824,7 @@ ecoaliface==0.4.0
|
|||||||
eheimdigital==1.0.5
|
eheimdigital==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.electric_kiwi
|
# homeassistant.components.electric_kiwi
|
||||||
electrickiwi-api==0.8.5
|
electrickiwi-api==0.9.12
|
||||||
|
|
||||||
# homeassistant.components.elevenlabs
|
# homeassistant.components.elevenlabs
|
||||||
elevenlabs==1.9.0
|
elevenlabs==1.9.0
|
||||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -702,7 +702,7 @@ easyenergy==2.1.2
|
|||||||
eheimdigital==1.0.5
|
eheimdigital==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.electric_kiwi
|
# homeassistant.components.electric_kiwi
|
||||||
electrickiwi-api==0.8.5
|
electrickiwi-api==0.9.12
|
||||||
|
|
||||||
# homeassistant.components.elevenlabs
|
# homeassistant.components.elevenlabs
|
||||||
elevenlabs==1.9.0
|
elevenlabs==1.9.0
|
||||||
|
@ -1 +1,13 @@
|
|||||||
"""Tests for the Electric Kiwi integration."""
|
"""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 __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable, Generator
|
from collections.abc import Generator
|
||||||
from time import time
|
from time import time
|
||||||
from unittest.mock import AsyncMock, patch
|
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
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.application_credentials import (
|
from homeassistant.components.application_credentials import (
|
||||||
@ -23,37 +30,55 @@ 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"
|
||||||
|
|
||||||
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)
|
@pytest.fixture(autouse=True)
|
||||||
async def request_setup(current_request_with_host: None) -> None:
|
def electrickiwi_api() -> Generator[AsyncMock]:
|
||||||
"""Request setup."""
|
"""Mock ek api and return values."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
@pytest.fixture
|
"homeassistant.components.electric_kiwi.ElectricKiwiApi",
|
||||||
def component_setup(
|
autospec=True,
|
||||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
) as mock_client,
|
||||||
) -> ComponentSetup:
|
patch(
|
||||||
"""Fixture for setting up the integration."""
|
"homeassistant.components.electric_kiwi.config_flow.ElectricKiwiApi",
|
||||||
|
new=mock_client,
|
||||||
async def _setup_func() -> bool:
|
),
|
||||||
assert await async_setup_component(hass, "application_credentials", {})
|
):
|
||||||
await hass.async_block_till_done()
|
client = mock_client.return_value
|
||||||
await async_import_client_credential(
|
client.customer_number = 123456
|
||||||
hass,
|
client.electricity = Service(
|
||||||
DOMAIN,
|
identifier="00000000DDA",
|
||||||
ClientCredential(CLIENT_ID, CLIENT_SECRET),
|
service="electricity",
|
||||||
DOMAIN,
|
service_status="Y",
|
||||||
|
is_primary_service=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
client.get_active_session.return_value = Session.from_dict(
|
||||||
config_entry.add_to_hass(hass)
|
load_json_value_fixture("session.json", DOMAIN)
|
||||||
result = await hass.config_entries.async_setup(config_entry.entry_id)
|
)
|
||||||
await hass.async_block_till_done()
|
client.get_hop_intervals.return_value = HopIntervals.from_dict(
|
||||||
return result
|
load_json_value_fixture("hop_intervals.json", DOMAIN)
|
||||||
|
)
|
||||||
return _setup_func
|
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")
|
@pytest.fixture(name="config_entry")
|
||||||
@ -63,7 +88,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
|||||||
title="Electric Kiwi",
|
title="Electric Kiwi",
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={
|
data={
|
||||||
"id": "12345",
|
"id": "123456",
|
||||||
"auth_implementation": DOMAIN,
|
"auth_implementation": DOMAIN,
|
||||||
"token": {
|
"token": {
|
||||||
"refresh_token": "mock-refresh-token",
|
"refresh_token": "mock-refresh-token",
|
||||||
@ -74,6 +99,54 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
unique_id=DOMAIN,
|
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")
|
@pytest.fixture(name="ek_auth")
|
||||||
def electric_kiwi_auth() -> YieldFixture:
|
def electric_kiwi_auth() -> Generator[AsyncMock]:
|
||||||
"""Patch access to electric kiwi access token."""
|
"""Patch access to electric kiwi access token."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.electric_kiwi.api.AsyncConfigEntryAuth"
|
"homeassistant.components.electric_kiwi.api.ConfigEntryElectricKiwiAuth"
|
||||||
) as mock_auth:
|
) as mock_auth:
|
||||||
mock_auth.return_value.async_get_access_token = AsyncMock("auth_token")
|
mock_auth.return_value.async_get_access_token = AsyncMock("auth_token")
|
||||||
yield mock_auth
|
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": {
|
"data": {
|
||||||
"connection_id": "3",
|
"type": "hop_customer",
|
||||||
"customer_number": 1000001,
|
"customer_id": 123456,
|
||||||
"end": {
|
"service_type": "electricity",
|
||||||
"end_time": "5:00 PM",
|
"connection_id": 515363,
|
||||||
"interval": "34"
|
"billing_id": 1247975,
|
||||||
},
|
|
||||||
"start": {
|
"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
|
"status": 1
|
||||||
}
|
}
|
||||||
|
@ -1,249 +1,250 @@
|
|||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"hop_duration": "60",
|
|
||||||
"type": "hop_intervals",
|
"type": "hop_intervals",
|
||||||
|
"hop_duration": "60",
|
||||||
"intervals": {
|
"intervals": {
|
||||||
"1": {
|
"1": {
|
||||||
"active": 1,
|
"start_time": "12:00 AM",
|
||||||
"end_time": "1:00 AM",
|
"end_time": "1:00 AM",
|
||||||
"start_time": "12:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"active": 1,
|
"start_time": "12:30 AM",
|
||||||
"end_time": "1:30 AM",
|
"end_time": "1:30 AM",
|
||||||
"start_time": "12:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"3": {
|
"3": {
|
||||||
"active": 1,
|
"start_time": "1:00 AM",
|
||||||
"end_time": "2:00 AM",
|
"end_time": "2:00 AM",
|
||||||
"start_time": "1:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"4": {
|
"4": {
|
||||||
"active": 1,
|
"start_time": "1:30 AM",
|
||||||
"end_time": "2:30 AM",
|
"end_time": "2:30 AM",
|
||||||
"start_time": "1:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"5": {
|
"5": {
|
||||||
"active": 1,
|
"start_time": "2:00 AM",
|
||||||
"end_time": "3:00 AM",
|
"end_time": "3:00 AM",
|
||||||
"start_time": "2:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"6": {
|
"6": {
|
||||||
"active": 1,
|
"start_time": "2:30 AM",
|
||||||
"end_time": "3:30 AM",
|
"end_time": "3:30 AM",
|
||||||
"start_time": "2:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"7": {
|
"7": {
|
||||||
"active": 1,
|
"start_time": "3:00 AM",
|
||||||
"end_time": "4:00 AM",
|
"end_time": "4:00 AM",
|
||||||
"start_time": "3:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"8": {
|
"8": {
|
||||||
"active": 1,
|
"start_time": "3:30 AM",
|
||||||
"end_time": "4:30 AM",
|
"end_time": "4:30 AM",
|
||||||
"start_time": "3:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"active": 1,
|
"start_time": "4:00 AM",
|
||||||
"end_time": "5:00 AM",
|
"end_time": "5:00 AM",
|
||||||
"start_time": "4:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"10": {
|
"10": {
|
||||||
"active": 1,
|
"start_time": "4:30 AM",
|
||||||
"end_time": "5:30 AM",
|
"end_time": "5:30 AM",
|
||||||
"start_time": "4:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"11": {
|
"11": {
|
||||||
"active": 1,
|
"start_time": "5:00 AM",
|
||||||
"end_time": "6:00 AM",
|
"end_time": "6:00 AM",
|
||||||
"start_time": "5:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"12": {
|
"12": {
|
||||||
"active": 1,
|
"start_time": "5:30 AM",
|
||||||
"end_time": "6:30 AM",
|
"end_time": "6:30 AM",
|
||||||
"start_time": "5:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"13": {
|
"13": {
|
||||||
"active": 1,
|
"start_time": "6:00 AM",
|
||||||
"end_time": "7:00 AM",
|
"end_time": "7:00 AM",
|
||||||
"start_time": "6:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"14": {
|
"14": {
|
||||||
"active": 1,
|
"start_time": "6:30 AM",
|
||||||
"end_time": "7:30 AM",
|
"end_time": "7:30 AM",
|
||||||
"start_time": "6:30 AM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"15": {
|
"15": {
|
||||||
"active": 1,
|
"start_time": "7:00 AM",
|
||||||
"end_time": "8:00 AM",
|
"end_time": "8:00 AM",
|
||||||
"start_time": "7:00 AM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"16": {
|
"16": {
|
||||||
"active": 1,
|
"start_time": "7:30 AM",
|
||||||
"end_time": "8:30 AM",
|
"end_time": "8:30 AM",
|
||||||
"start_time": "7:30 AM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"17": {
|
"17": {
|
||||||
"active": 1,
|
"start_time": "8:00 AM",
|
||||||
"end_time": "9:00 AM",
|
"end_time": "9:00 AM",
|
||||||
"start_time": "8:00 AM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"18": {
|
"18": {
|
||||||
"active": 1,
|
"start_time": "8:30 AM",
|
||||||
"end_time": "9:30 AM",
|
"end_time": "9:30 AM",
|
||||||
"start_time": "8:30 AM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"19": {
|
"19": {
|
||||||
"active": 1,
|
"start_time": "9:00 AM",
|
||||||
"end_time": "10:00 AM",
|
"end_time": "10:00 AM",
|
||||||
"start_time": "9:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"20": {
|
"20": {
|
||||||
"active": 1,
|
"start_time": "9:30 AM",
|
||||||
"end_time": "10:30 AM",
|
"end_time": "10:30 AM",
|
||||||
"start_time": "9:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"21": {
|
"21": {
|
||||||
"active": 1,
|
"start_time": "10:00 AM",
|
||||||
"end_time": "11:00 AM",
|
"end_time": "11:00 AM",
|
||||||
"start_time": "10:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"22": {
|
"22": {
|
||||||
"active": 1,
|
"start_time": "10:30 AM",
|
||||||
"end_time": "11:30 AM",
|
"end_time": "11:30 AM",
|
||||||
"start_time": "10:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"23": {
|
"23": {
|
||||||
"active": 1,
|
"start_time": "11:00 AM",
|
||||||
"end_time": "12:00 PM",
|
"end_time": "12:00 PM",
|
||||||
"start_time": "11:00 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"24": {
|
"24": {
|
||||||
"active": 1,
|
"start_time": "11:30 AM",
|
||||||
"end_time": "12:30 PM",
|
"end_time": "12:30 PM",
|
||||||
"start_time": "11:30 AM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"25": {
|
"25": {
|
||||||
"active": 1,
|
"start_time": "12:00 PM",
|
||||||
"end_time": "1:00 PM",
|
"end_time": "1:00 PM",
|
||||||
"start_time": "12:00 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"26": {
|
"26": {
|
||||||
"active": 1,
|
"start_time": "12:30 PM",
|
||||||
"end_time": "1:30 PM",
|
"end_time": "1:30 PM",
|
||||||
"start_time": "12:30 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"27": {
|
"27": {
|
||||||
"active": 1,
|
"start_time": "1:00 PM",
|
||||||
"end_time": "2:00 PM",
|
"end_time": "2:00 PM",
|
||||||
"start_time": "1:00 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"28": {
|
"28": {
|
||||||
"active": 1,
|
"start_time": "1:30 PM",
|
||||||
"end_time": "2:30 PM",
|
"end_time": "2:30 PM",
|
||||||
"start_time": "1:30 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"29": {
|
"29": {
|
||||||
"active": 1,
|
"start_time": "2:00 PM",
|
||||||
"end_time": "3:00 PM",
|
"end_time": "3:00 PM",
|
||||||
"start_time": "2:00 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"30": {
|
"30": {
|
||||||
"active": 1,
|
"start_time": "2:30 PM",
|
||||||
"end_time": "3:30 PM",
|
"end_time": "3:30 PM",
|
||||||
"start_time": "2:30 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"31": {
|
"31": {
|
||||||
"active": 1,
|
"start_time": "3:00 PM",
|
||||||
"end_time": "4:00 PM",
|
"end_time": "4:00 PM",
|
||||||
"start_time": "3:00 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"32": {
|
"32": {
|
||||||
"active": 1,
|
"start_time": "3:30 PM",
|
||||||
"end_time": "4:30 PM",
|
"end_time": "4:30 PM",
|
||||||
"start_time": "3:30 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"33": {
|
"33": {
|
||||||
"active": 1,
|
"start_time": "4:00 PM",
|
||||||
"end_time": "5:00 PM",
|
"end_time": "5:00 PM",
|
||||||
"start_time": "4:00 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"34": {
|
"34": {
|
||||||
"active": 1,
|
"start_time": "4:30 PM",
|
||||||
"end_time": "5:30 PM",
|
"end_time": "5:30 PM",
|
||||||
"start_time": "4:30 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"35": {
|
"35": {
|
||||||
"active": 1,
|
"start_time": "5:00 PM",
|
||||||
"end_time": "6:00 PM",
|
"end_time": "6:00 PM",
|
||||||
"start_time": "5:00 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"36": {
|
"36": {
|
||||||
"active": 1,
|
"start_time": "5:30 PM",
|
||||||
"end_time": "6:30 PM",
|
"end_time": "6:30 PM",
|
||||||
"start_time": "5:30 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"37": {
|
"37": {
|
||||||
"active": 1,
|
"start_time": "6:00 PM",
|
||||||
"end_time": "7:00 PM",
|
"end_time": "7:00 PM",
|
||||||
"start_time": "6:00 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"38": {
|
"38": {
|
||||||
"active": 1,
|
"start_time": "6:30 PM",
|
||||||
"end_time": "7:30 PM",
|
"end_time": "7:30 PM",
|
||||||
"start_time": "6:30 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"39": {
|
"39": {
|
||||||
"active": 1,
|
"start_time": "7:00 PM",
|
||||||
"end_time": "8:00 PM",
|
"end_time": "8:00 PM",
|
||||||
"start_time": "7:00 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"40": {
|
"40": {
|
||||||
"active": 1,
|
"start_time": "7:30 PM",
|
||||||
"end_time": "8:30 PM",
|
"end_time": "8:30 PM",
|
||||||
"start_time": "7:30 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"41": {
|
"41": {
|
||||||
"active": 1,
|
"start_time": "8:00 PM",
|
||||||
"end_time": "9:00 PM",
|
"end_time": "9:00 PM",
|
||||||
"start_time": "8:00 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"42": {
|
"42": {
|
||||||
"active": 1,
|
"start_time": "8:30 PM",
|
||||||
"end_time": "9:30 PM",
|
"end_time": "9:30 PM",
|
||||||
"start_time": "8:30 PM"
|
"active": 0
|
||||||
},
|
},
|
||||||
"43": {
|
"43": {
|
||||||
"active": 1,
|
"start_time": "9:00 PM",
|
||||||
"end_time": "10:00 PM",
|
"end_time": "10:00 PM",
|
||||||
"start_time": "9:00 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"44": {
|
"44": {
|
||||||
"active": 1,
|
"start_time": "9:30 PM",
|
||||||
"end_time": "10:30 PM",
|
"end_time": "10:30 PM",
|
||||||
"start_time": "9:30 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"45": {
|
"45": {
|
||||||
"active": 1,
|
"start_time": "10:00 PM",
|
||||||
"end_time": "11:00 AM",
|
"end_time": "11:00 PM",
|
||||||
"start_time": "10:00 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"46": {
|
"46": {
|
||||||
"active": 1,
|
"start_time": "10:30 PM",
|
||||||
"end_time": "11:30 PM",
|
"end_time": "11:30 PM",
|
||||||
"start_time": "10:30 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"47": {
|
"47": {
|
||||||
"active": 1,
|
"start_time": "11:00 PM",
|
||||||
"end_time": "12:00 AM",
|
"end_time": "12:00 AM",
|
||||||
"start_time": "11:00 PM"
|
"active": 1
|
||||||
},
|
},
|
||||||
"48": {
|
"48": {
|
||||||
"active": 1,
|
"start_time": "11:30 PM",
|
||||||
"end_time": "12:30 AM",
|
"end_time": "12:30 AM",
|
||||||
"start_time": "11:30 PM"
|
"active": 0
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"service_type": "electricity"
|
||||||
},
|
},
|
||||||
"status": 1
|
"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 __future__ import annotations
|
||||||
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from electrickiwi_api.exceptions import ApiException
|
||||||
import pytest
|
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 (
|
from homeassistant.components.electric_kiwi.const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
OAUTH2_AUTHORIZE,
|
OAUTH2_AUTHORIZE,
|
||||||
OAUTH2_TOKEN,
|
OAUTH2_TOKEN,
|
||||||
SCOPE_VALUES,
|
SCOPE_VALUES,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
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, REDIRECT_URI
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
from tests.typing import ClientSessionGenerator
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("current_request_with_host", "electrickiwi_api")
|
||||||
@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")
|
|
||||||
async def test_full_flow(
|
async def test_full_flow(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
setup_credentials: None,
|
|
||||||
mock_setup_entry: AsyncMock,
|
mock_setup_entry: AsyncMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check full flow."""
|
"""Check full flow."""
|
||||||
await async_import_client_credential(
|
|
||||||
hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET)
|
|
||||||
)
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
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(
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
hass,
|
hass,
|
||||||
@ -76,13 +46,13 @@ async def test_full_flow(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
URL_SCOPE = SCOPE_VALUES.replace(" ", "+")
|
url_scope = SCOPE_VALUES.replace(" ", "+")
|
||||||
|
|
||||||
assert result["url"] == (
|
assert result["url"] == (
|
||||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||||
f"&redirect_uri={REDIRECT_URI}"
|
f"&redirect_uri={REDIRECT_URI}"
|
||||||
f"&state={state}"
|
f"&state={state}"
|
||||||
f"&scope={URL_SCOPE}"
|
f"&scope={url_scope}"
|
||||||
)
|
)
|
||||||
|
|
||||||
client = await hass_client_no_auth()
|
client = await hass_client_no_auth()
|
||||||
@ -90,6 +60,7 @@ async def test_full_flow(
|
|||||||
assert resp.status == HTTPStatus.OK
|
assert resp.status == HTTPStatus.OK
|
||||||
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
aioclient_mock.post(
|
aioclient_mock.post(
|
||||||
OAUTH2_TOKEN,
|
OAUTH2_TOKEN,
|
||||||
json={
|
json={
|
||||||
@ -106,20 +77,73 @@ async def test_full_flow(
|
|||||||
assert len(mock_setup_entry.mock_calls) == 1
|
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")
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
async def test_existing_entry(
|
async def test_existing_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
setup_credentials: None,
|
migrated_config_entry: MockConfigEntry,
|
||||||
config_entry: MockConfigEntry,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check existing entry."""
|
"""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
|
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(
|
||||||
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(
|
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
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
|
||||||
|
|
||||||
@ -154,13 +180,13 @@ async def test_reauthentication(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
mock_setup_entry: MagicMock,
|
mock_setup_entry: AsyncMock,
|
||||||
config_entry: MockConfigEntry,
|
migrated_config_entry: MockConfigEntry,
|
||||||
setup_credentials: None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Electric Kiwi reauthentication."""
|
"""Test Electric Kiwi reauthentication."""
|
||||||
config_entry.add_to_hass(hass)
|
migrated_config_entry.add_to_hass(hass)
|
||||||
result = await config_entry.start_reauth_flow(hass)
|
|
||||||
|
result = await migrated_config_entry.start_reauth_flow(hass)
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "reauth_confirm"
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
assert len(mock_setup_entry.mock_calls) == 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.helpers.entity_registry import EntityRegistry
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .conftest import ComponentSetup, YieldFixture
|
from . import init_integration
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -47,10 +47,9 @@ def restore_timezone():
|
|||||||
async def test_hop_sensors(
|
async def test_hop_sensors(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
ek_api: YieldFixture,
|
electrickiwi_api: Mock,
|
||||||
ek_auth: YieldFixture,
|
ek_auth: AsyncMock,
|
||||||
entity_registry: EntityRegistry,
|
entity_registry: EntityRegistry,
|
||||||
component_setup: ComponentSetup,
|
|
||||||
sensor: str,
|
sensor: str,
|
||||||
sensor_state: str,
|
sensor_state: str,
|
||||||
) -> None:
|
) -> 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,
|
sensor state should be set to today at 4pm or if now is past 4pm,
|
||||||
then tomorrow at 4pm.
|
then tomorrow at 4pm.
|
||||||
"""
|
"""
|
||||||
assert await component_setup()
|
await init_integration(hass, config_entry)
|
||||||
assert config_entry.state is ConfigEntryState.LOADED
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
entity = entity_registry.async_get(sensor)
|
entity = entity_registry.async_get(sensor)
|
||||||
@ -70,8 +69,7 @@ async def test_hop_sensors(
|
|||||||
state = hass.states.get(sensor)
|
state = hass.states.get(sensor)
|
||||||
assert state
|
assert state
|
||||||
|
|
||||||
api = ek_api(Mock())
|
hop_data = await electrickiwi_api.get_hop()
|
||||||
hop_data = await api.get_hop()
|
|
||||||
|
|
||||||
value = _check_and_move_time(hop_data, sensor_state)
|
value = _check_and_move_time(hop_data, sensor_state)
|
||||||
|
|
||||||
@ -98,20 +96,19 @@ async def test_hop_sensors(
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"sensor.next_billing_date",
|
"sensor.next_billing_date",
|
||||||
"2020-11-03T00:00:00",
|
"2025-02-19T00:00:00",
|
||||||
SensorDeviceClass.DATE,
|
SensorDeviceClass.DATE,
|
||||||
None,
|
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(
|
async def test_account_sensors(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
ek_api: YieldFixture,
|
electrickiwi_api: AsyncMock,
|
||||||
ek_auth: YieldFixture,
|
ek_auth: AsyncMock,
|
||||||
entity_registry: EntityRegistry,
|
entity_registry: EntityRegistry,
|
||||||
component_setup: ComponentSetup,
|
|
||||||
sensor: str,
|
sensor: str,
|
||||||
sensor_state: str,
|
sensor_state: str,
|
||||||
device_class: str,
|
device_class: str,
|
||||||
@ -119,7 +116,7 @@ async def test_account_sensors(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test Account sensors for the Electric Kiwi integration."""
|
"""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
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
entity = entity_registry.async_get(sensor)
|
entity = entity_registry.async_get(sensor)
|
||||||
@ -133,9 +130,9 @@ async def test_account_sensors(
|
|||||||
assert state.attributes.get(ATTR_STATE_CLASS) == state_class
|
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."""
|
"""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)
|
test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TEST_TIMEZONE)
|
||||||
dt_util.set_default_time_zone(TEST_TIMEZONE)
|
dt_util.set_default_time_zone(TEST_TIMEZONE)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user