1
0
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:
Brynley McDonald
2025-10-30 23:23:37 +13:00
committed by GitHub
parent 06dbfe52d0
commit 99eb48c27f
21 changed files with 0 additions and 1644 deletions
Generated
-2
View File
@@ -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"
}
}
}
}
-1
View File
@@ -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",
-3
View File
@@ -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
-3
View File
@@ -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
-2
View File
@@ -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",
},
],
}
)
-105
View File
@@ -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