From 0e58df94348e1a584734eb40857d3feb66383d3d Mon Sep 17 00:00:00 2001 From: Jules Dejaeghere Date: Sat, 30 Dec 2023 18:23:36 +0100 Subject: [PATCH] Add config option to use the deprecated forecast attribute --- custom_components/irm_kmi/__init__.py | 13 ++++-- custom_components/irm_kmi/config_flow.py | 59 +++++++++++++++++++++--- custom_components/irm_kmi/const.py | 35 +++++++++----- custom_components/irm_kmi/coordinator.py | 25 +++++----- custom_components/irm_kmi/utils.py | 7 +++ custom_components/irm_kmi/weather.py | 28 +++++++++-- tests/conftest.py | 8 ++-- tests/test_config_flow.py | 10 ++-- 8 files changed, 138 insertions(+), 47 deletions(-) diff --git a/custom_components/irm_kmi/__init__.py b/custom_components/irm_kmi/__init__.py index e3bb875..29db1df 100644 --- a/custom_components/irm_kmi/__init__.py +++ b/custom_components/irm_kmi/__init__.py @@ -7,8 +7,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError -from .const import (CONF_DARK_MODE, CONF_STYLE, CONF_STYLE_STD, DOMAIN, - PLATFORMS) +from .const import (CONF_DARK_MODE, CONF_STYLE, OPTION_STYLE_STD, DOMAIN, + PLATFORMS, OPTION_DEPRECATED_FORECAST_NOT_USED, + CONF_USE_DEPRECATED_FORECAST) from .coordinator import IrmKmiCoordinator from .weather import IrmKmiWeather @@ -50,15 +51,19 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): """Migrate old entry.""" _LOGGER.debug(f"Migrating from version {config_entry.version}") - if config_entry.version > 1: + if config_entry.version > 2: # This means the user has downgraded from a future version return False new = {**config_entry.data} if config_entry.version == 1: - new = new | {CONF_STYLE: CONF_STYLE_STD, CONF_DARK_MODE: True} + new = new | {CONF_STYLE: OPTION_STYLE_STD, CONF_DARK_MODE: True} config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry, data=new) + if config_entry.version == 2: + new = new | {CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED} + config_entry.version = 3 hass.config_entries.async_update_entry(config_entry, data=new) _LOGGER.debug(f"Migration to version {config_entry.version} successful") diff --git a/custom_components/irm_kmi/config_flow.py b/custom_components/irm_kmi/config_flow.py index 60db263..b55edb0 100644 --- a/custom_components/irm_kmi/config_flow.py +++ b/custom_components/irm_kmi/config_flow.py @@ -3,8 +3,9 @@ import logging import voluptuous as vol from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, OptionsFlow, ConfigEntry from homeassistant.const import CONF_ZONE +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import (EntitySelector, EntitySelectorConfig, @@ -12,14 +13,22 @@ from homeassistant.helpers.selector import (EntitySelector, SelectSelectorConfig, SelectSelectorMode) +from .utils import get_config_value from .const import (CONF_DARK_MODE, CONF_STYLE, CONF_STYLE_OPTIONS, - CONF_STYLE_STD, DOMAIN) + OPTION_STYLE_STD, DOMAIN, CONF_USE_DEPRECATED_FORECAST, OPTION_DEPRECATED_FORECAST_NOT_USED, + CONF_USE_DEPRECATED_FORECAST_OPTIONS) _LOGGER = logging.getLogger(__name__) class IrmKmiConfigFlow(ConfigFlow, domain=DOMAIN): - VERSION = 2 + VERSION = 3 + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Create the options flow.""" + return IrmKmiOptionFlow(config_entry) async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Define the user step of the configuration flow.""" @@ -34,7 +43,8 @@ class IrmKmiConfigFlow(ConfigFlow, domain=DOMAIN): title=state.name if state else "IRM KMI", data={CONF_ZONE: user_input[CONF_ZONE], CONF_STYLE: user_input[CONF_STYLE], - CONF_DARK_MODE: user_input[CONF_DARK_MODE]}, + CONF_DARK_MODE: user_input[CONF_DARK_MODE], + CONF_USE_DEPRECATED_FORECAST: user_input[CONF_USE_DEPRECATED_FORECAST]}, ) return self.async_show_form( @@ -43,10 +53,47 @@ class IrmKmiConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required(CONF_ZONE): EntitySelector(EntitySelectorConfig(domain=ZONE_DOMAIN)), - vol.Optional(CONF_STYLE, default=CONF_STYLE_STD): + vol.Optional(CONF_STYLE, default=OPTION_STYLE_STD): SelectSelector(SelectSelectorConfig(options=CONF_STYLE_OPTIONS, mode=SelectSelectorMode.DROPDOWN, translation_key=CONF_STYLE)), - vol.Optional(CONF_DARK_MODE, default=False): bool + vol.Optional(CONF_DARK_MODE, default=False): bool, + + vol.Optional(CONF_USE_DEPRECATED_FORECAST, default=OPTION_DEPRECATED_FORECAST_NOT_USED): + SelectSelector(SelectSelectorConfig(options=CONF_USE_DEPRECATED_FORECAST_OPTIONS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_USE_DEPRECATED_FORECAST)) + })) + + +class IrmKmiOptionFlow(OptionsFlow): + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input: dict | None = None) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional(CONF_STYLE, default=get_config_value(self.config_entry, CONF_STYLE)): + SelectSelector(SelectSelectorConfig(options=CONF_STYLE_OPTIONS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_STYLE)), + + vol.Optional(CONF_DARK_MODE, default=get_config_value(self.config_entry, CONF_DARK_MODE)): bool, + + vol.Optional(CONF_USE_DEPRECATED_FORECAST, + default=get_config_value(self.config_entry, CONF_USE_DEPRECATED_FORECAST)): + SelectSelector(SelectSelectorConfig(options=CONF_USE_DEPRECATED_FORECAST_OPTIONS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_USE_DEPRECATED_FORECAST)) + } + ), + ) diff --git a/custom_components/irm_kmi/const.py b/custom_components/irm_kmi/const.py index 1bca189..8999d78 100644 --- a/custom_components/irm_kmi/const.py +++ b/custom_components/irm_kmi/const.py @@ -22,28 +22,39 @@ OUT_OF_BENELUX: Final = ["außerhalb der Benelux (Brussels)", "Buiten de Benelux (Brussel)"] LANGS: Final = ['en', 'fr', 'nl', 'de'] -CONF_STYLE_STD: Final = 'standard_style' -CONF_STYLE_CONTRAST: Final = 'contrast_style' -CONF_STYLE_YELLOW_RED: Final = 'yellow_red_style' -CONF_STYLE_SATELLITE: Final = 'satellite_style' +OPTION_STYLE_STD: Final = 'standard_style' +OPTION_STYLE_CONTRAST: Final = 'contrast_style' +OPTION_STYLE_YELLOW_RED: Final = 'yellow_red_style' +OPTION_STYLE_SATELLITE: Final = 'satellite_style' CONF_STYLE: Final = "style" CONF_STYLE_OPTIONS: Final = [ - CONF_STYLE_STD, - CONF_STYLE_CONTRAST, - CONF_STYLE_YELLOW_RED, - CONF_STYLE_SATELLITE + OPTION_STYLE_STD, + OPTION_STYLE_CONTRAST, + OPTION_STYLE_YELLOW_RED, + OPTION_STYLE_SATELLITE ] CONF_DARK_MODE: Final = "dark_mode" STYLE_TO_PARAM_MAP: Final = { - CONF_STYLE_STD: 1, - CONF_STYLE_CONTRAST: 2, - CONF_STYLE_YELLOW_RED: 3, - CONF_STYLE_SATELLITE: 4 + OPTION_STYLE_STD: 1, + OPTION_STYLE_CONTRAST: 2, + OPTION_STYLE_YELLOW_RED: 3, + OPTION_STYLE_SATELLITE: 4 } +CONF_USE_DEPRECATED_FORECAST: Final = 'use_deprecated_forecast_attribute' +OPTION_DEPRECATED_FORECAST_NOT_USED: Final = 'do_not_use_deprecated_forecast' +OPTION_DEPRECATED_FORECAST_DAILY: Final = 'daily_in_deprecated_forecast' +OPTION_DEPRECATED_FORECAST_HOURLY: Final = 'hourly_in_use_deprecated_forecast' + +CONF_USE_DEPRECATED_FORECAST_OPTIONS: Final = [ + OPTION_DEPRECATED_FORECAST_NOT_USED, + OPTION_DEPRECATED_FORECAST_DAILY, + OPTION_DEPRECATED_FORECAST_HOURLY +] + # map ('ww', 'dayNight') tuple from IRM KMI to HA conditions IRM_KMI_TO_HA_CONDITION_MAP: Final = { (0, 'd'): ATTR_CONDITION_SUNNY, diff --git a/custom_components/irm_kmi/coordinator.py b/custom_components/irm_kmi/coordinator.py index 61e1186..f5c1f17 100644 --- a/custom_components/irm_kmi/coordinator.py +++ b/custom_components/irm_kmi/coordinator.py @@ -9,7 +9,7 @@ import async_timeout import pytz from homeassistant.components.weather import Forecast from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -18,12 +18,12 @@ from homeassistant.helpers.update_coordinator import (DataUpdateCoordinator, from PIL import Image, ImageDraw, ImageFont from .api import IrmKmiApiClient, IrmKmiApiError -from .const import CONF_DARK_MODE, CONF_STYLE, CONF_STYLE_SATELLITE +from .const import CONF_DARK_MODE, CONF_STYLE, OPTION_STYLE_SATELLITE from .const import IRM_KMI_TO_HA_CONDITION_MAP as CDT_MAP from .const import LANGS, OUT_OF_BENELUX, STYLE_TO_PARAM_MAP from .data import (AnimationFrameData, CurrentWeatherData, IrmKmiForecast, ProcessedCoordinatorData, RadarAnimationData) -from .utils import disable_from_config +from .utils import disable_from_config, get_config_value _LOGGER = logging.getLogger(__name__) @@ -42,9 +42,11 @@ class IrmKmiCoordinator(DataUpdateCoordinator): update_interval=timedelta(minutes=7), ) self._api_client = IrmKmiApiClient(session=async_get_clientsession(hass)) - self._zone = entry.data.get('zone') + self._zone = get_config_value(entry, CONF_ZONE) + self._dark_mode = get_config_value(entry, CONF_DARK_MODE) + self._style = get_config_value(entry, CONF_STYLE) self._config_entry = entry - self._disabled = False + _LOGGER.debug(f"Config entry: {entry.title} -- {entry.data} -- {entry.options}") async def _async_update_data(self) -> ProcessedCoordinatorData: """Fetch data from API endpoint. @@ -127,16 +129,14 @@ class IrmKmiCoordinator(DataUpdateCoordinator): localisation_layer_url: str) -> tuple[Any]: """Download a batch of images to create the radar frames.""" coroutines = list() - dark_mode = self._config_entry.data[CONF_DARK_MODE] - style = self._config_entry.data[CONF_STYLE] coroutines.append( self._api_client.get_image(localisation_layer_url, - params={'th': 'd' if country == 'NL' or not dark_mode else 'n'})) + params={'th': 'd' if country == 'NL' or not self._dark_mode else 'n'})) for frame in animation_data: if frame.get('uri', None) is not None: coroutines.append( - self._api_client.get_image(frame.get('uri'), params={'rs': STYLE_TO_PARAM_MAP[style]})) + self._api_client.get_image(frame.get('uri'), params={'rs': STYLE_TO_PARAM_MAP[self._style]})) async with async_timeout.timeout(20): images_from_api = await asyncio.gather(*coroutines) @@ -153,17 +153,16 @@ class IrmKmiCoordinator(DataUpdateCoordinator): Adds text in the top right to specify the timestamp of each image.""" background: Image fill_color: tuple - dark_mode = self._config_entry.data[CONF_DARK_MODE] - satellite_mode = self._config_entry.data[CONF_STYLE] == CONF_STYLE_SATELLITE + satellite_mode = self._style == OPTION_STYLE_SATELLITE if country == 'NL': background = Image.open("custom_components/irm_kmi/resources/nl.png").convert('RGBA') fill_color = (0, 0, 0) else: image_path = (f"custom_components/irm_kmi/resources/be_" - f"{'satellite' if satellite_mode else 'black' if dark_mode else 'white'}.png") + f"{'satellite' if satellite_mode else 'black' if self._dark_mode else 'white'}.png") background = (Image.open(image_path).convert('RGBA')) - fill_color = (255, 255, 255) if dark_mode or satellite_mode else (0, 0, 0) + fill_color = (255, 255, 255) if self._dark_mode or satellite_mode else (0, 0, 0) most_recent_frame = None tz = pytz.timezone(self.hass.config.time_zone) diff --git a/custom_components/irm_kmi/utils.py b/custom_components/irm_kmi/utils.py index 56662e0..62d0957 100644 --- a/custom_components/irm_kmi/utils.py +++ b/custom_components/irm_kmi/utils.py @@ -1,4 +1,5 @@ import logging +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -23,3 +24,9 @@ def modify_from_config(hass: HomeAssistant, config_entry: ConfigEntry, enable: b _LOGGER.info(f"Disabling device {device.name} because it is out of Benelux") dr.async_update_device(device_id=device.id, disabled_by=None if enable else device_registry.DeviceEntryDisabler.INTEGRATION) + + +def get_config_value(config_entry: ConfigEntry, key: str) -> Any: + if config_entry.options and key in config_entry.options: + return config_entry.options[key] + return config_entry.data[key] diff --git a/custom_components/irm_kmi/weather.py b/custom_components/irm_kmi/weather.py index 7026149..3396e66 100644 --- a/custom_components/irm_kmi/weather.py +++ b/custom_components/irm_kmi/weather.py @@ -1,5 +1,5 @@ """Support for IRM KMI weather.""" - +import asyncio import logging from typing import List @@ -13,8 +13,11 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN +from . import DOMAIN, CONF_USE_DEPRECATED_FORECAST +from .const import OPTION_DEPRECATED_FORECAST_HOURLY, OPTION_DEPRECATED_FORECAST_NOT_USED, \ + OPTION_DEPRECATED_FORECAST_DAILY from .coordinator import IrmKmiCoordinator +from .utils import get_config_value _LOGGER = logging.getLogger(__name__) @@ -42,6 +45,7 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity): manufacturer="IRM KMI", name=entry.title ) + self._deprecated_forecast_as = get_config_value(entry, CONF_USE_DEPRECATED_FORECAST) @property def supported_features(self) -> WeatherEntityFeature: @@ -99,10 +103,27 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity): def uv_index(self) -> float | None: return self.coordinator.data.get('current_weather', {}).get('uv_index') + @property + def forecast(self) -> list[Forecast] | None: + """This attribute is deprecated by Home Assistant by still implemented for compatibility + with older components. Newer components should use the service weather.get_forecasts instead.""" + if self._deprecated_forecast_as == OPTION_DEPRECATED_FORECAST_NOT_USED: + return None + elif self._deprecated_forecast_as == OPTION_DEPRECATED_FORECAST_HOURLY: + return self.coordinator.data.get('hourly_forecast') + elif self._deprecated_forecast_as == OPTION_DEPRECATED_FORECAST_DAILY: + return self.daily_forecast() + async def async_forecast_twice_daily(self) -> List[Forecast] | None: return self.coordinator.data.get('daily_forecast') async def async_forecast_daily(self) -> list[Forecast] | None: + return self.daily_forecast() + + async def async_forecast_hourly(self) -> list[Forecast] | None: + return self.coordinator.data.get('hourly_forecast') + + def daily_forecast(self) -> list[Forecast] | None: data: list[Forecast] = self.coordinator.data.get('daily_forecast') if not isinstance(data, list): return None @@ -113,6 +134,3 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity): if len(data) > 1 and data[0].get('native_templow') is None and not data[1].get('is_daytime'): data[0]['native_templow'] = data[1].get('native_templow') return [f for f in data if f.get('is_daytime')] - - async def async_forecast_hourly(self) -> list[Forecast] | None: - return self.coordinator.data.get('hourly_forecast') diff --git a/tests/conftest.py b/tests/conftest.py index 73cb3f8..ef9bfe5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,8 @@ from pytest_homeassistant_custom_component.common import (MockConfigEntry, from custom_components.irm_kmi.api import IrmKmiApiParametersError from custom_components.irm_kmi.const import (CONF_DARK_MODE, CONF_STYLE, - CONF_STYLE_STD, DOMAIN) + OPTION_STYLE_STD, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED, + CONF_USE_DEPRECATED_FORECAST) async def patched(url: str, params: dict | None = None) -> bytes: @@ -43,8 +44,9 @@ def mock_config_entry() -> MockConfigEntry: title="Home", domain=DOMAIN, data={CONF_ZONE: "zone.home", - CONF_STYLE: CONF_STYLE_STD, - CONF_DARK_MODE: True}, + CONF_STYLE: OPTION_STYLE_STD, + CONF_DARK_MODE: True, + CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED}, unique_id="zone.home", ) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index a9b7c6e..38d2a60 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -9,7 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from custom_components.irm_kmi.const import (CONF_DARK_MODE, CONF_STYLE, - CONF_STYLE_STD, DOMAIN) + OPTION_STYLE_STD, DOMAIN, CONF_USE_DEPRECATED_FORECAST, + OPTION_DEPRECATED_FORECAST_NOT_USED) async def test_full_user_flow( @@ -27,12 +28,13 @@ async def test_full_user_flow( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ZONE: ENTITY_ID_HOME, - CONF_STYLE: CONF_STYLE_STD, + CONF_STYLE: OPTION_STYLE_STD, CONF_DARK_MODE: False}, ) print(result2) assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "test home" assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME, - CONF_STYLE: CONF_STYLE_STD, - CONF_DARK_MODE: False} + CONF_STYLE: OPTION_STYLE_STD, + CONF_DARK_MODE: False, + CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED}