From 621c66a2147ee4fff784ee75767fa784681aca76 Mon Sep 17 00:00:00 2001 From: Myles Eftos Date: Wed, 20 Nov 2024 21:27:24 +1100 Subject: [PATCH] Update Amberelectric to use amberelectric version 2.0.12 (#125701) * Add price descriptor attribute to price sensors * Adding price descriptor sensor * Use correct number of sensors in spike sensor tests * Add tests for normalize_descriptor * Removing debug message * Removing price_descriptor attribute from the current sensor * Refactoring everything to use the new API * Use SiteStatus object, fix some typnig issues * fixing test * Adding predicted price to attributes * Fix advanced price in forecast * Testing advanced forecasts * WIP: Adding advanced forecast sensor. need to add attributes, and tests * Add advanced price attributes * Adding forecasts to the advanced price sensor * Appending forecasts corectly * Appending forecasts correctly. Again * Removing sensor for the moment. Will do in another PR * Fix failing test that had the wrong sign * Adding test to improve coverage on config_flow test * Bumping amberelectric dependency to version 2 * Remove advanced code from helpers * Use f-strings * Bumping to version 2.0.1 * Bumping amberelectric to version 2.0.2 * Bumping amberelectric to version 2.0.2 * Bumping verion amberelectric.py to 2.0.3. Using correct enums * Bumping amberelectric.py version to 2.0.4 * Bump version to 2.0.5 * Fix formatting * fixing mocks to include interval_length * Bumping to 2.0.6 * Bumping to 2.0.7 * Bumping to 2.0.8 * Bumping to 2.0.9 * Bumping version 2.0.12 --- .../components/amberelectric/__init__.py | 8 +- .../components/amberelectric/config_flow.py | 19 +- .../components/amberelectric/coordinator.py | 33 +-- .../components/amberelectric/manifest.json | 2 +- .../components/amberelectric/sensor.py | 20 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/amberelectric/helpers.py | 121 ++++++----- .../amberelectric/test_binary_sensor.py | 40 ++-- .../amberelectric/test_config_flow.py | 142 +++++++++---- .../amberelectric/test_coordinator.py | 194 ++++++++++-------- tests/components/amberelectric/test_sensor.py | 23 ++- 12 files changed, 352 insertions(+), 254 deletions(-) diff --git a/homeassistant/components/amberelectric/__init__.py b/homeassistant/components/amberelectric/__init__.py index cd44886c9ef..29d8f166f2a 100644 --- a/homeassistant/components/amberelectric/__init__.py +++ b/homeassistant/components/amberelectric/__init__.py @@ -1,7 +1,6 @@ """Support for Amber Electric.""" -from amberelectric import Configuration -from amberelectric.api import amber_api +import amberelectric from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN @@ -15,8 +14,9 @@ type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool: """Set up Amber Electric from a config entry.""" - configuration = Configuration(access_token=entry.data[CONF_API_TOKEN]) - api_instance = amber_api.AmberApi.create(configuration) + configuration = amberelectric.Configuration(access_token=entry.data[CONF_API_TOKEN]) + api_client = amberelectric.ApiClient(configuration) + api_instance = amberelectric.AmberApi(api_client) site_id = entry.data[CONF_SITE_ID] coordinator = AmberUpdateCoordinator(hass, api_instance, site_id) diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index a94700c27d1..c25258e2e33 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -3,8 +3,8 @@ from __future__ import annotations import amberelectric -from amberelectric.api import amber_api -from amberelectric.model.site import Site, SiteStatus +from amberelectric.models.site import Site +from amberelectric.models.site_status import SiteStatus import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -23,11 +23,15 @@ API_URL = "https://app.amber.com.au/developers" def generate_site_selector_name(site: Site) -> str: """Generate the name to show in the site drop down in the configuration flow.""" + # For some reason the generated API key returns this as any, not a string. Thanks pydantic + nmi = str(site.nmi) if site.status == SiteStatus.CLOSED: - return site.nmi + " (Closed: " + site.closed_on.isoformat() + ")" # type: ignore[no-any-return] + if site.closed_on is None: + return f"{nmi} (Closed)" + return f"{nmi} (Closed: {site.closed_on.isoformat()})" if site.status == SiteStatus.PENDING: - return site.nmi + " (Pending)" # type: ignore[no-any-return] - return site.nmi # type: ignore[no-any-return] + return f"{nmi} (Pending)" + return nmi def filter_sites(sites: list[Site]) -> list[Site]: @@ -35,7 +39,7 @@ def filter_sites(sites: list[Site]) -> list[Site]: filtered: list[Site] = [] filtered_nmi: set[str] = set() - for site in sorted(sites, key=lambda site: site.status.value): + for site in sorted(sites, key=lambda site: site.status): if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi: filtered.append(site) filtered_nmi.add(site.nmi) @@ -56,7 +60,8 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN): def _fetch_sites(self, token: str) -> list[Site] | None: configuration = amberelectric.Configuration(access_token=token) - api: amber_api.AmberApi = amber_api.AmberApi.create(configuration) + api_client = amberelectric.ApiClient(configuration) + api = amberelectric.AmberApi(api_client) try: sites: list[Site] = filter_sites(api.get_sites()) diff --git a/homeassistant/components/amberelectric/coordinator.py b/homeassistant/components/amberelectric/coordinator.py index a95aa3fa529..57028e07d21 100644 --- a/homeassistant/components/amberelectric/coordinator.py +++ b/homeassistant/components/amberelectric/coordinator.py @@ -5,13 +5,13 @@ from __future__ import annotations from datetime import timedelta from typing import Any -from amberelectric import ApiException -from amberelectric.api import amber_api -from amberelectric.model.actual_interval import ActualInterval -from amberelectric.model.channel import ChannelType -from amberelectric.model.current_interval import CurrentInterval -from amberelectric.model.forecast_interval import ForecastInterval -from amberelectric.model.interval import Descriptor +import amberelectric +from amberelectric.models.actual_interval import ActualInterval +from amberelectric.models.channel import ChannelType +from amberelectric.models.current_interval import CurrentInterval +from amberelectric.models.forecast_interval import ForecastInterval +from amberelectric.models.price_descriptor import PriceDescriptor +from amberelectric.rest import ApiException from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -31,22 +31,22 @@ def is_forecast(interval: ActualInterval | CurrentInterval | ForecastInterval) - def is_general(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool: """Return true if the supplied interval is on the general channel.""" - return interval.channel_type == ChannelType.GENERAL # type: ignore[no-any-return] + return interval.channel_type == ChannelType.GENERAL def is_controlled_load( interval: ActualInterval | CurrentInterval | ForecastInterval, ) -> bool: """Return true if the supplied interval is on the controlled load channel.""" - return interval.channel_type == ChannelType.CONTROLLED_LOAD # type: ignore[no-any-return] + return interval.channel_type == ChannelType.CONTROLLEDLOAD def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool: """Return true if the supplied interval is on the feed in channel.""" - return interval.channel_type == ChannelType.FEED_IN # type: ignore[no-any-return] + return interval.channel_type == ChannelType.FEEDIN -def normalize_descriptor(descriptor: Descriptor) -> str | None: +def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None: """Return the snake case versions of descriptor names. Returns None if the name is not recognized.""" if descriptor is None: return None @@ -71,7 +71,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): """AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read.""" def __init__( - self, hass: HomeAssistant, api: amber_api.AmberApi, site_id: str + self, hass: HomeAssistant, api: amberelectric.AmberApi, site_id: str ) -> None: """Initialise the data service.""" super().__init__( @@ -93,12 +93,13 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): "grid": {}, } try: - data = self._api.get_current_price(self.site_id, next=48) + data = self._api.get_current_prices(self.site_id, next=48) + intervals = [interval.actual_instance for interval in data] except ApiException as api_exception: raise UpdateFailed("Missing price data, skipping update") from api_exception - current = [interval for interval in data if is_current(interval)] - forecasts = [interval for interval in data if is_forecast(interval)] + current = [interval for interval in intervals if is_current(interval)] + forecasts = [interval for interval in intervals if is_forecast(interval)] general = [interval for interval in current if is_general(interval)] if len(general) == 0: @@ -137,7 +138,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): interval for interval in forecasts if is_feed_in(interval) ] - LOGGER.debug("Fetched new Amber data: %s", data) + LOGGER.debug("Fetched new Amber data: %s", intervals) return result async def _async_update_data(self) -> dict[str, Any]: diff --git a/homeassistant/components/amberelectric/manifest.json b/homeassistant/components/amberelectric/manifest.json index 51be42cfa68..401eb1629a1 100644 --- a/homeassistant/components/amberelectric/manifest.json +++ b/homeassistant/components/amberelectric/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/amberelectric", "iot_class": "cloud_polling", "loggers": ["amberelectric"], - "requirements": ["amberelectric==1.1.1"] + "requirements": ["amberelectric==2.0.12"] } diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 52c0c42e7bc..cdf40e5804d 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -8,9 +8,9 @@ from __future__ import annotations from typing import Any -from amberelectric.model.channel import ChannelType -from amberelectric.model.current_interval import CurrentInterval -from amberelectric.model.forecast_interval import ForecastInterval +from amberelectric.models.channel import ChannelType +from amberelectric.models.current_interval import CurrentInterval +from amberelectric.models.forecast_interval import ForecastInterval from homeassistant.components.sensor import ( SensorEntity, @@ -52,7 +52,7 @@ class AmberSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity): self, coordinator: AmberUpdateCoordinator, description: SensorEntityDescription, - channel_type: ChannelType, + channel_type: str, ) -> None: """Initialize the Sensor.""" super().__init__(coordinator) @@ -73,7 +73,7 @@ class AmberPriceSensor(AmberSensor): """Return the current price in $/kWh.""" interval = self.coordinator.data[self.entity_description.key][self.channel_type] - if interval.channel_type == ChannelType.FEED_IN: + if interval.channel_type == ChannelType.FEEDIN: return format_cents_to_dollars(interval.per_kwh) * -1 return format_cents_to_dollars(interval.per_kwh) @@ -87,9 +87,9 @@ class AmberPriceSensor(AmberSensor): return data data["duration"] = interval.duration - data["date"] = interval.date.isoformat() + data["date"] = interval.var_date.isoformat() data["per_kwh"] = format_cents_to_dollars(interval.per_kwh) - if interval.channel_type == ChannelType.FEED_IN: + if interval.channel_type == ChannelType.FEEDIN: data["per_kwh"] = data["per_kwh"] * -1 data["nem_date"] = interval.nem_time.isoformat() data["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh) @@ -120,7 +120,7 @@ class AmberForecastSensor(AmberSensor): return None interval = intervals[0] - if interval.channel_type == ChannelType.FEED_IN: + if interval.channel_type == ChannelType.FEEDIN: return format_cents_to_dollars(interval.per_kwh) * -1 return format_cents_to_dollars(interval.per_kwh) @@ -142,10 +142,10 @@ class AmberForecastSensor(AmberSensor): for interval in intervals: datum = {} datum["duration"] = interval.duration - datum["date"] = interval.date.isoformat() + datum["date"] = interval.var_date.isoformat() datum["nem_date"] = interval.nem_time.isoformat() datum["per_kwh"] = format_cents_to_dollars(interval.per_kwh) - if interval.channel_type == ChannelType.FEED_IN: + if interval.channel_type == ChannelType.FEEDIN: datum["per_kwh"] = datum["per_kwh"] * -1 datum["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh) datum["start_time"] = interval.start_time.isoformat() diff --git a/requirements_all.txt b/requirements_all.txt index 6e5ec19d253..fe4786562b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -447,7 +447,7 @@ airtouch5py==0.2.10 alpha-vantage==2.3.1 # homeassistant.components.amberelectric -amberelectric==1.1.1 +amberelectric==2.0.12 # homeassistant.components.amcrest amcrest==1.9.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f70132e059..063eedd7a95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ airtouch4pyapi==1.0.5 airtouch5py==0.2.10 # homeassistant.components.amberelectric -amberelectric==1.1.1 +amberelectric==2.0.12 # homeassistant.components.androidtv androidtv[async]==0.0.73 diff --git a/tests/components/amberelectric/helpers.py b/tests/components/amberelectric/helpers.py index 2bc65fdd558..971f3690a0d 100644 --- a/tests/components/amberelectric/helpers.py +++ b/tests/components/amberelectric/helpers.py @@ -2,73 +2,82 @@ from datetime import datetime, timedelta -from amberelectric.model.actual_interval import ActualInterval -from amberelectric.model.channel import ChannelType -from amberelectric.model.current_interval import CurrentInterval -from amberelectric.model.forecast_interval import ForecastInterval -from amberelectric.model.interval import Descriptor, SpikeStatus +from amberelectric.models.actual_interval import ActualInterval +from amberelectric.models.channel import ChannelType +from amberelectric.models.current_interval import CurrentInterval +from amberelectric.models.forecast_interval import ForecastInterval +from amberelectric.models.interval import Interval +from amberelectric.models.price_descriptor import PriceDescriptor +from amberelectric.models.spike_status import SpikeStatus from dateutil import parser -def generate_actual_interval( - channel_type: ChannelType, end_time: datetime -) -> ActualInterval: +def generate_actual_interval(channel_type: ChannelType, end_time: datetime) -> Interval: """Generate a mock actual interval.""" start_time = end_time - timedelta(minutes=30) - return ActualInterval( - duration=30, - spot_per_kwh=1.0, - per_kwh=8.0, - date=start_time.date(), - nem_time=end_time, - start_time=start_time, - end_time=end_time, - renewables=50, - channel_type=channel_type.value, - spike_status=SpikeStatus.NO_SPIKE.value, - descriptor=Descriptor.LOW.value, + return Interval( + ActualInterval( + type="ActualInterval", + duration=30, + spot_per_kwh=1.0, + per_kwh=8.0, + date=start_time.date(), + nem_time=end_time, + start_time=start_time, + end_time=end_time, + renewables=50, + channel_type=channel_type, + spike_status=SpikeStatus.NONE, + descriptor=PriceDescriptor.LOW, + ) ) def generate_current_interval( channel_type: ChannelType, end_time: datetime -) -> CurrentInterval: +) -> Interval: """Generate a mock current price.""" start_time = end_time - timedelta(minutes=30) - return CurrentInterval( - duration=30, - spot_per_kwh=1.0, - per_kwh=8.0, - date=start_time.date(), - nem_time=end_time, - start_time=start_time, - end_time=end_time, - renewables=50.6, - channel_type=channel_type.value, - spike_status=SpikeStatus.NO_SPIKE.value, - descriptor=Descriptor.EXTREMELY_LOW.value, - estimate=True, + return Interval( + CurrentInterval( + type="CurrentInterval", + duration=30, + spot_per_kwh=1.0, + per_kwh=8.0, + date=start_time.date(), + nem_time=end_time, + start_time=start_time, + end_time=end_time, + renewables=50.6, + channel_type=channel_type, + spike_status=SpikeStatus.NONE, + descriptor=PriceDescriptor.EXTREMELYLOW, + estimate=True, + ) ) def generate_forecast_interval( channel_type: ChannelType, end_time: datetime -) -> ForecastInterval: +) -> Interval: """Generate a mock forecast interval.""" start_time = end_time - timedelta(minutes=30) - return ForecastInterval( - duration=30, - spot_per_kwh=1.1, - per_kwh=8.8, - date=start_time.date(), - nem_time=end_time, - start_time=start_time, - end_time=end_time, - renewables=50, - channel_type=channel_type.value, - spike_status=SpikeStatus.NO_SPIKE.value, - descriptor=Descriptor.VERY_LOW.value, - estimate=True, + return Interval( + ForecastInterval( + type="ForecastInterval", + duration=30, + spot_per_kwh=1.1, + per_kwh=8.8, + date=start_time.date(), + nem_time=end_time, + start_time=start_time, + end_time=end_time, + renewables=50, + channel_type=channel_type, + spike_status=SpikeStatus.NONE, + descriptor=PriceDescriptor.VERYLOW, + estimate=True, + ) ) @@ -94,31 +103,31 @@ GENERAL_CHANNEL = [ CONTROLLED_LOAD_CHANNEL = [ generate_current_interval( - ChannelType.CONTROLLED_LOAD, parser.parse("2021-09-21T08:30:00+10:00") + ChannelType.CONTROLLEDLOAD, parser.parse("2021-09-21T08:30:00+10:00") ), generate_forecast_interval( - ChannelType.CONTROLLED_LOAD, parser.parse("2021-09-21T09:00:00+10:00") + ChannelType.CONTROLLEDLOAD, parser.parse("2021-09-21T09:00:00+10:00") ), generate_forecast_interval( - ChannelType.CONTROLLED_LOAD, parser.parse("2021-09-21T09:30:00+10:00") + ChannelType.CONTROLLEDLOAD, parser.parse("2021-09-21T09:30:00+10:00") ), generate_forecast_interval( - ChannelType.CONTROLLED_LOAD, parser.parse("2021-09-21T10:00:00+10:00") + ChannelType.CONTROLLEDLOAD, parser.parse("2021-09-21T10:00:00+10:00") ), ] FEED_IN_CHANNEL = [ generate_current_interval( - ChannelType.FEED_IN, parser.parse("2021-09-21T08:30:00+10:00") + ChannelType.FEEDIN, parser.parse("2021-09-21T08:30:00+10:00") ), generate_forecast_interval( - ChannelType.FEED_IN, parser.parse("2021-09-21T09:00:00+10:00") + ChannelType.FEEDIN, parser.parse("2021-09-21T09:00:00+10:00") ), generate_forecast_interval( - ChannelType.FEED_IN, parser.parse("2021-09-21T09:30:00+10:00") + ChannelType.FEEDIN, parser.parse("2021-09-21T09:30:00+10:00") ), generate_forecast_interval( - ChannelType.FEED_IN, parser.parse("2021-09-21T10:00:00+10:00") + ChannelType.FEEDIN, parser.parse("2021-09-21T10:00:00+10:00") ), ] diff --git a/tests/components/amberelectric/test_binary_sensor.py b/tests/components/amberelectric/test_binary_sensor.py index 2c1ee22b644..6a6ca372bc2 100644 --- a/tests/components/amberelectric/test_binary_sensor.py +++ b/tests/components/amberelectric/test_binary_sensor.py @@ -5,10 +5,10 @@ from __future__ import annotations from collections.abc import AsyncGenerator from unittest.mock import Mock, patch -from amberelectric.model.channel import ChannelType -from amberelectric.model.current_interval import CurrentInterval -from amberelectric.model.interval import SpikeStatus -from amberelectric.model.tariff_information import TariffInformation +from amberelectric.models.channel import ChannelType +from amberelectric.models.current_interval import CurrentInterval +from amberelectric.models.spike_status import SpikeStatus +from amberelectric.models.tariff_information import TariffInformation from dateutil import parser import pytest @@ -42,10 +42,10 @@ async def setup_no_spike(hass: HomeAssistant) -> AsyncGenerator[Mock]: instance = Mock() with patch( - "amberelectric.api.AmberApi.create", + "amberelectric.AmberApi", return_value=instance, ) as mock_update: - instance.get_current_price = Mock(return_value=GENERAL_CHANNEL) + instance.get_current_prices = Mock(return_value=GENERAL_CHANNEL) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() yield mock_update.return_value @@ -65,7 +65,7 @@ async def setup_potential_spike(hass: HomeAssistant) -> AsyncGenerator[Mock]: instance = Mock() with patch( - "amberelectric.api.AmberApi.create", + "amberelectric.AmberApi", return_value=instance, ) as mock_update: general_channel: list[CurrentInterval] = [ @@ -73,8 +73,8 @@ async def setup_potential_spike(hass: HomeAssistant) -> AsyncGenerator[Mock]: ChannelType.GENERAL, parser.parse("2021-09-21T08:30:00+10:00") ), ] - general_channel[0].spike_status = SpikeStatus.POTENTIAL - instance.get_current_price = Mock(return_value=general_channel) + general_channel[0].actual_instance.spike_status = SpikeStatus.POTENTIAL + instance.get_current_prices = Mock(return_value=general_channel) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() yield mock_update.return_value @@ -94,7 +94,7 @@ async def setup_spike(hass: HomeAssistant) -> AsyncGenerator[Mock]: instance = Mock() with patch( - "amberelectric.api.AmberApi.create", + "amberelectric.AmberApi", return_value=instance, ) as mock_update: general_channel: list[CurrentInterval] = [ @@ -102,8 +102,8 @@ async def setup_spike(hass: HomeAssistant) -> AsyncGenerator[Mock]: ChannelType.GENERAL, parser.parse("2021-09-21T08:30:00+10:00") ), ] - general_channel[0].spike_status = SpikeStatus.SPIKE - instance.get_current_price = Mock(return_value=general_channel) + general_channel[0].actual_instance.spike_status = SpikeStatus.SPIKE + instance.get_current_prices = Mock(return_value=general_channel) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() yield mock_update.return_value @@ -156,7 +156,7 @@ async def setup_inactive_demand_window(hass: HomeAssistant) -> AsyncGenerator[Mo instance = Mock() with patch( - "amberelectric.api.AmberApi.create", + "amberelectric.AmberApi", return_value=instance, ) as mock_update: general_channel: list[CurrentInterval] = [ @@ -164,8 +164,10 @@ async def setup_inactive_demand_window(hass: HomeAssistant) -> AsyncGenerator[Mo ChannelType.GENERAL, parser.parse("2021-09-21T08:30:00+10:00") ), ] - general_channel[0].tariff_information = TariffInformation(demandWindow=False) - instance.get_current_price = Mock(return_value=general_channel) + general_channel[0].actual_instance.tariff_information = TariffInformation( + demandWindow=False + ) + instance.get_current_prices = Mock(return_value=general_channel) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() yield mock_update.return_value @@ -185,7 +187,7 @@ async def setup_active_demand_window(hass: HomeAssistant) -> AsyncGenerator[Mock instance = Mock() with patch( - "amberelectric.api.AmberApi.create", + "amberelectric.AmberApi", return_value=instance, ) as mock_update: general_channel: list[CurrentInterval] = [ @@ -193,8 +195,10 @@ async def setup_active_demand_window(hass: HomeAssistant) -> AsyncGenerator[Mock ChannelType.GENERAL, parser.parse("2021-09-21T08:30:00+10:00") ), ] - general_channel[0].tariff_information = TariffInformation(demandWindow=True) - instance.get_current_price = Mock(return_value=general_channel) + general_channel[0].actual_instance.tariff_information = TariffInformation( + demandWindow=True + ) + instance.get_current_prices = Mock(return_value=general_channel) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() yield mock_update.return_value diff --git a/tests/components/amberelectric/test_config_flow.py b/tests/components/amberelectric/test_config_flow.py index 030b82d3596..b394977b0e8 100644 --- a/tests/components/amberelectric/test_config_flow.py +++ b/tests/components/amberelectric/test_config_flow.py @@ -5,7 +5,8 @@ from datetime import date from unittest.mock import Mock, patch from amberelectric import ApiException -from amberelectric.model.site import Site, SiteStatus +from amberelectric.models.site import Site +from amberelectric.models.site_status import SiteStatus import pytest from homeassistant.components.amberelectric.config_flow import filter_sites @@ -28,7 +29,7 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry") def mock_invalid_key_api() -> Generator: """Return an authentication error.""" - with patch("amberelectric.api.AmberApi.create") as mock: + with patch("amberelectric.AmberApi") as mock: mock.return_value.get_sites.side_effect = ApiException(status=403) yield mock @@ -36,7 +37,7 @@ def mock_invalid_key_api() -> Generator: @pytest.fixture(name="api_error") def mock_api_error() -> Generator: """Return an authentication error.""" - with patch("amberelectric.api.AmberApi.create") as mock: + with patch("amberelectric.AmberApi") as mock: mock.return_value.get_sites.side_effect = ApiException(status=500) yield mock @@ -45,16 +46,36 @@ def mock_api_error() -> Generator: def mock_single_site_api() -> Generator: """Return a single site.""" site = Site( - "01FG0AGP818PXK0DWHXJRRT2DH", - "11111111111", - [], - "Jemena", - SiteStatus.ACTIVE, - date(2002, 1, 1), - None, + id="01FG0AGP818PXK0DWHXJRRT2DH", + nmi="11111111111", + channels=[], + network="Jemena", + status=SiteStatus.ACTIVE, + active_from=date(2002, 1, 1), + closed_on=None, + interval_length=30, ) - with patch("amberelectric.api.AmberApi.create") as mock: + with patch("amberelectric.AmberApi") as mock: + mock.return_value.get_sites.return_value = [site] + yield mock + + +@pytest.fixture(name="single_site_closed_no_close_date_api") +def single_site_closed_no_close_date_api() -> Generator: + """Return a single closed site with no closed date.""" + site = Site( + id="01FG0AGP818PXK0DWHXJRRT2DH", + nmi="11111111111", + channels=[], + network="Jemena", + status=SiteStatus.CLOSED, + active_from=None, + closed_on=None, + interval_length=30, + ) + + with patch("amberelectric.AmberApi") as mock: mock.return_value.get_sites.return_value = [site] yield mock @@ -63,16 +84,17 @@ def mock_single_site_api() -> Generator: def mock_single_site_pending_api() -> Generator: """Return a single site.""" site = Site( - "01FG0AGP818PXK0DWHXJRRT2DH", - "11111111111", - [], - "Jemena", - SiteStatus.PENDING, - None, - None, + id="01FG0AGP818PXK0DWHXJRRT2DH", + nmi="11111111111", + channels=[], + network="Jemena", + status=SiteStatus.PENDING, + active_from=None, + closed_on=None, + interval_length=30, ) - with patch("amberelectric.api.AmberApi.create") as mock: + with patch("amberelectric.AmberApi") as mock: mock.return_value.get_sites.return_value = [site] yield mock @@ -82,35 +104,38 @@ def mock_single_site_rejoin_api() -> Generator: """Return a single site.""" instance = Mock() site_1 = Site( - "01HGD9QB72HB3DWQNJ6SSCGXGV", - "11111111111", - [], - "Jemena", - SiteStatus.CLOSED, - date(2002, 1, 1), - date(2002, 6, 1), + id="01HGD9QB72HB3DWQNJ6SSCGXGV", + nmi="11111111111", + channels=[], + network="Jemena", + status=SiteStatus.CLOSED, + active_from=date(2002, 1, 1), + closed_on=date(2002, 6, 1), + interval_length=30, ) site_2 = Site( - "01FG0AGP818PXK0DWHXJRRT2DH", - "11111111111", - [], - "Jemena", - SiteStatus.ACTIVE, - date(2003, 1, 1), - None, + id="01FG0AGP818PXK0DWHXJRRT2DH", + nmi="11111111111", + channels=[], + network="Jemena", + status=SiteStatus.ACTIVE, + active_from=date(2003, 1, 1), + closed_on=None, + interval_length=30, ) site_3 = Site( - "01FG0AGP818PXK0DWHXJRRT2DH", - "11111111112", - [], - "Jemena", - SiteStatus.CLOSED, - date(2003, 1, 1), - date(2003, 6, 1), + id="01FG0AGP818PXK0DWHXJRRT2DH", + nmi="11111111112", + channels=[], + network="Jemena", + status=SiteStatus.CLOSED, + active_from=date(2003, 1, 1), + closed_on=date(2003, 6, 1), + interval_length=30, ) instance.get_sites.return_value = [site_1, site_2, site_3] - with patch("amberelectric.api.AmberApi.create", return_value=instance): + with patch("amberelectric.AmberApi", return_value=instance): yield instance @@ -120,7 +145,7 @@ def mock_no_site_api() -> Generator: instance = Mock() instance.get_sites.return_value = [] - with patch("amberelectric.api.AmberApi.create", return_value=instance): + with patch("amberelectric.AmberApi", return_value=instance): yield instance @@ -188,6 +213,39 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH" +async def test_single_closed_site_no_closed_date( + hass: HomeAssistant, single_site_closed_no_close_date_api: Mock +) -> None: + """Test single closed site with no closed date.""" + initial_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert initial_result.get("type") is FlowResultType.FORM + assert initial_result.get("step_id") == "user" + + # Test filling in API key + enter_api_key_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_API_TOKEN: API_KEY}, + ) + assert enter_api_key_result.get("type") is FlowResultType.FORM + assert enter_api_key_result.get("step_id") == "site" + + select_site_result = await hass.config_entries.flow.async_configure( + enter_api_key_result["flow_id"], + {CONF_SITE_ID: "01FG0AGP818PXK0DWHXJRRT2DH", CONF_SITE_NAME: "Home"}, + ) + + # Show available sites + assert select_site_result.get("type") is FlowResultType.CREATE_ENTRY + assert select_site_result.get("title") == "Home" + data = select_site_result.get("data") + assert data + assert data[CONF_API_TOKEN] == API_KEY + assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH" + + async def test_single_site_rejoin( hass: HomeAssistant, single_site_rejoin_api: Mock ) -> None: diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index cb3912cb5ac..0a8f5b874fa 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -7,10 +7,12 @@ from datetime import date from unittest.mock import Mock, patch from amberelectric import ApiException -from amberelectric.model.channel import Channel, ChannelType -from amberelectric.model.current_interval import CurrentInterval -from amberelectric.model.interval import Descriptor, SpikeStatus -from amberelectric.model.site import Site, SiteStatus +from amberelectric.models.channel import Channel, ChannelType +from amberelectric.models.interval import Interval +from amberelectric.models.price_descriptor import PriceDescriptor +from amberelectric.models.site import Site +from amberelectric.models.site_status import SiteStatus +from amberelectric.models.spike_status import SpikeStatus from dateutil import parser import pytest @@ -38,37 +40,40 @@ def mock_api_current_price() -> Generator: instance = Mock() general_site = Site( - GENERAL_ONLY_SITE_ID, - "11111111111", - [Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100")], - "Jemena", - SiteStatus.ACTIVE, - date(2021, 1, 1), - None, + id=GENERAL_ONLY_SITE_ID, + nmi="11111111111", + channels=[Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100")], + network="Jemena", + status=SiteStatus("active"), + activeFrom=date(2021, 1, 1), + closedOn=None, + interval_length=30, ) general_and_controlled_load = Site( - GENERAL_AND_CONTROLLED_SITE_ID, - "11111111112", - [ + id=GENERAL_AND_CONTROLLED_SITE_ID, + nmi="11111111112", + channels=[ Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100"), - Channel(identifier="E2", type=ChannelType.CONTROLLED_LOAD, tariff="A180"), + Channel(identifier="E2", type=ChannelType.CONTROLLEDLOAD, tariff="A180"), ], - "Jemena", - SiteStatus.ACTIVE, - date(2021, 1, 1), - None, + network="Jemena", + status=SiteStatus("active"), + activeFrom=date(2021, 1, 1), + closedOn=None, + interval_length=30, ) general_and_feed_in = Site( - GENERAL_AND_FEED_IN_SITE_ID, - "11111111113", - [ + id=GENERAL_AND_FEED_IN_SITE_ID, + nmi="11111111113", + channels=[ Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100"), - Channel(identifier="E2", type=ChannelType.FEED_IN, tariff="A100"), + Channel(identifier="E2", type=ChannelType.FEEDIN, tariff="A100"), ], - "Jemena", - SiteStatus.ACTIVE, - date(2021, 1, 1), - None, + network="Jemena", + status=SiteStatus("active"), + activeFrom=date(2021, 1, 1), + closedOn=None, + interval_length=30, ) instance.get_sites.return_value = [ general_site, @@ -76,44 +81,46 @@ def mock_api_current_price() -> Generator: general_and_feed_in, ] - with patch("amberelectric.api.AmberApi.create", return_value=instance): + with patch("amberelectric.AmberApi", return_value=instance): yield instance def test_normalize_descriptor() -> None: """Test normalizing descriptors works correctly.""" assert normalize_descriptor(None) is None - assert normalize_descriptor(Descriptor.NEGATIVE) == "negative" - assert normalize_descriptor(Descriptor.EXTREMELY_LOW) == "extremely_low" - assert normalize_descriptor(Descriptor.VERY_LOW) == "very_low" - assert normalize_descriptor(Descriptor.LOW) == "low" - assert normalize_descriptor(Descriptor.NEUTRAL) == "neutral" - assert normalize_descriptor(Descriptor.HIGH) == "high" - assert normalize_descriptor(Descriptor.SPIKE) == "spike" + assert normalize_descriptor(PriceDescriptor.NEGATIVE) == "negative" + assert normalize_descriptor(PriceDescriptor.EXTREMELYLOW) == "extremely_low" + assert normalize_descriptor(PriceDescriptor.VERYLOW) == "very_low" + assert normalize_descriptor(PriceDescriptor.LOW) == "low" + assert normalize_descriptor(PriceDescriptor.NEUTRAL) == "neutral" + assert normalize_descriptor(PriceDescriptor.HIGH) == "high" + assert normalize_descriptor(PriceDescriptor.SPIKE) == "spike" async def test_fetch_general_site(hass: HomeAssistant, current_price_api: Mock) -> None: """Test fetching a site with only a general channel.""" - current_price_api.get_current_price.return_value = GENERAL_CHANNEL + current_price_api.get_current_prices.return_value = GENERAL_CHANNEL data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) result = await data_service._async_update_data() - current_price_api.get_current_price.assert_called_with( + current_price_api.get_current_prices.assert_called_with( GENERAL_ONLY_SITE_ID, next=48 ) - assert result["current"].get("general") == GENERAL_CHANNEL[0] + assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance assert result["forecasts"].get("general") == [ - GENERAL_CHANNEL[1], - GENERAL_CHANNEL[2], - GENERAL_CHANNEL[3], + GENERAL_CHANNEL[1].actual_instance, + GENERAL_CHANNEL[2].actual_instance, + GENERAL_CHANNEL[3].actual_instance, ] assert result["current"].get("controlled_load") is None assert result["forecasts"].get("controlled_load") is None assert result["current"].get("feed_in") is None assert result["forecasts"].get("feed_in") is None - assert result["grid"]["renewables"] == round(GENERAL_CHANNEL[0].renewables) + assert result["grid"]["renewables"] == round( + GENERAL_CHANNEL[0].actual_instance.renewables + ) assert result["grid"]["price_spike"] == "none" @@ -122,12 +129,12 @@ async def test_fetch_no_general_site( ) -> None: """Test fetching a site with no general channel.""" - current_price_api.get_current_price.return_value = CONTROLLED_LOAD_CHANNEL + current_price_api.get_current_prices.return_value = CONTROLLED_LOAD_CHANNEL data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) with pytest.raises(UpdateFailed): await data_service._async_update_data() - current_price_api.get_current_price.assert_called_with( + current_price_api.get_current_prices.assert_called_with( GENERAL_ONLY_SITE_ID, next=48 ) @@ -135,41 +142,45 @@ async def test_fetch_no_general_site( async def test_fetch_api_error(hass: HomeAssistant, current_price_api: Mock) -> None: """Test that the old values are maintained if a second call fails.""" - current_price_api.get_current_price.return_value = GENERAL_CHANNEL + current_price_api.get_current_prices.return_value = GENERAL_CHANNEL data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) result = await data_service._async_update_data() - current_price_api.get_current_price.assert_called_with( + current_price_api.get_current_prices.assert_called_with( GENERAL_ONLY_SITE_ID, next=48 ) - assert result["current"].get("general") == GENERAL_CHANNEL[0] + assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance assert result["forecasts"].get("general") == [ - GENERAL_CHANNEL[1], - GENERAL_CHANNEL[2], - GENERAL_CHANNEL[3], + GENERAL_CHANNEL[1].actual_instance, + GENERAL_CHANNEL[2].actual_instance, + GENERAL_CHANNEL[3].actual_instance, ] assert result["current"].get("controlled_load") is None assert result["forecasts"].get("controlled_load") is None assert result["current"].get("feed_in") is None assert result["forecasts"].get("feed_in") is None - assert result["grid"]["renewables"] == round(GENERAL_CHANNEL[0].renewables) + assert result["grid"]["renewables"] == round( + GENERAL_CHANNEL[0].actual_instance.renewables + ) - current_price_api.get_current_price.side_effect = ApiException(status=403) + current_price_api.get_current_prices.side_effect = ApiException(status=403) with pytest.raises(UpdateFailed): await data_service._async_update_data() - assert result["current"].get("general") == GENERAL_CHANNEL[0] + assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance assert result["forecasts"].get("general") == [ - GENERAL_CHANNEL[1], - GENERAL_CHANNEL[2], - GENERAL_CHANNEL[3], + GENERAL_CHANNEL[1].actual_instance, + GENERAL_CHANNEL[2].actual_instance, + GENERAL_CHANNEL[3].actual_instance, ] assert result["current"].get("controlled_load") is None assert result["forecasts"].get("controlled_load") is None assert result["current"].get("feed_in") is None assert result["forecasts"].get("feed_in") is None - assert result["grid"]["renewables"] == round(GENERAL_CHANNEL[0].renewables) + assert result["grid"]["renewables"] == round( + GENERAL_CHANNEL[0].actual_instance.renewables + ) assert result["grid"]["price_spike"] == "none" @@ -178,7 +189,7 @@ async def test_fetch_general_and_controlled_load_site( ) -> None: """Test fetching a site with a general and controlled load channel.""" - current_price_api.get_current_price.return_value = ( + current_price_api.get_current_prices.return_value = ( GENERAL_CHANNEL + CONTROLLED_LOAD_CHANNEL ) data_service = AmberUpdateCoordinator( @@ -186,25 +197,30 @@ async def test_fetch_general_and_controlled_load_site( ) result = await data_service._async_update_data() - current_price_api.get_current_price.assert_called_with( + current_price_api.get_current_prices.assert_called_with( GENERAL_AND_CONTROLLED_SITE_ID, next=48 ) - assert result["current"].get("general") == GENERAL_CHANNEL[0] + assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance assert result["forecasts"].get("general") == [ - GENERAL_CHANNEL[1], - GENERAL_CHANNEL[2], - GENERAL_CHANNEL[3], + GENERAL_CHANNEL[1].actual_instance, + GENERAL_CHANNEL[2].actual_instance, + GENERAL_CHANNEL[3].actual_instance, ] - assert result["current"].get("controlled_load") is CONTROLLED_LOAD_CHANNEL[0] + assert ( + result["current"].get("controlled_load") + is CONTROLLED_LOAD_CHANNEL[0].actual_instance + ) assert result["forecasts"].get("controlled_load") == [ - CONTROLLED_LOAD_CHANNEL[1], - CONTROLLED_LOAD_CHANNEL[2], - CONTROLLED_LOAD_CHANNEL[3], + CONTROLLED_LOAD_CHANNEL[1].actual_instance, + CONTROLLED_LOAD_CHANNEL[2].actual_instance, + CONTROLLED_LOAD_CHANNEL[3].actual_instance, ] assert result["current"].get("feed_in") is None assert result["forecasts"].get("feed_in") is None - assert result["grid"]["renewables"] == round(GENERAL_CHANNEL[0].renewables) + assert result["grid"]["renewables"] == round( + GENERAL_CHANNEL[0].actual_instance.renewables + ) assert result["grid"]["price_spike"] == "none" @@ -213,31 +229,35 @@ async def test_fetch_general_and_feed_in_site( ) -> None: """Test fetching a site with a general and feed_in channel.""" - current_price_api.get_current_price.return_value = GENERAL_CHANNEL + FEED_IN_CHANNEL + current_price_api.get_current_prices.return_value = ( + GENERAL_CHANNEL + FEED_IN_CHANNEL + ) data_service = AmberUpdateCoordinator( hass, current_price_api, GENERAL_AND_FEED_IN_SITE_ID ) result = await data_service._async_update_data() - current_price_api.get_current_price.assert_called_with( + current_price_api.get_current_prices.assert_called_with( GENERAL_AND_FEED_IN_SITE_ID, next=48 ) - assert result["current"].get("general") == GENERAL_CHANNEL[0] + assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance assert result["forecasts"].get("general") == [ - GENERAL_CHANNEL[1], - GENERAL_CHANNEL[2], - GENERAL_CHANNEL[3], + GENERAL_CHANNEL[1].actual_instance, + GENERAL_CHANNEL[2].actual_instance, + GENERAL_CHANNEL[3].actual_instance, ] assert result["current"].get("controlled_load") is None assert result["forecasts"].get("controlled_load") is None - assert result["current"].get("feed_in") is FEED_IN_CHANNEL[0] + assert result["current"].get("feed_in") is FEED_IN_CHANNEL[0].actual_instance assert result["forecasts"].get("feed_in") == [ - FEED_IN_CHANNEL[1], - FEED_IN_CHANNEL[2], - FEED_IN_CHANNEL[3], + FEED_IN_CHANNEL[1].actual_instance, + FEED_IN_CHANNEL[2].actual_instance, + FEED_IN_CHANNEL[3].actual_instance, ] - assert result["grid"]["renewables"] == round(GENERAL_CHANNEL[0].renewables) + assert result["grid"]["renewables"] == round( + GENERAL_CHANNEL[0].actual_instance.renewables + ) assert result["grid"]["price_spike"] == "none" @@ -246,13 +266,13 @@ async def test_fetch_potential_spike( ) -> None: """Test fetching a site with only a general channel.""" - general_channel: list[CurrentInterval] = [ + general_channel: list[Interval] = [ generate_current_interval( ChannelType.GENERAL, parser.parse("2021-09-21T08:30:00+10:00") - ), + ) ] - general_channel[0].spike_status = SpikeStatus.POTENTIAL - current_price_api.get_current_price.return_value = general_channel + general_channel[0].actual_instance.spike_status = SpikeStatus.POTENTIAL + current_price_api.get_current_prices.return_value = general_channel data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) result = await data_service._async_update_data() assert result["grid"]["price_spike"] == "potential" @@ -261,13 +281,13 @@ async def test_fetch_potential_spike( async def test_fetch_spike(hass: HomeAssistant, current_price_api: Mock) -> None: """Test fetching a site with only a general channel.""" - general_channel: list[CurrentInterval] = [ + general_channel: list[Interval] = [ generate_current_interval( ChannelType.GENERAL, parser.parse("2021-09-21T08:30:00+10:00") - ), + ) ] - general_channel[0].spike_status = SpikeStatus.SPIKE - current_price_api.get_current_price.return_value = general_channel + general_channel[0].actual_instance.spike_status = SpikeStatus.SPIKE + current_price_api.get_current_prices.return_value = general_channel data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) result = await data_service._async_update_data() assert result["grid"]["price_spike"] == "spike" diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index 3a5626d14d5..203b65d6df6 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -3,8 +3,9 @@ from collections.abc import AsyncGenerator from unittest.mock import Mock, patch -from amberelectric.model.current_interval import CurrentInterval -from amberelectric.model.range import Range +from amberelectric.models.current_interval import CurrentInterval +from amberelectric.models.interval import Interval +from amberelectric.models.range import Range import pytest from homeassistant.components.amberelectric.const import ( @@ -44,10 +45,10 @@ async def setup_general(hass: HomeAssistant) -> AsyncGenerator[Mock]: instance = Mock() with patch( - "amberelectric.api.AmberApi.create", + "amberelectric.AmberApi", return_value=instance, ) as mock_update: - instance.get_current_price = Mock(return_value=GENERAL_CHANNEL) + instance.get_current_prices = Mock(return_value=GENERAL_CHANNEL) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() yield mock_update.return_value @@ -68,10 +69,10 @@ async def setup_general_and_controlled_load( instance = Mock() with patch( - "amberelectric.api.AmberApi.create", + "amberelectric.AmberApi", return_value=instance, ) as mock_update: - instance.get_current_price = Mock( + instance.get_current_prices = Mock( return_value=GENERAL_CHANNEL + CONTROLLED_LOAD_CHANNEL ) assert await async_setup_component(hass, DOMAIN, {}) @@ -92,10 +93,10 @@ async def setup_general_and_feed_in(hass: HomeAssistant) -> AsyncGenerator[Mock] instance = Mock() with patch( - "amberelectric.api.AmberApi.create", + "amberelectric.AmberApi", return_value=instance, ) as mock_update: - instance.get_current_price = Mock( + instance.get_current_prices = Mock( return_value=GENERAL_CHANNEL + FEED_IN_CHANNEL ) assert await async_setup_component(hass, DOMAIN, {}) @@ -126,7 +127,7 @@ async def test_general_price_sensor(hass: HomeAssistant, setup_general: Mock) -> assert attributes.get("range_max") is None with_range: list[CurrentInterval] = GENERAL_CHANNEL - with_range[0].range = Range(7.8, 12.4) + with_range[0].actual_instance.range = Range(min=7.8, max=12.4) setup_general.get_current_price.return_value = with_range config_entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -211,8 +212,8 @@ async def test_general_forecast_sensor( assert first_forecast.get("range_min") is None assert first_forecast.get("range_max") is None - with_range: list[CurrentInterval] = GENERAL_CHANNEL - with_range[1].range = Range(7.8, 12.4) + with_range: list[Interval] = GENERAL_CHANNEL + with_range[1].actual_instance.range = Range(min=7.8, max=12.4) setup_general.get_current_price.return_value = with_range config_entry = hass.config_entries.async_entries(DOMAIN)[0]