diff --git a/CODEOWNERS b/CODEOWNERS index 0974b47691d..6e7fcb5a2e5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py deleted file mode 100644 index ad772c06b2e..00000000000 --- a/homeassistant/components/flick_electric/__init__.py +++ /dev/null @@ -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] diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py deleted file mode 100644 index b6b7327fcb0..00000000000 --- a/homeassistant/components/flick_electric/config_flow.py +++ /dev/null @@ -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.""" diff --git a/homeassistant/components/flick_electric/const.py b/homeassistant/components/flick_electric/const.py deleted file mode 100644 index 0f94aa909b7..00000000000 --- a/homeassistant/components/flick_electric/const.py +++ /dev/null @@ -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"] diff --git a/homeassistant/components/flick_electric/coordinator.py b/homeassistant/components/flick_electric/coordinator.py deleted file mode 100644 index 114b364635f..00000000000 --- a/homeassistant/components/flick_electric/coordinator.py +++ /dev/null @@ -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 diff --git a/homeassistant/components/flick_electric/manifest.json b/homeassistant/components/flick_electric/manifest.json deleted file mode 100644 index 3096590f47a..00000000000 --- a/homeassistant/components/flick_electric/manifest.json +++ /dev/null @@ -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"] -} diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py deleted file mode 100644 index 636d12525ad..00000000000 --- a/homeassistant/components/flick_electric/sensor.py +++ /dev/null @@ -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, - } diff --git a/homeassistant/components/flick_electric/strings.json b/homeassistant/components/flick_electric/strings.json deleted file mode 100644 index 5f05883d679..00000000000 --- a/homeassistant/components/flick_electric/strings.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 67c1d0009d9..d6699271266 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -210,7 +210,6 @@ FLOWS = { "fivem", "fjaraskupan", "flexit_bacnet", - "flick_electric", "flipr", "flo", "flume", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 41485289c85..0845af438b8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -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", diff --git a/requirements_all.txt b/requirements_all.txt index b14139066e2..55ede75a18c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae7eb8da0fc..e2677d2af24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 0c1c59338a2..4019a0881d5 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -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", diff --git a/tests/components/analytics_insights/fixtures/current_data.json b/tests/components/analytics_insights/fixtures/current_data.json index d221ccda301..9c223f5f822 100644 --- a/tests/components/analytics_insights/fixtures/current_data.json +++ b/tests/components/analytics_insights/fixtures/current_data.json @@ -928,7 +928,6 @@ "oncue": 73, "tailwind": 44, "dunehd": 102, - "flick_electric": 42, "home_plus_control": 85, "weishaupt_wcm_com": 1, "sentry": 63, diff --git a/tests/components/flick_electric/__init__.py b/tests/components/flick_electric/__init__.py deleted file mode 100644 index 3632ce204aa..00000000000 --- a/tests/components/flick_electric/__init__.py +++ /dev/null @@ -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", - }, - ], - } - ) diff --git a/tests/components/flick_electric/conftest.py b/tests/components/flick_electric/conftest.py deleted file mode 100644 index 2abfafab55d..00000000000 --- a/tests/components/flick_electric/conftest.py +++ /dev/null @@ -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 diff --git a/tests/components/flick_electric/fixtures/accounts.json b/tests/components/flick_electric/fixtures/accounts.json deleted file mode 100644 index a1c08ecd7c0..00000000000 --- a/tests/components/flick_electric/fixtures/accounts.json +++ /dev/null @@ -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 - } -} diff --git a/tests/components/flick_electric/fixtures/accounts_multi.json b/tests/components/flick_electric/fixtures/accounts_multi.json deleted file mode 100644 index 7c1f3fba2ef..00000000000 --- a/tests/components/flick_electric/fixtures/accounts_multi.json +++ /dev/null @@ -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 - } -} diff --git a/tests/components/flick_electric/fixtures/rated_period.json b/tests/components/flick_electric/fixtures/rated_period.json deleted file mode 100644 index 8e6ce96a9b7..00000000000 --- a/tests/components/flick_electric/fixtures/rated_period.json +++ /dev/null @@ -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 - } -} diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py deleted file mode 100644 index c14303278a3..00000000000 --- a/tests/components/flick_electric/test_config_flow.py +++ /dev/null @@ -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" diff --git a/tests/components/flick_electric/test_init.py b/tests/components/flick_electric/test_init.py deleted file mode 100644 index d420a78ccfc..00000000000 --- a/tests/components/flick_electric/test_init.py +++ /dev/null @@ -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