diff --git a/custom_components/irm_kmi/__init__.py b/custom_components/irm_kmi/__init__.py index 39625a7..5453fa5 100644 --- a/custom_components/irm_kmi/__init__.py +++ b/custom_components/irm_kmi/__init__.py @@ -7,8 +7,8 @@ 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_USE_DEPRECATED_FORECAST, - CONFIG_FLOW_VERSION, DOMAIN, +from .const import (CONF_DARK_MODE, CONF_LANGUAGE_OVERRIDE, CONF_STYLE, + CONF_USE_DEPRECATED_FORECAST, CONFIG_FLOW_VERSION, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED, OPTION_STYLE_STD, PLATFORMS) from .coordinator import IrmKmiCoordinator @@ -68,6 +68,11 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): config_entry.version = 3 hass.config_entries.async_update_entry(config_entry, data=new) + if config_entry.version == 3: + new = new | {CONF_LANGUAGE_OVERRIDE: None} + config_entry.version = 4 + hass.config_entries.async_update_entry(config_entry, data=new) + _LOGGER.debug(f"Migration to version {config_entry.version} successful") return True diff --git a/custom_components/irm_kmi/config_flow.py b/custom_components/irm_kmi/config_flow.py index e81a9dc..68db696 100644 --- a/custom_components/irm_kmi/config_flow.py +++ b/custom_components/irm_kmi/config_flow.py @@ -16,8 +16,9 @@ from homeassistant.helpers.selector import (EntitySelector, SelectSelectorMode) from .api import IrmKmiApiClient -from .const import (CONF_DARK_MODE, CONF_STYLE, CONF_STYLE_OPTIONS, - CONF_USE_DEPRECATED_FORECAST, +from .const import (CONF_DARK_MODE, CONF_LANGUAGE_OVERRIDE, + CONF_LANGUAGE_OVERRIDE_OPTIONS, CONF_STYLE, + CONF_STYLE_OPTIONS, CONF_USE_DEPRECATED_FORECAST, CONF_USE_DEPRECATED_FORECAST_OPTIONS, CONFIG_FLOW_VERSION, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED, OPTION_STYLE_STD, OUT_OF_BENELUX) @@ -49,7 +50,7 @@ class IrmKmiConfigFlow(ConfigFlow, domain=DOMAIN): if not errors: api_data = {} try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(60): api_data = await IrmKmiApiClient( session=async_get_clientsession(self.hass)).get_forecasts_coord( {'lat': zone.attributes[ATTR_LATITUDE], @@ -71,7 +72,8 @@ class IrmKmiConfigFlow(ConfigFlow, domain=DOMAIN): data={CONF_ZONE: user_input[CONF_ZONE], CONF_STYLE: user_input[CONF_STYLE], CONF_DARK_MODE: user_input[CONF_DARK_MODE], - CONF_USE_DEPRECATED_FORECAST: user_input[CONF_USE_DEPRECATED_FORECAST]}, + CONF_USE_DEPRECATED_FORECAST: user_input[CONF_USE_DEPRECATED_FORECAST], + CONF_LANGUAGE_OVERRIDE: user_input[CONF_LANGUAGE_OVERRIDE]}, ) return self.async_show_form( @@ -92,7 +94,12 @@ class IrmKmiConfigFlow(ConfigFlow, domain=DOMAIN): 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)) + translation_key=CONF_USE_DEPRECATED_FORECAST)), + + vol.Optional(CONF_LANGUAGE_OVERRIDE, default='none'): + SelectSelector(SelectSelectorConfig(options=CONF_LANGUAGE_OVERRIDE_OPTIONS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_LANGUAGE_OVERRIDE)) })) @@ -105,6 +112,7 @@ class IrmKmiOptionFlow(OptionsFlow): async def async_step_init(self, user_input: dict | None = None) -> FlowResult: """Manage the options.""" if user_input is not None: + _LOGGER.debug(user_input) return self.async_create_entry(data=user_input) return self.async_show_form( @@ -122,7 +130,13 @@ class IrmKmiOptionFlow(OptionsFlow): 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)) + translation_key=CONF_USE_DEPRECATED_FORECAST)), + + vol.Optional(CONF_LANGUAGE_OVERRIDE, + default=get_config_value(self.config_entry, CONF_LANGUAGE_OVERRIDE)): + SelectSelector(SelectSelectorConfig(options=CONF_LANGUAGE_OVERRIDE_OPTIONS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_LANGUAGE_OVERRIDE)) } ), ) diff --git a/custom_components/irm_kmi/const.py b/custom_components/irm_kmi/const.py index 72727ef..1e6b60e 100644 --- a/custom_components/irm_kmi/const.py +++ b/custom_components/irm_kmi/const.py @@ -15,7 +15,7 @@ from homeassistant.const import Platform DOMAIN: Final = 'irm_kmi' PLATFORMS: Final = [Platform.WEATHER, Platform.CAMERA, Platform.BINARY_SENSOR, Platform.SENSOR] -CONFIG_FLOW_VERSION = 3 +CONFIG_FLOW_VERSION = 4 OUT_OF_BENELUX: Final = ["außerhalb der Benelux (Brussels)", "Hors de Belgique (Bxl)", @@ -58,6 +58,12 @@ CONF_USE_DEPRECATED_FORECAST_OPTIONS: Final = [ OPTION_DEPRECATED_FORECAST_HOURLY ] +CONF_LANGUAGE_OVERRIDE: Final = 'language_override' + +CONF_LANGUAGE_OVERRIDE_OPTIONS: Final = [ + 'none', "fr", "nl", "de", "en" +] + REPAIR_SOLUTION: Final = "repair_solution" REPAIR_OPT_MOVE: Final = "repair_option_move" REPAIR_OPT_DELETE: Final = "repair_option_delete" diff --git a/custom_components/irm_kmi/coordinator.py b/custom_components/irm_kmi/coordinator.py index d5721d3..808e6c2 100644 --- a/custom_components/irm_kmi/coordinator.py +++ b/custom_components/irm_kmi/coordinator.py @@ -27,7 +27,7 @@ from .data import (AnimationFrameData, CurrentWeatherData, IrmKmiForecast, ProcessedCoordinatorData, RadarAnimationData, WarningData) from .pollen import PollenParser from .rain_graph import RainGraph -from .utils import disable_from_config, get_config_value +from .utils import disable_from_config, get_config_value, preferred_language _LOGGER = logging.getLogger(__name__) @@ -128,7 +128,7 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator): localisation = images_from_api[0] images_from_api = images_from_api[1:] - lang = self.hass.config.language if self.hass.config.language in LANGS else 'en' + lang = preferred_language(self.hass, self._config_entry) radar_animation = RadarAnimationData( hint=api_data.get('animation', {}).get('sequenceHint', {}).get(lang), unit=api_data.get('animation', {}).get('unit', {}).get(lang), @@ -340,6 +340,7 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator): forecasts = list() n_days = 0 + lang = preferred_language(self.hass, self._config_entry) for (idx, f) in enumerate(data): precipitation = None @@ -377,7 +378,7 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator): precipitation_probability=f.get('precipChance', None), wind_bearing=wind_bearing, is_daytime=is_daytime, - text=f.get('text', {}).get(self.hass.config.language, ""), + text=f.get('text', {}).get(lang, ""), ) # Swap temperature and templow if needed if (forecast['native_templow'] is not None @@ -441,6 +442,7 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator): if warning_data is None or not isinstance(warning_data, list) or len(warning_data) == 0: return [] + lang = preferred_language(self.hass, self._config_entry) result = list() for data in warning_data: try: @@ -456,7 +458,6 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator): except TypeError: level = None - lang = self.hass.config.language if self.hass.config.language in LANGS else 'en' result.append( WarningData( slug=SLUG_MAP.get(warning_id, 'unknown'), diff --git a/custom_components/irm_kmi/translations/en.json b/custom_components/irm_kmi/translations/en.json index 7aee83c..9f898b4 100644 --- a/custom_components/irm_kmi/translations/en.json +++ b/custom_components/irm_kmi/translations/en.json @@ -9,9 +9,11 @@ "user": { "title": "Configuration", "data": { + "zone": "Zone", "style": "Style of the radar", "dark_mode": "Radar dark mode", - "use_deprecated_forecast_attribute": "Use the deprecated forecat attribute" + "use_deprecated_forecast_attribute": "Use the deprecated forecat attribute", + "language_override": "Language" } } }, @@ -43,6 +45,15 @@ "repair_option_move": "I moved the zone in Benelux", "repair_option_delete": "Delete that config entry" } + }, + "language_override": { + "options": { + "none": "Follow Home Assistant server language", + "fr": "French", + "nl": "Dutch", + "de": "German", + "en": "English" + } } }, "options": { @@ -52,7 +63,8 @@ "data": { "style": "Style of the radar", "dark_mode": "Radar dark mode", - "use_deprecated_forecast_attribute": "Use the deprecated forecat attribute" + "use_deprecated_forecast_attribute": "Use the deprecated forecat attribute", + "language_override": "Language" } } } diff --git a/custom_components/irm_kmi/translations/fr.json b/custom_components/irm_kmi/translations/fr.json index 0447d2c..219a173 100644 --- a/custom_components/irm_kmi/translations/fr.json +++ b/custom_components/irm_kmi/translations/fr.json @@ -9,9 +9,11 @@ "user": { "title": "Configuration", "data": { + "zone": "Zone", "style": "Style du radar", "dark_mode": "Radar en mode sombre", - "use_deprecated_forecast_attribute": "Utiliser l'attribut forecat (déprécié)" + "use_deprecated_forecast_attribute": "Utiliser l'attribut forecat (déprécié)", + "language_override": "Langue" } } }, @@ -43,6 +45,15 @@ "repair_option_move": "J'ai déplacé la zone dans le Benelux", "repair_option_delete": "Supprimer cette configuration" } + }, + "language_override": { + "options": { + "none": "Langue du serveur Home Assistant", + "fr": "Français", + "nl": "Néerlandais", + "de": "Allemand", + "en": "Anglais" + } } }, "options": { @@ -52,7 +63,8 @@ "data": { "style": "Style du radar", "dark_mode": "Radar en mode sombre", - "use_deprecated_forecast_attribute": "Utiliser l'attribut forecat (déprécié)" + "use_deprecated_forecast_attribute": "Utiliser l'attribut forecat (déprécié)", + "language_override": "Langue" } } } diff --git a/custom_components/irm_kmi/translations/nl.json b/custom_components/irm_kmi/translations/nl.json index 63b1014..9c3a27c 100644 --- a/custom_components/irm_kmi/translations/nl.json +++ b/custom_components/irm_kmi/translations/nl.json @@ -9,9 +9,11 @@ "user": { "title": "Instellingen", "data": { + "zone": "Zone", "style": "Radarstijl", "dark_mode": "Radar in donkere modus", - "use_deprecated_forecast_attribute": "Gebruik het forecat attribuut (afgeschaft)" + "use_deprecated_forecast_attribute": "Gebruik het forecat attribuut (afgeschaft)", + "language_override": "Taal" } } }, @@ -43,6 +45,15 @@ "repair_option_move": "Ik heb de zone verplaats naar de Benelux", "repair_option_delete": "Deze configuratie verwijderen" } + }, + "language_override": { + "options": { + "none": "Zelfde als Home Assistant server taal", + "fr": "Frans", + "nl": "Nederlands", + "de": "Duits", + "en": "Engels" + } } }, "options": { @@ -52,7 +63,8 @@ "data": { "style": "Radarstijl", "dark_mode": "Radar in donkere modus", - "use_deprecated_forecast_attribute": "Gebruik het forecat attribuut (afgeschaft)" + "use_deprecated_forecast_attribute": "Gebruik het forecat attribuut (afgeschaft)", + "language_override": "Taal" } } } diff --git a/custom_components/irm_kmi/utils.py b/custom_components/irm_kmi/utils.py index 05d62d0..e9bb7af 100644 --- a/custom_components/irm_kmi/utils.py +++ b/custom_components/irm_kmi/utils.py @@ -5,6 +5,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry +from .const import CONF_LANGUAGE_OVERRIDE, LANGS + _LOGGER = logging.getLogger(__name__) @@ -29,3 +31,10 @@ 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] + + +def preferred_language(hass: HomeAssistant, config_entry: ConfigEntry) -> str: + if get_config_value(config_entry, CONF_LANGUAGE_OVERRIDE) == 'none': + return hass.config.language if hass.config.language in LANGS else 'en' + + return get_config_value(config_entry, CONF_LANGUAGE_OVERRIDE) diff --git a/custom_components/irm_kmi/weather.py b/custom_components/irm_kmi/weather.py index 46da146..114700b 100644 --- a/custom_components/irm_kmi/weather.py +++ b/custom_components/irm_kmi/weather.py @@ -162,6 +162,8 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity): now = dt.now() now = now.replace(minute=(now.minute // 10) * 10, second=0, microsecond=0) + # TODO adapt the return value to match the weather.get_forecasts in next breaking change release + # return { 'forecast': [...] } return [f for f in self.coordinator.data.get('radar_forecast') if include_past_forecasts or datetime.fromisoformat(f.get('datetime')) >= now] diff --git a/requirements.txt b/requirements.txt index a57093f..6f26ef2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ aiohttp==3.9.5 async-timeout==4.0.3 -homeassistant==2024.5.2 +homeassistant==2024.5.4 voluptuous==0.13.1 pytz==2024.1 svgwrite==1.4.3 \ No newline at end of file diff --git a/requirements_tests.txt b/requirements_tests.txt index 7c241b8..4726781 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -1,5 +1,5 @@ -homeassistant==2024.5.2 +homeassistant==2024.5.4 pytest -pytest_homeassistant_custom_component==0.13.122 +pytest_homeassistant_custom_component==0.13.124 freezegun isort \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 80a33f1..e9134a5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ from custom_components.irm_kmi.api import (IrmKmiApiError, from custom_components.irm_kmi.const import ( CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED, - OPTION_DEPRECATED_FORECAST_TWICE_DAILY, OPTION_STYLE_STD) + OPTION_DEPRECATED_FORECAST_TWICE_DAILY, OPTION_STYLE_STD, CONF_LANGUAGE_OVERRIDE) def get_api_data(fixture: str) -> dict: @@ -52,7 +52,8 @@ def mock_config_entry() -> MockConfigEntry: data={CONF_ZONE: "zone.home", CONF_STYLE: OPTION_STYLE_STD, CONF_DARK_MODE: True, - CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED}, + CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED, + CONF_LANGUAGE_OVERRIDE: 'none'}, unique_id="zone.home", ) @@ -66,7 +67,8 @@ def mock_config_entry_with_deprecated() -> MockConfigEntry: data={CONF_ZONE: "zone.home", CONF_STYLE: OPTION_STYLE_STD, CONF_DARK_MODE: True, - CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_TWICE_DAILY}, + CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_TWICE_DAILY, + CONF_LANGUAGE_OVERRIDE: 'none'}, unique_id="zone.home", ) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index ca57350..549148c 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -11,9 +11,10 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.irm_kmi import async_migrate_entry from custom_components.irm_kmi.const import ( - CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, - CONFIG_FLOW_VERSION, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED, - OPTION_STYLE_SATELLITE, OPTION_STYLE_STD) + CONF_DARK_MODE, CONF_LANGUAGE_OVERRIDE, CONF_STYLE, + CONF_USE_DEPRECATED_FORECAST, CONFIG_FLOW_VERSION, DOMAIN, + OPTION_DEPRECATED_FORECAST_NOT_USED, OPTION_STYLE_SATELLITE, + OPTION_STYLE_STD) async def test_full_user_flow( @@ -40,7 +41,8 @@ async def test_full_user_flow( assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME, CONF_STYLE: OPTION_STYLE_STD, CONF_DARK_MODE: False, - CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED} + CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED, + CONF_LANGUAGE_OVERRIDE: 'none'} async def test_config_flow_out_benelux_zone( @@ -128,7 +130,8 @@ async def test_option_flow( assert result["data"] == { CONF_STYLE: OPTION_STYLE_SATELLITE, CONF_DARK_MODE: True, - CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED + CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED, + CONF_LANGUAGE_OVERRIDE: 'none' } diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 432a205..bf62186 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -7,6 +7,7 @@ from homeassistant.components.weather import (ATTR_CONDITION_CLOUDY, from homeassistant.core import HomeAssistant from pytest_homeassistant_custom_component.common import MockConfigEntry +from custom_components.irm_kmi.const import CONF_LANGUAGE_OVERRIDE from custom_components.irm_kmi.coordinator import IrmKmiCoordinator from custom_components.irm_kmi.data import (CurrentWeatherData, IrmKmiForecast, ProcessedCoordinatorData, @@ -91,6 +92,7 @@ async def test_daily_forecast( ) -> None: api_data = get_api_data("forecast.json").get('for', {}).get('daily') + mock_config_entry.data = mock_config_entry.data | {CONF_LANGUAGE_OVERRIDE: 'fr'} coordinator = IrmKmiCoordinator(hass, mock_config_entry) result = coordinator.daily_list_to_forecast(api_data) @@ -108,7 +110,7 @@ async def test_daily_forecast( precipitation_probability=0, wind_bearing=180, is_daytime=True, - text='Hey!', + text='Bar', ) assert result[1] == expected @@ -163,13 +165,13 @@ async def test_refresh_succeed_even_when_pollen_and_radar_fail( assert result.get('pollen') == PollenParser.get_unavailable_data() existing_data = ProcessedCoordinatorData( - current_weather=CurrentWeatherData(), - daily_forecast=[], - hourly_forecast=[], - animation=RadarAnimationData(hint="This will remain unchanged"), - warnings=[], - pollen={'foo': 'bar'} - ) + current_weather=CurrentWeatherData(), + daily_forecast=[], + hourly_forecast=[], + animation=RadarAnimationData(hint="This will remain unchanged"), + warnings=[], + pollen={'foo': 'bar'} + ) coordinator.data = existing_data result = await coordinator._async_update_data() diff --git a/tests/test_warning_sensors.py b/tests/test_warning_sensors.py index 71f38e6..c0a07a0 100644 --- a/tests/test_warning_sensors.py +++ b/tests/test_warning_sensors.py @@ -6,6 +6,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.irm_kmi import IrmKmiCoordinator from custom_components.irm_kmi.binary_sensor import IrmKmiWarning +from custom_components.irm_kmi.const import CONF_LANGUAGE_OVERRIDE from custom_components.irm_kmi.sensor import IrmKmiNextWarning from tests.conftest import get_api_data @@ -65,6 +66,8 @@ async def test_next_warning_when_data_available( mock_config_entry: MockConfigEntry ) -> None: api_data = get_api_data("be_forecast_warning.json") + mock_config_entry.data = mock_config_entry.data | {CONF_LANGUAGE_OVERRIDE: 'de'} + coordinator = IrmKmiCoordinator(hass, mock_config_entry) result = coordinator.warnings_from_data(api_data.get('for', {}).get('warning')) @@ -76,7 +79,7 @@ async def test_next_warning_when_data_available( assert warning.state == "2024-01-12T06:00:00+00:00" assert len(warning.extra_state_attributes['next_warnings']) == 2 - assert warning.extra_state_attributes['next_warnings_friendly_names'] == "Fog, Ice or snow" + assert warning.extra_state_attributes['next_warnings_friendly_names'] == "Nebel, Glätte" @freeze_time(datetime.fromisoformat('2024-01-12T07:30:00+01:00'))