mirror of
https://github.com/home-assistant/core.git
synced 2026-05-08 17:49:37 +01:00
Remove Flick Electric integration (#155469)
This commit is contained in:
Generated
-2
@@ -512,8 +512,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/fjaraskupan/ @elupus
|
||||
/homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||
/tests/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||
/homeassistant/components/flick_electric/ @ZephireNZ
|
||||
/tests/components/flick_electric/ @ZephireNZ
|
||||
/homeassistant/components/flipr/ @cnico
|
||||
/tests/components/flipr/ @cnico
|
||||
/homeassistant/components/flo/ @dmulcahey
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
"""The Flick Electric integration."""
|
||||
|
||||
from datetime import datetime as dt
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
from pyflick import FlickAPI
|
||||
from pyflick.authentication import SimpleFlickAuth
|
||||
from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_ACCOUNT_ID, CONF_SUPPLY_NODE_REF, CONF_TOKEN_EXPIRY
|
||||
from .coordinator import FlickConfigEntry, FlickElectricDataCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ID_TOKEN = "id_token"
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: FlickConfigEntry) -> bool:
|
||||
"""Set up Flick Electric from a config entry."""
|
||||
auth = HassFlickAuth(hass, entry)
|
||||
|
||||
coordinator = FlickElectricDataCoordinator(hass, entry, FlickAPI(auth))
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: FlickConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: FlickConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug(
|
||||
"Migrating configuration from version %s.%s",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
if config_entry.version > 2:
|
||||
return False
|
||||
|
||||
if config_entry.version == 1:
|
||||
api = FlickAPI(HassFlickAuth(hass, config_entry))
|
||||
|
||||
accounts = await api.getCustomerAccounts()
|
||||
active_accounts = [
|
||||
account for account in accounts if account["status"] == "active"
|
||||
]
|
||||
|
||||
# A single active account can be auto-migrated
|
||||
if (len(active_accounts)) == 1:
|
||||
account = active_accounts[0]
|
||||
|
||||
new_data = {**config_entry.data}
|
||||
new_data[CONF_ACCOUNT_ID] = account["id"]
|
||||
new_data[CONF_SUPPLY_NODE_REF] = account["main_consumer"]["supply_node_ref"]
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
title=account["address"],
|
||||
unique_id=account["id"],
|
||||
data=new_data,
|
||||
version=2,
|
||||
)
|
||||
return True
|
||||
|
||||
config_entry.async_start_reauth(hass, data={**config_entry.data})
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HassFlickAuth(SimpleFlickAuth):
|
||||
"""Implementation of AbstractFlickAuth based on a Home Assistant entity config."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: FlickConfigEntry) -> None:
|
||||
"""Flick authentication based on a Home Assistant entity config."""
|
||||
super().__init__(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
client_id=entry.data.get(CONF_CLIENT_ID, DEFAULT_CLIENT_ID),
|
||||
client_secret=entry.data.get(CONF_CLIENT_SECRET, DEFAULT_CLIENT_SECRET),
|
||||
websession=aiohttp_client.async_get_clientsession(hass),
|
||||
)
|
||||
self._entry = entry
|
||||
self._hass = hass
|
||||
|
||||
async def _get_entry_token(self) -> dict[str, Any]:
|
||||
# No token saved, generate one
|
||||
if (
|
||||
CONF_TOKEN_EXPIRY not in self._entry.data
|
||||
or CONF_ACCESS_TOKEN not in self._entry.data
|
||||
):
|
||||
await self._update_token()
|
||||
|
||||
# Token is expired, generate a new one
|
||||
if self._entry.data[CONF_TOKEN_EXPIRY] <= dt.now().timestamp():
|
||||
await self._update_token()
|
||||
|
||||
return self._entry.data[CONF_ACCESS_TOKEN]
|
||||
|
||||
async def _update_token(self):
|
||||
_LOGGER.debug("Fetching new access token")
|
||||
|
||||
token = await super().get_new_token(
|
||||
self._username, self._password, self._client_id, self._client_secret
|
||||
)
|
||||
|
||||
_LOGGER.debug("New token: %s", token)
|
||||
|
||||
# Flick will send the same token, but expiry is relative - so grab it from the token
|
||||
token_decoded = jwt.decode(
|
||||
token[CONF_ID_TOKEN], options={"verify_signature": False}
|
||||
)
|
||||
|
||||
self._hass.config_entries.async_update_entry(
|
||||
self._entry,
|
||||
data={
|
||||
**self._entry.data,
|
||||
CONF_ACCESS_TOKEN: token,
|
||||
CONF_TOKEN_EXPIRY: token_decoded["exp"],
|
||||
},
|
||||
)
|
||||
|
||||
async def async_get_access_token(self):
|
||||
"""Get Access Token from HASS Storage."""
|
||||
token = await self._get_entry_token()
|
||||
|
||||
return token[CONF_ID_TOKEN]
|
||||
@@ -1,210 +0,0 @@
|
||||
"""Config Flow for Flick Electric integration."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pyflick import FlickAPI
|
||||
from pyflick.authentication import AbstractFlickAuth, SimpleFlickAuth
|
||||
from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET
|
||||
from pyflick.types import APIException, AuthException, CustomerAccount
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_ACCOUNT_ID, CONF_SUPPLY_NODE_REF, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LOGIN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_CLIENT_ID): str,
|
||||
vol.Optional(CONF_CLIENT_SECRET): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FlickConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Flick config flow."""
|
||||
|
||||
VERSION = 2
|
||||
auth: AbstractFlickAuth
|
||||
accounts: list[CustomerAccount]
|
||||
data: dict[str, Any]
|
||||
|
||||
async def _validate_auth(self, user_input: Mapping[str, Any]) -> bool:
|
||||
self.auth = SimpleFlickAuth(
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
websession=aiohttp_client.async_get_clientsession(self.hass),
|
||||
client_id=user_input.get(CONF_CLIENT_ID, DEFAULT_CLIENT_ID),
|
||||
client_secret=user_input.get(CONF_CLIENT_SECRET, DEFAULT_CLIENT_SECRET),
|
||||
)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(60):
|
||||
token = await self.auth.async_get_access_token()
|
||||
except (TimeoutError, ClientResponseError) as err:
|
||||
raise CannotConnect from err
|
||||
except AuthException as err:
|
||||
raise InvalidAuth from err
|
||||
|
||||
return token is not None
|
||||
|
||||
async def async_step_select_account(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Ask user to select account."""
|
||||
|
||||
errors = {}
|
||||
if user_input is not None and CONF_ACCOUNT_ID in user_input:
|
||||
self.data[CONF_ACCOUNT_ID] = user_input[CONF_ACCOUNT_ID]
|
||||
self.data[CONF_SUPPLY_NODE_REF] = self._get_supply_node_ref(
|
||||
user_input[CONF_ACCOUNT_ID]
|
||||
)
|
||||
try:
|
||||
# Ensure supply node is active
|
||||
await FlickAPI(self.auth).getPricing(self.data[CONF_SUPPLY_NODE_REF])
|
||||
except (APIException, ClientResponseError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except AuthException:
|
||||
# We should never get here as we have a valid token
|
||||
return self.async_abort(reason="no_permissions")
|
||||
else:
|
||||
# Supply node is active
|
||||
return await self._async_create_entry()
|
||||
|
||||
try:
|
||||
self.accounts = await FlickAPI(self.auth).getCustomerAccounts()
|
||||
except (APIException, ClientResponseError):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
active_accounts = [a for a in self.accounts if a["status"] == "active"]
|
||||
|
||||
if len(active_accounts) == 0:
|
||||
return self.async_abort(reason="no_accounts")
|
||||
|
||||
if len(active_accounts) == 1:
|
||||
self.data[CONF_ACCOUNT_ID] = active_accounts[0]["id"]
|
||||
self.data[CONF_SUPPLY_NODE_REF] = self._get_supply_node_ref(
|
||||
active_accounts[0]["id"]
|
||||
)
|
||||
|
||||
return await self._async_create_entry()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="select_account",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ACCOUNT_ID): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
SelectOptionDict(
|
||||
value=account["id"], label=account["address"]
|
||||
)
|
||||
for account in active_accounts
|
||||
],
|
||||
mode=SelectSelectorMode.LIST,
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle gathering login info."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self._validate_auth(user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.data = dict(user_input)
|
||||
return await self.async_step_select_account(user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=LOGIN_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, user_input: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle re-authentication."""
|
||||
|
||||
self.data = {**user_input}
|
||||
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def _async_create_entry(self) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow."""
|
||||
|
||||
await self.async_set_unique_id(self.data[CONF_ACCOUNT_ID])
|
||||
|
||||
account = self._get_account(self.data[CONF_ACCOUNT_ID])
|
||||
|
||||
if self.source == SOURCE_REAUTH:
|
||||
# Migration completed
|
||||
if self._get_reauth_entry().version == 1:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self._get_reauth_entry(),
|
||||
unique_id=self.unique_id,
|
||||
data=self.data,
|
||||
version=self.VERSION,
|
||||
)
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
unique_id=self.unique_id,
|
||||
title=account["address"],
|
||||
data=self.data,
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=account["address"],
|
||||
data=self.data,
|
||||
)
|
||||
|
||||
def _get_account(self, account_id: str) -> CustomerAccount:
|
||||
"""Get the account for the account ID."""
|
||||
return next(a for a in self.accounts if a["id"] == account_id)
|
||||
|
||||
def _get_supply_node_ref(self, account_id: str) -> str:
|
||||
"""Get the supply node ref for the account."""
|
||||
return self._get_account(account_id)["main_consumer"][CONF_SUPPLY_NODE_REF]
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
@@ -1,12 +0,0 @@
|
||||
"""Constants for the Flick Electric integration."""
|
||||
|
||||
DOMAIN = "flick_electric"
|
||||
|
||||
CONF_TOKEN_EXPIRY = "expires"
|
||||
CONF_ACCOUNT_ID = "account_id"
|
||||
CONF_SUPPLY_NODE_REF = "supply_node_ref"
|
||||
|
||||
ATTR_START_AT = "start_at"
|
||||
ATTR_END_AT = "end_at"
|
||||
|
||||
ATTR_COMPONENTS = ["retailer", "ea", "metering", "generation", "admin", "network"]
|
||||
@@ -1,55 +0,0 @@
|
||||
"""Data Coordinator for Flick Electric."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from pyflick import FlickAPI, FlickPrice
|
||||
from pyflick.types import APIException, AuthException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SUPPLY_NODE_REF
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
type FlickConfigEntry = ConfigEntry[FlickElectricDataCoordinator]
|
||||
|
||||
|
||||
class FlickElectricDataCoordinator(DataUpdateCoordinator[FlickPrice]):
|
||||
"""Coordinator for flick power price."""
|
||||
|
||||
config_entry: FlickConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: FlickConfigEntry,
|
||||
api: FlickAPI,
|
||||
) -> None:
|
||||
"""Initialize FlickElectricDataCoordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name="Flick Electric",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.supply_node_ref = config_entry.data[CONF_SUPPLY_NODE_REF]
|
||||
self._api = api
|
||||
|
||||
async def _async_update_data(self) -> FlickPrice:
|
||||
"""Fetch pricing data from Flick Electric."""
|
||||
try:
|
||||
async with asyncio.timeout(60):
|
||||
return await self._api.getPricing(self.supply_node_ref)
|
||||
except AuthException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (APIException, aiohttp.ClientResponseError) as err:
|
||||
raise UpdateFailed from err
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "flick_electric",
|
||||
"name": "Flick Electric",
|
||||
"codeowners": ["@ZephireNZ"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flick_electric",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyflick"],
|
||||
"requirements": ["PyFlick==1.1.3"]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
"""Support for Flick Electric Pricing data."""
|
||||
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import CURRENCY_CENT, UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTR_COMPONENTS, ATTR_END_AT, ATTR_START_AT
|
||||
from .coordinator import FlickConfigEntry, FlickElectricDataCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: FlickConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Flick Sensor Setup."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities([FlickPricingSensor(coordinator)])
|
||||
|
||||
|
||||
class FlickPricingSensor(CoordinatorEntity[FlickElectricDataCoordinator], SensorEntity):
|
||||
"""Entity object for Flick Electric sensor."""
|
||||
|
||||
_attr_attribution = "Data provided by Flick Electric"
|
||||
_attr_native_unit_of_measurement = f"{CURRENCY_CENT}/{UnitOfEnergy.KILO_WATT_HOUR}"
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "power_price"
|
||||
|
||||
def __init__(self, coordinator: FlickElectricDataCoordinator) -> None:
|
||||
"""Entity object for Flick Electric sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.supply_node_ref}_pricing"
|
||||
|
||||
@property
|
||||
def native_value(self) -> Decimal:
|
||||
"""Return the state of the sensor."""
|
||||
# The API should return a unit price with quantity of 1.0 when no start/end time is provided
|
||||
if self.coordinator.data.quantity != 1:
|
||||
_LOGGER.warning(
|
||||
"Unexpected quantity for unit price: %s", self.coordinator.data
|
||||
)
|
||||
return self.coordinator.data.cost * 100
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return the state attributes."""
|
||||
components: dict[str, float] = {}
|
||||
|
||||
for component in self.coordinator.data.components:
|
||||
if component.charge_setter not in ATTR_COMPONENTS:
|
||||
_LOGGER.warning("Found unknown component: %s", component.charge_setter)
|
||||
continue
|
||||
|
||||
components[component.charge_setter] = float(component.value * 100)
|
||||
|
||||
return {
|
||||
ATTR_START_AT: self.coordinator.data.start_at,
|
||||
ATTR_END_AT: self.coordinator.data.end_at,
|
||||
**components,
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"no_accounts": "No services are active on this Flick account",
|
||||
"no_permissions": "Cannot get pricing for this account. Please check user permissions.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"select_account": {
|
||||
"data": {
|
||||
"account_id": "Account"
|
||||
},
|
||||
"title": "Select account"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"client_id": "Client ID (optional)",
|
||||
"client_secret": "Client Secret (optional)",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"title": "Flick Login Credentials"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"power_price": {
|
||||
"name": "Flick power price"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-1
@@ -210,7 +210,6 @@ FLOWS = {
|
||||
"fivem",
|
||||
"fjaraskupan",
|
||||
"flexit_bacnet",
|
||||
"flick_electric",
|
||||
"flipr",
|
||||
"flo",
|
||||
"flume",
|
||||
|
||||
@@ -2054,12 +2054,6 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"flick_electric": {
|
||||
"name": "Flick Electric",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"flipr": {
|
||||
"name": "Flipr",
|
||||
"integration_type": "hub",
|
||||
|
||||
Generated
-3
@@ -49,9 +49,6 @@ ProgettiHWSW==0.1.3
|
||||
# homeassistant.components.cast
|
||||
PyChromecast==14.0.9
|
||||
|
||||
# homeassistant.components.flick_electric
|
||||
PyFlick==1.1.3
|
||||
|
||||
# homeassistant.components.flume
|
||||
PyFlume==0.6.5
|
||||
|
||||
|
||||
Generated
-3
@@ -46,9 +46,6 @@ ProgettiHWSW==0.1.3
|
||||
# homeassistant.components.cast
|
||||
PyChromecast==14.0.9
|
||||
|
||||
# homeassistant.components.flick_electric
|
||||
PyFlick==1.1.3
|
||||
|
||||
# homeassistant.components.flume
|
||||
PyFlume==0.6.5
|
||||
|
||||
|
||||
@@ -381,7 +381,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"fleetgo",
|
||||
"flexit",
|
||||
"flic",
|
||||
"flick_electric",
|
||||
"flipr",
|
||||
"flo",
|
||||
"flock",
|
||||
@@ -1397,7 +1396,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"fleetgo",
|
||||
"flexit",
|
||||
"flic",
|
||||
"flick_electric",
|
||||
"flipr",
|
||||
"flo",
|
||||
"flock",
|
||||
|
||||
@@ -928,7 +928,6 @@
|
||||
"oncue": 73,
|
||||
"tailwind": 44,
|
||||
"dunehd": 102,
|
||||
"flick_electric": 42,
|
||||
"home_plus_control": 85,
|
||||
"weishaupt_wcm_com": 1,
|
||||
"sentry": 63,
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
"""Tests for the Flick Electric integration."""
|
||||
|
||||
from pyflick.types import FlickPrice
|
||||
|
||||
from homeassistant.components.flick_electric.const import (
|
||||
CONF_ACCOUNT_ID,
|
||||
CONF_SUPPLY_NODE_REF,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONF = {
|
||||
CONF_USERNAME: "9973debf-963f-49b0-9a73-ba9c3400cbed@anonymised.example.com",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_ACCOUNT_ID: "134800",
|
||||
CONF_SUPPLY_NODE_REF: "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef8299",
|
||||
}
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
def _mock_flick_price():
|
||||
return FlickPrice(
|
||||
{
|
||||
"cost": "0.25",
|
||||
"quantity": "1.0",
|
||||
"status": "final",
|
||||
"start_at": "2024-01-01T00:00:00Z",
|
||||
"end_at": "2024-01-01T00:00:00Z",
|
||||
"type": "flat",
|
||||
"components": [
|
||||
{
|
||||
"charge_method": "kwh",
|
||||
"charge_setter": "network",
|
||||
"value": "1.00",
|
||||
"single_unit_price": "1.00",
|
||||
"quantity": "1.0",
|
||||
"unit_code": "NZD",
|
||||
"charge_per": "kwh",
|
||||
"flow_direction": "import",
|
||||
},
|
||||
{
|
||||
"charge_method": "kwh",
|
||||
"charge_setter": "nonsupported",
|
||||
"value": "1.00",
|
||||
"single_unit_price": "1.00",
|
||||
"quantity": "1.0",
|
||||
"unit_code": "NZD",
|
||||
"charge_per": "kwh",
|
||||
"flow_direction": "import",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -1,105 +0,0 @@
|
||||
"""Flick Electric tests configuration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import json_api_doc
|
||||
from pyflick import FlickPrice
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.flick_electric.const import CONF_ACCOUNT_ID, DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from . import CONF
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_value_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="123 Fake Street, Newtown, Wellington 6021",
|
||||
data={**CONF},
|
||||
version=2,
|
||||
entry_id="974e52a5c0724d17b7ed876dd6ff4bc8",
|
||||
unique_id=CONF[CONF_ACCOUNT_ID],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_old_config_entry() -> MockConfigEntry:
|
||||
"""Mock an outdated config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_USERNAME: CONF[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONF[CONF_PASSWORD],
|
||||
},
|
||||
title=CONF[CONF_USERNAME],
|
||||
unique_id=CONF[CONF_USERNAME],
|
||||
version=1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_flick_client() -> Generator[AsyncMock]:
|
||||
"""Mock a Flick Electric client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.flick_electric.FlickAPI",
|
||||
autospec=True,
|
||||
) as mock_api,
|
||||
patch(
|
||||
"homeassistant.components.flick_electric.config_flow.FlickAPI",
|
||||
new=mock_api,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
|
||||
return_value="123456789abcdef",
|
||||
),
|
||||
):
|
||||
api = mock_api.return_value
|
||||
|
||||
api.getCustomerAccounts.return_value = json_api_doc.deserialize(
|
||||
load_json_value_fixture("accounts.json", DOMAIN)
|
||||
)
|
||||
api.getPricing.return_value = FlickPrice(
|
||||
json_api_doc.deserialize(
|
||||
load_json_value_fixture("rated_period.json", DOMAIN)
|
||||
)
|
||||
)
|
||||
|
||||
yield api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_flick_client_multiple() -> Generator[AsyncMock]:
|
||||
"""Mock a Flick Electric with multiple accounts."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.flick_electric.FlickAPI",
|
||||
autospec=True,
|
||||
) as mock_api,
|
||||
patch(
|
||||
"homeassistant.components.flick_electric.config_flow.FlickAPI",
|
||||
new=mock_api,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
|
||||
return_value="123456789abcdef",
|
||||
),
|
||||
):
|
||||
api = mock_api.return_value
|
||||
|
||||
api.getCustomerAccounts.return_value = json_api_doc.deserialize(
|
||||
load_json_value_fixture("accounts_multi.json", DOMAIN)
|
||||
)
|
||||
api.getPricing.return_value = FlickPrice(
|
||||
json_api_doc.deserialize(
|
||||
load_json_value_fixture("rated_period.json", DOMAIN)
|
||||
)
|
||||
)
|
||||
|
||||
yield api
|
||||
@@ -1,105 +0,0 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "134800",
|
||||
"type": "customer_account",
|
||||
"attributes": {
|
||||
"account_number": "10123404",
|
||||
"billing_name": "9973debf-963f-49b0-9a73-Ba9c3400cbed@Anonymised Example",
|
||||
"billing_email": null,
|
||||
"address": "123 Fake Street, Newtown, Wellington 6021",
|
||||
"brand": "flick",
|
||||
"vulnerability_state": "none",
|
||||
"medical_dependency": false,
|
||||
"status": "active",
|
||||
"start_at": "2023-03-02T00:00:00.000+13:00",
|
||||
"end_at": null,
|
||||
"application_id": "5dfc4978-07de-4d18-8ef7-055603805ba6",
|
||||
"active": true,
|
||||
"on_join_journey": false,
|
||||
"placeholder": false
|
||||
},
|
||||
"relationships": {
|
||||
"user": {
|
||||
"data": {
|
||||
"id": "106676",
|
||||
"type": "customer_user"
|
||||
}
|
||||
},
|
||||
"sign_up": {
|
||||
"data": {
|
||||
"id": "877039",
|
||||
"type": "customer_sign_up"
|
||||
}
|
||||
},
|
||||
"main_customer": {
|
||||
"data": {
|
||||
"id": "108335",
|
||||
"type": "customer_customer"
|
||||
}
|
||||
},
|
||||
"main_consumer": {
|
||||
"data": {
|
||||
"id": "108291",
|
||||
"type": "customer_icp_consumer"
|
||||
}
|
||||
},
|
||||
"primary_contact": {
|
||||
"data": {
|
||||
"id": "121953",
|
||||
"type": "customer_contact"
|
||||
}
|
||||
},
|
||||
"default_payment_method": {
|
||||
"data": {
|
||||
"id": "602801",
|
||||
"type": "customer_payment_method"
|
||||
}
|
||||
},
|
||||
"phone_numbers": {
|
||||
"data": [
|
||||
{
|
||||
"id": "111604",
|
||||
"type": "customer_phone_number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"payment_methods": {
|
||||
"data": [
|
||||
{
|
||||
"id": "602801",
|
||||
"type": "customer_payment_method"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"included": [
|
||||
{
|
||||
"id": "108291",
|
||||
"type": "customer_icp_consumer",
|
||||
"attributes": {
|
||||
"start_date": "2023-03-02",
|
||||
"end_date": null,
|
||||
"icp_number": "0001234567UNB12",
|
||||
"supply_node_ref": "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef8299",
|
||||
"physical_address": "123 FAKE STREET,NEWTOWN,WELLINGTON,6021"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"verb": "get",
|
||||
"type": "customer_account",
|
||||
"params": [],
|
||||
"permission": {
|
||||
"uri": "flick:customer_app:resource:account:list",
|
||||
"data_context": null
|
||||
},
|
||||
"host": "https://api.flickuat.com",
|
||||
"service": "customer",
|
||||
"path": "/accounts",
|
||||
"description": "Returns the accounts viewable by the current user",
|
||||
"respond_with_array": true
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "134800",
|
||||
"type": "customer_account",
|
||||
"attributes": {
|
||||
"account_number": "10123404",
|
||||
"billing_name": "9973debf-963f-49b0-9a73-Ba9c3400cbed@Anonymised Example",
|
||||
"billing_email": null,
|
||||
"address": "123 Fake Street, Newtown, Wellington 6021",
|
||||
"brand": "flick",
|
||||
"vulnerability_state": "none",
|
||||
"medical_dependency": false,
|
||||
"status": "active",
|
||||
"start_at": "2023-03-02T00:00:00.000+13:00",
|
||||
"end_at": null,
|
||||
"application_id": "5dfc4978-07de-4d18-8ef7-055603805ba6",
|
||||
"active": true,
|
||||
"on_join_journey": false,
|
||||
"placeholder": false
|
||||
},
|
||||
"relationships": {
|
||||
"user": {
|
||||
"data": {
|
||||
"id": "106676",
|
||||
"type": "customer_user"
|
||||
}
|
||||
},
|
||||
"sign_up": {
|
||||
"data": {
|
||||
"id": "877039",
|
||||
"type": "customer_sign_up"
|
||||
}
|
||||
},
|
||||
"main_customer": {
|
||||
"data": {
|
||||
"id": "108335",
|
||||
"type": "customer_customer"
|
||||
}
|
||||
},
|
||||
"main_consumer": {
|
||||
"data": {
|
||||
"id": "108291",
|
||||
"type": "customer_icp_consumer"
|
||||
}
|
||||
},
|
||||
"primary_contact": {
|
||||
"data": {
|
||||
"id": "121953",
|
||||
"type": "customer_contact"
|
||||
}
|
||||
},
|
||||
"default_payment_method": {
|
||||
"data": {
|
||||
"id": "602801",
|
||||
"type": "customer_payment_method"
|
||||
}
|
||||
},
|
||||
"phone_numbers": {
|
||||
"data": [
|
||||
{
|
||||
"id": "111604",
|
||||
"type": "customer_phone_number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"payment_methods": {
|
||||
"data": [
|
||||
{
|
||||
"id": "602801",
|
||||
"type": "customer_payment_method"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "123456",
|
||||
"type": "customer_account",
|
||||
"attributes": {
|
||||
"account_number": "123123123",
|
||||
"billing_name": "9973debf-963f-49b0-9a73-Ba9c3400cbed@Anonymised Example",
|
||||
"billing_email": null,
|
||||
"address": "456 Fake Street, Newtown, Wellington 6021",
|
||||
"brand": "flick",
|
||||
"vulnerability_state": "none",
|
||||
"medical_dependency": false,
|
||||
"status": "active",
|
||||
"start_at": "2023-03-02T00:00:00.000+13:00",
|
||||
"end_at": null,
|
||||
"application_id": "5dfc4978-07de-4d18-8ef7-055603805ba6",
|
||||
"active": true,
|
||||
"on_join_journey": false,
|
||||
"placeholder": false
|
||||
},
|
||||
"relationships": {
|
||||
"main_consumer": {
|
||||
"data": {
|
||||
"id": "11223344",
|
||||
"type": "customer_icp_consumer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"included": [
|
||||
{
|
||||
"id": "108291",
|
||||
"type": "customer_icp_consumer",
|
||||
"attributes": {
|
||||
"start_date": "2023-03-02",
|
||||
"end_date": null,
|
||||
"icp_number": "0001234567UNB12",
|
||||
"supply_node_ref": "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef8299",
|
||||
"physical_address": "123 FAKE STREET,NEWTOWN,WELLINGTON,6021"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "11223344",
|
||||
"type": "customer_icp_consumer",
|
||||
"attributes": {
|
||||
"start_date": "2023-03-02",
|
||||
"end_date": null,
|
||||
"icp_number": "9991234567UNB12",
|
||||
"supply_node_ref": "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef1234",
|
||||
"physical_address": "456 FAKE STREET,NEWTOWN,WELLINGTON,6021"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"verb": "get",
|
||||
"type": "customer_account",
|
||||
"params": [],
|
||||
"permission": {
|
||||
"uri": "flick:customer_app:resource:account:list",
|
||||
"data_context": null
|
||||
},
|
||||
"host": "https://api.flickuat.com",
|
||||
"service": "customer",
|
||||
"path": "/accounts",
|
||||
"description": "Returns the accounts viewable by the current user",
|
||||
"respond_with_array": true
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
|
||||
"type": "rating_rated_period",
|
||||
"attributes": {
|
||||
"start_at": "2025-02-09T05:30:00.000Z",
|
||||
"end_at": "2025-02-09T05:59:59.000Z",
|
||||
"status": "final",
|
||||
"cost": "0.20011",
|
||||
"import_cost": "0.20011",
|
||||
"export_cost": null,
|
||||
"cost_unit": "NZD",
|
||||
"quantity": "1.0",
|
||||
"import_quantity": "1.0",
|
||||
"export_quantity": null,
|
||||
"quantity_unit": "kwh",
|
||||
"renewable_quantity": null,
|
||||
"generation_price_contract": null
|
||||
},
|
||||
"relationships": {
|
||||
"components": {
|
||||
"data": [
|
||||
{
|
||||
"id": "213507464_1_kwh_generation_UN_24_default_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
|
||||
"type": "rating_component"
|
||||
},
|
||||
{
|
||||
"id": "213507464_1_kwh_network_UN_24_offpeak_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
|
||||
"type": "rating_component"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"included": [
|
||||
{
|
||||
"id": "213507464_1_kwh_generation_UN_24_default_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
|
||||
"type": "rating_component",
|
||||
"attributes": {
|
||||
"charge_method": "kwh",
|
||||
"charge_setter": "generation",
|
||||
"value": "0.20011",
|
||||
"quantity": "1.0",
|
||||
"unit_code": "NZD",
|
||||
"charge_per": "kwh",
|
||||
"flow_direction": "import",
|
||||
"content_code": "UN",
|
||||
"hours_of_availability": 24,
|
||||
"channel_number": 1,
|
||||
"meter_serial_number": "213507464",
|
||||
"price_name": "default",
|
||||
"applicable_periods": [],
|
||||
"single_unit_price": "0.20011",
|
||||
"billable": true,
|
||||
"renewable_quantity": null,
|
||||
"generation_price_contract": "FLICK_FLAT_2024_04_01_midpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "213507464_1_kwh_network_UN_24_offpeak_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
|
||||
"type": "rating_component",
|
||||
"attributes": {
|
||||
"charge_method": "kwh",
|
||||
"charge_setter": "network",
|
||||
"value": "0.0406",
|
||||
"quantity": "1.0",
|
||||
"unit_code": "NZD",
|
||||
"charge_per": "kwh",
|
||||
"flow_direction": "import",
|
||||
"content_code": "UN",
|
||||
"hours_of_availability": 24,
|
||||
"channel_number": 1,
|
||||
"meter_serial_number": "213507464",
|
||||
"price_name": "offpeak",
|
||||
"applicable_periods": [],
|
||||
"single_unit_price": "0.0406",
|
||||
"billable": false,
|
||||
"renewable_quantity": null,
|
||||
"generation_price_contract": "FLICK_FLAT_2024_04_01_midpoint"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"verb": "get",
|
||||
"type": "rating_rated_period",
|
||||
"params": [
|
||||
{
|
||||
"name": "supply_node_ref",
|
||||
"type": "String",
|
||||
"description": "The supply node to rate",
|
||||
"example": "/network/nz/supply_nodes/bccd6f52-448b-4edf-a0c1-459ee67d215b",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "as_at",
|
||||
"type": "DateTime",
|
||||
"description": "The time to rate the supply node at; defaults to the current time",
|
||||
"example": "2023-04-01T15:20:15-07:00",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"permission": {
|
||||
"uri": "flick:rating:resource:rated_period:show",
|
||||
"data_context": "supply_node"
|
||||
},
|
||||
"host": "https://api.flickuat.com",
|
||||
"service": "rating",
|
||||
"path": "/rated_period",
|
||||
"description": "Fetch a rated period for a supply node in a specific point in time",
|
||||
"respond_with_array": false
|
||||
}
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
"""Test the Flick Electric config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pyflick.authentication import AuthException
|
||||
from pyflick.types import APIException
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.flick_electric.const import (
|
||||
CONF_ACCOUNT_ID,
|
||||
CONF_SUPPLY_NODE_REF,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import CONF, setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
# From test fixtures
|
||||
ACCOUNT_NAME_1 = "123 Fake Street, Newtown, Wellington 6021"
|
||||
ACCOUNT_NAME_2 = "456 Fake Street, Newtown, Wellington 6021"
|
||||
ACCOUNT_ID_2 = "123456"
|
||||
SUPPLY_NODE_REF_2 = "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef1234"
|
||||
|
||||
|
||||
async def _flow_submit(hass: HomeAssistant) -> ConfigFlowResult:
|
||||
return await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={
|
||||
CONF_USERNAME: CONF[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONF[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant, mock_flick_client: AsyncMock) -> None:
|
||||
"""Test we get the form with only one, with no account picker."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: CONF[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONF[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == ACCOUNT_NAME_1
|
||||
assert result2["data"] == CONF
|
||||
assert result2["result"].unique_id == CONF[CONF_ACCOUNT_ID]
|
||||
|
||||
|
||||
async def test_form_multi_account(
|
||||
hass: HomeAssistant, mock_flick_client_multiple: AsyncMock
|
||||
) -> None:
|
||||
"""Test the form when multiple accounts are available."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: CONF[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONF[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "select_account"
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{CONF_ACCOUNT_ID: ACCOUNT_ID_2},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == ACCOUNT_NAME_2
|
||||
assert result3["data"] == {
|
||||
**CONF,
|
||||
CONF_SUPPLY_NODE_REF: SUPPLY_NODE_REF_2,
|
||||
CONF_ACCOUNT_ID: ACCOUNT_ID_2,
|
||||
}
|
||||
assert result3["result"].unique_id == ACCOUNT_ID_2
|
||||
|
||||
|
||||
async def test_reauth_token(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_flick_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow when username/password is wrong."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
|
||||
side_effect=AuthException,
|
||||
):
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: CONF[CONF_USERNAME], CONF_PASSWORD: CONF[CONF_PASSWORD]},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
async def test_form_reauth_migrate(
|
||||
hass: HomeAssistant,
|
||||
mock_old_config_entry: MockConfigEntry,
|
||||
mock_flick_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow for v1 with single account."""
|
||||
mock_old_config_entry.add_to_hass(hass)
|
||||
result = await mock_old_config_entry.start_reauth_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_old_config_entry.version == 2
|
||||
assert mock_old_config_entry.unique_id == CONF[CONF_ACCOUNT_ID]
|
||||
assert mock_old_config_entry.data == CONF
|
||||
|
||||
|
||||
async def test_form_reauth_migrate_multi_account(
|
||||
hass: HomeAssistant,
|
||||
mock_old_config_entry: MockConfigEntry,
|
||||
mock_flick_client_multiple: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the form when multiple accounts are available."""
|
||||
mock_old_config_entry.add_to_hass(hass)
|
||||
result = await mock_old_config_entry.start_reauth_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "select_account"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ACCOUNT_ID: CONF[CONF_ACCOUNT_ID]},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
||||
assert mock_old_config_entry.version == 2
|
||||
assert mock_old_config_entry.unique_id == CONF[CONF_ACCOUNT_ID]
|
||||
assert mock_old_config_entry.data == CONF
|
||||
|
||||
|
||||
async def test_form_duplicate_account(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_flick_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test uniqueness for account_id."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
result = await _flow_submit(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
with patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
|
||||
side_effect=AuthException,
|
||||
):
|
||||
result = await _flow_submit(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
with patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
|
||||
side_effect=TimeoutError,
|
||||
):
|
||||
result = await _flow_submit(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_generic_exception(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
with patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result = await _flow_submit(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_select_account_cannot_connect(
|
||||
hass: HomeAssistant, mock_flick_client_multiple: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle connection errors for select account."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch.object(
|
||||
mock_flick_client_multiple,
|
||||
"getPricing",
|
||||
side_effect=APIException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: CONF[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONF[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "select_account"
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{CONF_ACCOUNT_ID: CONF[CONF_ACCOUNT_ID]},
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["step_id"] == "select_account"
|
||||
assert result3["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_select_account_invalid_auth(
|
||||
hass: HomeAssistant, mock_flick_client_multiple: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle auth errors for select account."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: CONF[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONF[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "select_account"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
|
||||
side_effect=AuthException,
|
||||
),
|
||||
patch.object(
|
||||
mock_flick_client_multiple,
|
||||
"getPricing",
|
||||
side_effect=AuthException,
|
||||
),
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{CONF_ACCOUNT_ID: CONF[CONF_ACCOUNT_ID]},
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.ABORT
|
||||
assert result3["reason"] == "no_permissions"
|
||||
|
||||
|
||||
async def test_form_select_account_failed_to_connect(
|
||||
hass: HomeAssistant, mock_flick_client_multiple: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle connection errors for select account."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: CONF[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONF[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "select_account"
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
mock_flick_client_multiple,
|
||||
"getCustomerAccounts",
|
||||
side_effect=APIException,
|
||||
),
|
||||
patch.object(
|
||||
mock_flick_client_multiple,
|
||||
"getPricing",
|
||||
side_effect=APIException,
|
||||
),
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{CONF_ACCOUNT_ID: CONF[CONF_ACCOUNT_ID]},
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{CONF_ACCOUNT_ID: ACCOUNT_ID_2},
|
||||
)
|
||||
|
||||
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result4["title"] == ACCOUNT_NAME_2
|
||||
assert result4["data"] == {
|
||||
**CONF,
|
||||
CONF_SUPPLY_NODE_REF: SUPPLY_NODE_REF_2,
|
||||
CONF_ACCOUNT_ID: ACCOUNT_ID_2,
|
||||
}
|
||||
assert result4["result"].unique_id == ACCOUNT_ID_2
|
||||
|
||||
|
||||
async def test_form_select_account_no_accounts(
|
||||
hass: HomeAssistant, mock_flick_client: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle connection errors for select account."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch.object(
|
||||
mock_flick_client,
|
||||
"getCustomerAccounts",
|
||||
return_value=[
|
||||
{
|
||||
"id": "1234",
|
||||
"status": "closed",
|
||||
"address": "123 Fake St",
|
||||
"main_consumer": {"supply_node_ref": "123"},
|
||||
},
|
||||
],
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: CONF[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONF[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "no_accounts"
|
||||
@@ -1,154 +0,0 @@
|
||||
"""Test the Flick Electric config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import jwt
|
||||
from pyflick.types import APIException, AuthException
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.flick_electric import CONF_ID_TOKEN, HassFlickAuth
|
||||
from homeassistant.components.flick_electric.const import (
|
||||
CONF_ACCOUNT_ID,
|
||||
CONF_TOKEN_EXPIRY,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import CONF, setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
NEW_TOKEN = jwt.encode(
|
||||
{"exp": dt_util.now().timestamp() + 86400}, "secret", algorithm="HS256"
|
||||
)
|
||||
EXISTING_TOKEN = jwt.encode(
|
||||
{"exp": dt_util.now().timestamp() + 3600}, "secret", algorithm="HS256"
|
||||
)
|
||||
EXPIRED_TOKEN = jwt.encode(
|
||||
{"exp": dt_util.now().timestamp() - 3600}, "secret", algorithm="HS256"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "config_entry_state"),
|
||||
[
|
||||
(AuthException, ConfigEntryState.SETUP_ERROR),
|
||||
(APIException, ConfigEntryState.SETUP_RETRY),
|
||||
],
|
||||
)
|
||||
async def test_init_auth_failure_triggers_auth(
|
||||
hass: HomeAssistant,
|
||||
mock_flick_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
config_entry_state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test integration handles initialisation errors."""
|
||||
with patch.object(mock_flick_client, "getPricing", side_effect=exception):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state == config_entry_state
|
||||
|
||||
|
||||
async def test_init_migration_single_account(
|
||||
hass: HomeAssistant,
|
||||
mock_old_config_entry: MockConfigEntry,
|
||||
mock_flick_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test migration with single account."""
|
||||
await setup_integration(hass, mock_old_config_entry)
|
||||
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
assert mock_old_config_entry.state is ConfigEntryState.LOADED
|
||||
assert mock_old_config_entry.version == 2
|
||||
assert mock_old_config_entry.unique_id == CONF[CONF_ACCOUNT_ID]
|
||||
assert mock_old_config_entry.data == CONF
|
||||
|
||||
|
||||
async def test_init_migration_multi_account_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_old_config_entry: MockConfigEntry,
|
||||
mock_flick_client_multiple: AsyncMock,
|
||||
) -> None:
|
||||
"""Test migration triggers reauth with multiple accounts."""
|
||||
await setup_integration(hass, mock_old_config_entry)
|
||||
|
||||
assert mock_old_config_entry.state is ConfigEntryState.MIGRATION_ERROR
|
||||
|
||||
# Ensure reauth flow is triggered
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.config_entries.flow.async_progress()) == 1
|
||||
|
||||
|
||||
async def test_fetch_fresh_token(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_flick_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test fetching a fresh token."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.get_new_token",
|
||||
return_value={CONF_ID_TOKEN: NEW_TOKEN},
|
||||
) as mock_get_new_token:
|
||||
auth = HassFlickAuth(hass, mock_config_entry)
|
||||
|
||||
assert await auth.async_get_access_token() == NEW_TOKEN
|
||||
assert mock_get_new_token.call_count == 1
|
||||
|
||||
|
||||
async def test_reuse_token(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_flick_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reusing entry token."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry,
|
||||
data={
|
||||
**mock_config_entry.data,
|
||||
CONF_ACCESS_TOKEN: {CONF_ID_TOKEN: EXISTING_TOKEN},
|
||||
CONF_TOKEN_EXPIRY: dt_util.now().timestamp() + 3600,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.get_new_token",
|
||||
return_value={CONF_ID_TOKEN: NEW_TOKEN},
|
||||
) as mock_get_new_token:
|
||||
auth = HassFlickAuth(hass, mock_config_entry)
|
||||
|
||||
assert await auth.async_get_access_token() == EXISTING_TOKEN
|
||||
assert mock_get_new_token.call_count == 0
|
||||
|
||||
|
||||
async def test_fetch_expired_token(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_flick_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test fetching token when existing token is expired."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry,
|
||||
data={
|
||||
**mock_config_entry.data,
|
||||
CONF_ACCESS_TOKEN: {CONF_ID_TOKEN: EXPIRED_TOKEN},
|
||||
CONF_TOKEN_EXPIRY: dt_util.now().timestamp() - 3600,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.get_new_token",
|
||||
return_value={CONF_ID_TOKEN: NEW_TOKEN},
|
||||
) as mock_get_new_token:
|
||||
auth = HassFlickAuth(hass, mock_config_entry)
|
||||
|
||||
assert await auth.async_get_access_token() == NEW_TOKEN
|
||||
assert mock_get_new_token.call_count == 1
|
||||
Reference in New Issue
Block a user