mirror of
https://github.com/jdejaegh/irm-kmi-ha.git
synced 2025-06-27 03:35:56 +02:00
Merge pull request #2 from jdejaegh/error_handling
Improve error handling and add repairs
This commit is contained in:
commit
cf9e361bb1
13 changed files with 511 additions and 78 deletions
|
@ -27,6 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
except ConfigEntryError:
|
except ConfigEntryError:
|
||||||
# This happens when the zone is out of Benelux (no forecast available there)
|
# This happens when the zone is out of Benelux (no forecast available there)
|
||||||
|
# This should be caught by the config flow anyway
|
||||||
return False
|
return False
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
"""Config flow to set up IRM KMI integration via the UI."""
|
"""Config flow to set up IRM KMI integration via the UI."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
|
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
||||||
from homeassistant.const import CONF_ZONE
|
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.selector import (EntitySelector,
|
from homeassistant.helpers.selector import (EntitySelector,
|
||||||
EntitySelectorConfig,
|
EntitySelectorConfig,
|
||||||
SelectSelector,
|
SelectSelector,
|
||||||
SelectSelectorConfig,
|
SelectSelectorConfig,
|
||||||
SelectSelectorMode)
|
SelectSelectorMode)
|
||||||
|
|
||||||
|
from .api import IrmKmiApiClient
|
||||||
from .const import (CONF_DARK_MODE, CONF_STYLE, CONF_STYLE_OPTIONS,
|
from .const import (CONF_DARK_MODE, CONF_STYLE, CONF_STYLE_OPTIONS,
|
||||||
CONF_USE_DEPRECATED_FORECAST,
|
CONF_USE_DEPRECATED_FORECAST,
|
||||||
CONF_USE_DEPRECATED_FORECAST_OPTIONS, CONFIG_FLOW_VERSION,
|
CONF_USE_DEPRECATED_FORECAST_OPTIONS, CONFIG_FLOW_VERSION,
|
||||||
DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED,
|
DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED,
|
||||||
OPTION_STYLE_STD)
|
OPTION_STYLE_STD, OUT_OF_BENELUX)
|
||||||
from .utils import get_config_value
|
from .utils import get_config_value
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -34,23 +37,47 @@ class IrmKmiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
|
async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
|
||||||
"""Define the user step of the configuration flow."""
|
"""Define the user step of the configuration flow."""
|
||||||
if user_input is not None:
|
errors = {}
|
||||||
|
|
||||||
|
if user_input:
|
||||||
_LOGGER.debug(f"Provided config user is: {user_input}")
|
_LOGGER.debug(f"Provided config user is: {user_input}")
|
||||||
|
|
||||||
await self.async_set_unique_id(user_input[CONF_ZONE])
|
if (zone := self.hass.states.get(user_input[CONF_ZONE])) is None:
|
||||||
self._abort_if_unique_id_configured()
|
errors[CONF_ZONE] = 'zone_not_exist'
|
||||||
|
|
||||||
state = self.hass.states.get(user_input[CONF_ZONE])
|
# Check if zone is in Benelux
|
||||||
return self.async_create_entry(
|
if not errors:
|
||||||
title=state.name if state else "IRM KMI",
|
api_data = {}
|
||||||
data={CONF_ZONE: user_input[CONF_ZONE],
|
try:
|
||||||
CONF_STYLE: user_input[CONF_STYLE],
|
async with async_timeout.timeout(10):
|
||||||
CONF_DARK_MODE: user_input[CONF_DARK_MODE],
|
api_data = await IrmKmiApiClient(
|
||||||
CONF_USE_DEPRECATED_FORECAST: user_input[CONF_USE_DEPRECATED_FORECAST]},
|
session=async_get_clientsession(self.hass)).get_forecasts_coord(
|
||||||
)
|
{'lat': zone.attributes[ATTR_LATITUDE],
|
||||||
|
'long': zone.attributes[ATTR_LONGITUDE]}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
errors['base'] = "api_error"
|
||||||
|
|
||||||
|
if api_data.get('cityName', None) in OUT_OF_BENELUX:
|
||||||
|
errors[CONF_ZONE] = 'out_of_benelux'
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
await self.async_set_unique_id(user_input[CONF_ZONE])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
state = self.hass.states.get(user_input[CONF_ZONE])
|
||||||
|
return self.async_create_entry(
|
||||||
|
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_USE_DEPRECATED_FORECAST: user_input[CONF_USE_DEPRECATED_FORECAST]},
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={'zone': user_input.get('zone') if user_input is not None else None},
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
vol.Required(CONF_ZONE):
|
vol.Required(CONF_ZONE):
|
||||||
EntitySelector(EntitySelectorConfig(domain=ZONE_DOMAIN)),
|
EntitySelector(EntitySelectorConfig(domain=ZONE_DOMAIN)),
|
||||||
|
|
|
@ -57,6 +57,11 @@ CONF_USE_DEPRECATED_FORECAST_OPTIONS: Final = [
|
||||||
OPTION_DEPRECATED_FORECAST_HOURLY
|
OPTION_DEPRECATED_FORECAST_HOURLY
|
||||||
]
|
]
|
||||||
|
|
||||||
|
REPAIR_SOLUTION: Final = "repair_solution"
|
||||||
|
REPAIR_OPT_MOVE: Final = "repair_option_move"
|
||||||
|
REPAIR_OPT_DELETE: Final = "repair_option_delete"
|
||||||
|
REPAIR_OPTIONS: Final = [REPAIR_OPT_MOVE, REPAIR_OPT_DELETE]
|
||||||
|
|
||||||
# map ('ww', 'dayNight') tuple from IRM KMI to HA conditions
|
# map ('ww', 'dayNight') tuple from IRM KMI to HA conditions
|
||||||
IRM_KMI_TO_HA_CONDITION_MAP: Final = {
|
IRM_KMI_TO_HA_CONDITION_MAP: Final = {
|
||||||
(0, 'd'): ATTR_CONDITION_SUNNY,
|
(0, 'd'): ATTR_CONDITION_SUNNY,
|
||||||
|
|
|
@ -10,13 +10,13 @@ from homeassistant.components.weather import Forecast
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE
|
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryError
|
from homeassistant.helpers import issue_registry
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.update_coordinator import (DataUpdateCoordinator,
|
from homeassistant.helpers.update_coordinator import (DataUpdateCoordinator,
|
||||||
UpdateFailed)
|
UpdateFailed)
|
||||||
|
|
||||||
from .api import IrmKmiApiClient, IrmKmiApiError
|
from .api import IrmKmiApiClient, IrmKmiApiError
|
||||||
from .const import CONF_DARK_MODE, CONF_STYLE
|
from .const import CONF_DARK_MODE, CONF_STYLE, DOMAIN
|
||||||
from .const import IRM_KMI_TO_HA_CONDITION_MAP as CDT_MAP
|
from .const import IRM_KMI_TO_HA_CONDITION_MAP as CDT_MAP
|
||||||
from .const import (LANGS, OPTION_STYLE_SATELLITE, OUT_OF_BENELUX,
|
from .const import (LANGS, OPTION_STYLE_SATELLITE, OUT_OF_BENELUX,
|
||||||
STYLE_TO_PARAM_MAP)
|
STYLE_TO_PARAM_MAP)
|
||||||
|
@ -39,7 +39,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
|
||||||
# Name of the data. For logging purposes.
|
# Name of the data. For logging purposes.
|
||||||
name="IRM KMI weather",
|
name="IRM KMI weather",
|
||||||
# Polling interval. Will only be polled if there are subscribers.
|
# Polling interval. Will only be polled if there are subscribers.
|
||||||
update_interval=timedelta(minutes=7),
|
update_interval=timedelta(seconds=15),
|
||||||
)
|
)
|
||||||
self._api_client = IrmKmiApiClient(session=async_get_clientsession(hass))
|
self._api_client = IrmKmiApiClient(session=async_get_clientsession(hass))
|
||||||
self._zone = get_config_value(entry, CONF_ZONE)
|
self._zone = get_config_value(entry, CONF_ZONE)
|
||||||
|
@ -70,17 +70,24 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
|
||||||
raise UpdateFailed(f"Error communicating with API: {err}")
|
raise UpdateFailed(f"Error communicating with API: {err}")
|
||||||
|
|
||||||
if api_data.get('cityName', None) in OUT_OF_BENELUX:
|
if api_data.get('cityName', None) in OUT_OF_BENELUX:
|
||||||
if self.data is None:
|
# TODO create a repair when this triggers
|
||||||
error_text = f"Zone '{self._zone}' is out of Benelux and forecast is only available in the Benelux"
|
_LOGGER.info(f"Config state: {self._config_entry.state}")
|
||||||
_LOGGER.error(error_text)
|
_LOGGER.error(f"The zone {self._zone} is now out of Benelux and forecast is only available in Benelux."
|
||||||
raise ConfigEntryError(error_text)
|
f"Associated device is now disabled. Move the zone back in Benelux and re-enable to fix "
|
||||||
else:
|
f"this")
|
||||||
# TODO create a repair when this triggers
|
disable_from_config(self.hass, self._config_entry)
|
||||||
_LOGGER.error(f"The zone {self._zone} is now out of Benelux and forecast is only available in Benelux."
|
|
||||||
f"Associated device is now disabled. Move the zone back in Benelux and re-enable to fix "
|
issue_registry.async_create_issue(
|
||||||
f"this")
|
self.hass,
|
||||||
disable_from_config(self.hass, self._config_entry)
|
DOMAIN,
|
||||||
return ProcessedCoordinatorData()
|
"zone_moved",
|
||||||
|
is_fixable=True,
|
||||||
|
severity=issue_registry.IssueSeverity.ERROR,
|
||||||
|
translation_key='zone_moved',
|
||||||
|
data={'config_entry_id': self._config_entry.entry_id, 'zone': self._zone},
|
||||||
|
translation_placeholders={'zone': self._zone}
|
||||||
|
)
|
||||||
|
return ProcessedCoordinatorData()
|
||||||
|
|
||||||
return await self.process_api_data(api_data)
|
return await self.process_api_data(api_data)
|
||||||
|
|
||||||
|
|
93
custom_components/irm_kmi/repairs.py
Normal file
93
custom_components/irm_kmi/repairs.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.repairs import RepairsFlow
|
||||||
|
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||||
|
|
||||||
|
from custom_components.irm_kmi import async_reload_entry
|
||||||
|
from custom_components.irm_kmi.api import IrmKmiApiClient
|
||||||
|
from custom_components.irm_kmi.const import (OUT_OF_BENELUX, REPAIR_OPT_DELETE,
|
||||||
|
REPAIR_OPT_MOVE, REPAIR_OPTIONS,
|
||||||
|
REPAIR_SOLUTION)
|
||||||
|
from custom_components.irm_kmi.utils import modify_from_config
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OutOfBeneluxRepairFlow(RepairsFlow):
|
||||||
|
"""Handler for an issue fixing flow."""
|
||||||
|
|
||||||
|
def __init__(self, data: dict):
|
||||||
|
self._data: dict = data
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, str] | None = None
|
||||||
|
) -> data_entry_flow.FlowResult:
|
||||||
|
"""Handle the first step of a fix flow."""
|
||||||
|
|
||||||
|
return await (self.async_step_confirm())
|
||||||
|
|
||||||
|
async def async_step_confirm(
|
||||||
|
self, user_input: dict[str, str] | None = None
|
||||||
|
) -> data_entry_flow.FlowResult:
|
||||||
|
"""Handle the confirm step of a fix flow."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
config_entry = self.hass.config_entries.async_get_entry(self._data['config_entry_id'])
|
||||||
|
_LOGGER.info(f"State of config entry: {config_entry.state}")
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
if user_input[REPAIR_SOLUTION] == REPAIR_OPT_MOVE:
|
||||||
|
if (zone := self.hass.states.get(self._data['zone'])) is None:
|
||||||
|
errors[REPAIR_SOLUTION] = "zone_not_exist"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
api_data = {}
|
||||||
|
try:
|
||||||
|
async with async_timeout.timeout(10):
|
||||||
|
api_data = await IrmKmiApiClient(
|
||||||
|
session=async_get_clientsession(self.hass)).get_forecasts_coord(
|
||||||
|
{'lat': zone.attributes[ATTR_LATITUDE],
|
||||||
|
'long': zone.attributes[ATTR_LONGITUDE]}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
errors[REPAIR_SOLUTION] = 'api_error'
|
||||||
|
|
||||||
|
if api_data.get('cityName', None) in OUT_OF_BENELUX:
|
||||||
|
errors[REPAIR_SOLUTION] = 'out_of_benelux'
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
modify_from_config(self.hass, self._data['config_entry_id'], enable=True)
|
||||||
|
await async_reload_entry(self.hass, config_entry)
|
||||||
|
|
||||||
|
elif user_input[REPAIR_SOLUTION] == REPAIR_OPT_DELETE:
|
||||||
|
await self.hass.config_entries.async_remove(self._data['config_entry_id'])
|
||||||
|
else:
|
||||||
|
errors[REPAIR_SOLUTION] = "invalid_choice"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
return self.async_create_entry(title="", data={})
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="confirm",
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={'zone': self._data['zone']},
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(REPAIR_SOLUTION, default=REPAIR_OPT_MOVE):
|
||||||
|
SelectSelector(SelectSelectorConfig(options=REPAIR_OPTIONS,
|
||||||
|
translation_key=REPAIR_SOLUTION)),
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
async def async_create_fix_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
issue_id: str,
|
||||||
|
data: dict[str, str | int | float | None] | None,
|
||||||
|
) -> OutOfBeneluxRepairFlow:
|
||||||
|
"""Create flow."""
|
||||||
|
return OutOfBeneluxRepairFlow(data)
|
|
@ -14,6 +14,11 @@
|
||||||
"use_deprecated_forecast_attribute": "Use the deprecated forecat attribute"
|
"use_deprecated_forecast_attribute": "Use the deprecated forecat attribute"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"out_of_benelux": "{zone} is out of Benelux. Pick a zone in Benelux.",
|
||||||
|
"api_error": "Could not get data from the API",
|
||||||
|
"zone_not_exist": "{zone} does not exist"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
|
@ -31,6 +36,12 @@
|
||||||
"daily_in_deprecated_forecast": "Use for daily forecast",
|
"daily_in_deprecated_forecast": "Use for daily forecast",
|
||||||
"hourly_in_use_deprecated_forecast": "Use for hourly forecast"
|
"hourly_in_use_deprecated_forecast": "Use for hourly forecast"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"repair_solution": {
|
||||||
|
"options": {
|
||||||
|
"repair_option_move": "I moved the zone in Benelux",
|
||||||
|
"repair_option_delete": "Delete that config entry"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
@ -47,8 +58,21 @@
|
||||||
},
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"zone_moved": {
|
"zone_moved": {
|
||||||
"title": "Zone moved",
|
"title": "{zone} is outside of Benelux",
|
||||||
"description": "Hey!"
|
"fix_flow": {
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"title": "Repair: {zone} is outside of Benelux",
|
||||||
|
"description": "This integration can only get data for location in the Benelux. Move the zone or delete this configuration entry."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"out_of_benelux": "{zone} is out of Benelux. Move it inside Benelux first.",
|
||||||
|
"api_error": "Could not get data from the API",
|
||||||
|
"zone_not_exist": "{zone} does not exist",
|
||||||
|
"invalid_choice": "The choice is not valid"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
aiohttp==3.9.1
|
aiohttp==3.9.1
|
||||||
async_timeout==4.0.3
|
async_timeout==4.0.3
|
||||||
homeassistant==2023.12.3
|
homeassistant==2024.1.2
|
||||||
voluptuous==0.13.1
|
voluptuous==0.13.1
|
||||||
pytz==2023.3.post1
|
pytz==2023.3.post1
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
|
@ -1,5 +1,6 @@
|
||||||
homeassistant==2023.12.3
|
homeassistant==2024.1.2
|
||||||
pytest==7.4.3
|
pytest
|
||||||
pytest_homeassistant_custom_component==0.13.85
|
pytest_homeassistant_custom_component @ git+https://github.com/MatthewFlamm/pytest-homeassistant-custom-component
|
||||||
freezegun==1.2.2
|
freezegun
|
||||||
Pillow==10.1.0
|
Pillow==10.1.0
|
||||||
|
isort
|
|
@ -1,6 +1,5 @@
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
testpaths = tests
|
testpaths = tests
|
||||||
norecursedirs = .git
|
norecursedirs = .git
|
||||||
addopts =
|
addopts = -s -v
|
||||||
--cov=custom_components
|
|
||||||
asyncio_mode = auto
|
asyncio_mode = auto
|
|
@ -10,7 +10,8 @@ from homeassistant.const import CONF_ZONE
|
||||||
from pytest_homeassistant_custom_component.common import (MockConfigEntry,
|
from pytest_homeassistant_custom_component.common import (MockConfigEntry,
|
||||||
load_fixture)
|
load_fixture)
|
||||||
|
|
||||||
from custom_components.irm_kmi.api import IrmKmiApiParametersError
|
from custom_components.irm_kmi.api import (IrmKmiApiError,
|
||||||
|
IrmKmiApiParametersError)
|
||||||
from custom_components.irm_kmi.const import (
|
from custom_components.irm_kmi.const import (
|
||||||
CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, DOMAIN,
|
CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, DOMAIN,
|
||||||
OPTION_DEPRECATED_FORECAST_NOT_USED, OPTION_STYLE_STD)
|
OPTION_DEPRECATED_FORECAST_NOT_USED, OPTION_STYLE_STD)
|
||||||
|
@ -60,6 +61,38 @@ def mock_setup_entry() -> Generator[None, None, None]:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_get_forecast_in_benelux():
|
||||||
|
"""Mock a call to IrmKmiApiClient.get_forecasts_coord() so that it returns something valid and in the Benelux"""
|
||||||
|
with patch("custom_components.irm_kmi.config_flow.IrmKmiApiClient.get_forecasts_coord",
|
||||||
|
return_value={'cityName': 'Brussels'}):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_get_forecast_out_benelux():
|
||||||
|
"""Mock a call to IrmKmiApiClient.get_forecasts_coord() so that it returns something outside Benelux"""
|
||||||
|
with patch("custom_components.irm_kmi.config_flow.IrmKmiApiClient.get_forecasts_coord",
|
||||||
|
return_value={'cityName': "Outside the Benelux (Brussels)"}):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_get_forecast_api_error():
|
||||||
|
"""Mock a call to IrmKmiApiClient.get_forecasts_coord() so that it raises an error"""
|
||||||
|
with patch("custom_components.irm_kmi.config_flow.IrmKmiApiClient.get_forecasts_coord",
|
||||||
|
side_effet=IrmKmiApiError):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_get_forecast_api_error_repair():
|
||||||
|
"""Mock a call to IrmKmiApiClient.get_forecasts_coord() so that it raises an error"""
|
||||||
|
with patch("custom_components.irm_kmi.repairs.IrmKmiApiClient.get_forecasts_coord",
|
||||||
|
side_effet=IrmKmiApiError):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
||||||
"""Return a mocked IrmKmi api client."""
|
"""Return a mocked IrmKmi api client."""
|
||||||
|
@ -75,7 +108,7 @@ def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMoc
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def mock_irm_kmi_api_out_benelux(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
def mock_irm_kmi_api_coordinator_out_benelux(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
||||||
"""Return a mocked IrmKmi api client."""
|
"""Return a mocked IrmKmi api client."""
|
||||||
fixture: str = "forecast_out_of_benelux.json"
|
fixture: str = "forecast_out_of_benelux.json"
|
||||||
|
|
||||||
|
@ -88,6 +121,34 @@ def mock_irm_kmi_api_out_benelux(request: pytest.FixtureRequest) -> Generator[No
|
||||||
yield irm_kmi
|
yield irm_kmi
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def mock_irm_kmi_api_repair_in_benelux(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
||||||
|
"""Return a mocked IrmKmi api client."""
|
||||||
|
fixture: str = "forecast.json"
|
||||||
|
|
||||||
|
forecast = json.loads(load_fixture(fixture))
|
||||||
|
with patch(
|
||||||
|
"custom_components.irm_kmi.repairs.IrmKmiApiClient", autospec=True
|
||||||
|
) as irm_kmi_api_mock:
|
||||||
|
irm_kmi = irm_kmi_api_mock.return_value
|
||||||
|
irm_kmi.get_forecasts_coord.return_value = forecast
|
||||||
|
yield irm_kmi
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def mock_irm_kmi_api_repair_out_of_benelux(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
||||||
|
"""Return a mocked IrmKmi api client."""
|
||||||
|
fixture: str = "forecast_out_of_benelux.json"
|
||||||
|
|
||||||
|
forecast = json.loads(load_fixture(fixture))
|
||||||
|
with patch(
|
||||||
|
"custom_components.irm_kmi.repairs.IrmKmiApiClient", autospec=True
|
||||||
|
) as irm_kmi_api_mock:
|
||||||
|
irm_kmi = irm_kmi_api_mock.return_value
|
||||||
|
irm_kmi.get_forecasts_coord.return_value = forecast
|
||||||
|
yield irm_kmi
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def mock_exception_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
def mock_exception_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
||||||
"""Return a mocked IrmKmi api client."""
|
"""Return a mocked IrmKmi api client."""
|
||||||
|
@ -99,18 +160,6 @@ def mock_exception_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None
|
||||||
yield irm_kmi
|
yield irm_kmi
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def mock_image_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
|
||||||
"""Return a mocked IrmKmi api client."""
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"custom_components.irm_kmi.coordinator.IrmKmiApiClient", autospec=True
|
|
||||||
) as irm_kmi_api_mock:
|
|
||||||
irm_kmi = irm_kmi_api_mock.return_value
|
|
||||||
irm_kmi.get_image.side_effect = patched
|
|
||||||
yield irm_kmi
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def mock_image_and_nl_forecast_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
def mock_image_and_nl_forecast_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
||||||
"""Return a mocked IrmKmi api client."""
|
"""Return a mocked IrmKmi api client."""
|
||||||
|
|
|
@ -13,12 +13,13 @@ from custom_components.irm_kmi import async_migrate_entry
|
||||||
from custom_components.irm_kmi.const import (
|
from custom_components.irm_kmi.const import (
|
||||||
CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST,
|
CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST,
|
||||||
CONFIG_FLOW_VERSION, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED,
|
CONFIG_FLOW_VERSION, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED,
|
||||||
OPTION_STYLE_STD)
|
OPTION_STYLE_SATELLITE, OPTION_STYLE_STD)
|
||||||
|
|
||||||
|
|
||||||
async def test_full_user_flow(
|
async def test_full_user_flow(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_setup_entry: MagicMock,
|
mock_setup_entry: MagicMock,
|
||||||
|
mock_get_forecast_in_benelux: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the full user configuration flow."""
|
"""Test the full user configuration flow."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
@ -43,6 +44,95 @@ async def test_full_user_flow(
|
||||||
CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED}
|
CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_out_benelux_zone(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
mock_get_forecast_out_benelux: MagicMock
|
||||||
|
) -> None:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_ZONE: ENTITY_ID_HOME,
|
||||||
|
CONF_STYLE: OPTION_STYLE_STD,
|
||||||
|
CONF_DARK_MODE: False},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2.get("type") == FlowResultType.FORM
|
||||||
|
assert result2.get("step_id") == "user"
|
||||||
|
assert CONF_ZONE in result2.get('errors')
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_with_api_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
mock_get_forecast_api_error: MagicMock
|
||||||
|
) -> None:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_ZONE: ENTITY_ID_HOME,
|
||||||
|
CONF_STYLE: OPTION_STYLE_STD,
|
||||||
|
CONF_DARK_MODE: False},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2.get("type") == FlowResultType.FORM
|
||||||
|
assert result2.get("step_id") == "user"
|
||||||
|
assert 'base' in result2.get('errors')
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_unknown_zone(hass: HomeAssistant) -> None:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_ZONE: "zone.what",
|
||||||
|
CONF_STYLE: OPTION_STYLE_STD,
|
||||||
|
CONF_DARK_MODE: False},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2.get("type") == FlowResultType.FORM
|
||||||
|
assert result2.get("step_id") == "user"
|
||||||
|
assert CONF_ZONE in result2.get('errors')
|
||||||
|
|
||||||
|
|
||||||
|
async def test_option_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert not mock_config_entry.options
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id, data=None)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
CONF_STYLE: OPTION_STYLE_SATELLITE,
|
||||||
|
CONF_DARK_MODE: True,
|
||||||
|
CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_STYLE: OPTION_STYLE_SATELLITE,
|
||||||
|
CONF_DARK_MODE: True,
|
||||||
|
CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_config_entry_migration(hass: HomeAssistant) -> None:
|
async def test_config_entry_migration(hass: HomeAssistant) -> None:
|
||||||
"""Ensure that config entry migration takes the configuration to the latest version"""
|
"""Ensure that config entry migration takes the configuration to the latest version"""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
|
|
|
@ -74,29 +74,3 @@ async def test_config_entry_zone_removed(
|
||||||
|
|
||||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
assert "Zone 'zone.castle' not found" in caplog.text
|
assert "Zone 'zone.castle' not found" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
async def test_zone_out_of_benelux(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
caplog: pytest.LogCaptureFixture,
|
|
||||||
mock_irm_kmi_api_out_benelux: AsyncMock
|
|
||||||
) -> None:
|
|
||||||
"""Test the IRM KMI when configuration zone is out of Benelux"""
|
|
||||||
mock_config_entry = MockConfigEntry(
|
|
||||||
title="London",
|
|
||||||
domain=DOMAIN,
|
|
||||||
data={CONF_ZONE: "zone.london"},
|
|
||||||
unique_id="zone.london",
|
|
||||||
)
|
|
||||||
hass.states.async_set(
|
|
||||||
"zone.london",
|
|
||||||
0,
|
|
||||||
{"latitude": 51.5072, "longitude": 0.1276},
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_config_entry.add_to_hass(hass)
|
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
|
||||||
assert "Zone 'zone.london' is out of Benelux" in caplog.text
|
|
||||||
|
|
163
tests/test_repairs.py
Normal file
163
tests/test_repairs.py
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
import logging
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.helpers import issue_registry
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
|
from custom_components.irm_kmi import DOMAIN, IrmKmiCoordinator
|
||||||
|
from custom_components.irm_kmi.const import (REPAIR_OPT_DELETE,
|
||||||
|
REPAIR_OPT_MOVE, REPAIR_SOLUTION)
|
||||||
|
from custom_components.irm_kmi.repairs import (OutOfBeneluxRepairFlow,
|
||||||
|
async_create_fix_flow)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_repair_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry
|
||||||
|
) -> OutOfBeneluxRepairFlow:
|
||||||
|
hass.states.async_set(
|
||||||
|
"zone.home",
|
||||||
|
0,
|
||||||
|
{"latitude": 50.738681639, "longitude": 4.054077148},
|
||||||
|
)
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
coordinator = IrmKmiCoordinator(hass, mock_config_entry)
|
||||||
|
await coordinator._async_update_data()
|
||||||
|
ir = issue_registry.async_get(hass)
|
||||||
|
issue = ir.async_get_issue(DOMAIN, "zone_moved")
|
||||||
|
repair_flow = await async_create_fix_flow(hass, issue.issue_id, issue.data)
|
||||||
|
repair_flow.hass = hass
|
||||||
|
return repair_flow
|
||||||
|
|
||||||
|
|
||||||
|
async def test_repair_triggers_when_out_of_benelux(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_irm_kmi_api_coordinator_out_benelux: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
hass.states.async_set(
|
||||||
|
"zone.home",
|
||||||
|
0,
|
||||||
|
{"latitude": 50.738681639, "longitude": 4.054077148},
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
coordinator = IrmKmiCoordinator(hass, mock_config_entry)
|
||||||
|
await coordinator._async_update_data()
|
||||||
|
|
||||||
|
ir = issue_registry.async_get(hass)
|
||||||
|
|
||||||
|
issue = ir.async_get_issue(DOMAIN, "zone_moved")
|
||||||
|
|
||||||
|
assert issue is not None
|
||||||
|
assert issue.data == {'config_entry_id': mock_config_entry.entry_id, 'zone': "zone.home"}
|
||||||
|
assert issue.translation_key == "zone_moved"
|
||||||
|
assert issue.is_fixable
|
||||||
|
assert issue.translation_placeholders == {'zone': "zone.home"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_repair_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_irm_kmi_api_coordinator_out_benelux: MagicMock,
|
||||||
|
mock_irm_kmi_api_repair_in_benelux: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
repair_flow = await get_repair_flow(hass, mock_config_entry)
|
||||||
|
result = await repair_flow.async_step_init()
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
assert result['errors'] == {}
|
||||||
|
assert result['description_placeholders'] == {"zone": "zone.home"}
|
||||||
|
|
||||||
|
user_input = {REPAIR_SOLUTION: REPAIR_OPT_MOVE}
|
||||||
|
|
||||||
|
result = await repair_flow.async_step_confirm(user_input)
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result['title'] == ""
|
||||||
|
assert result['data'] == {}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_repair_flow_invalid_choice(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_irm_kmi_api_coordinator_out_benelux: MagicMock,
|
||||||
|
mock_irm_kmi_api_repair_in_benelux: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
repair_flow = await get_repair_flow(hass, mock_config_entry)
|
||||||
|
result = await repair_flow.async_step_init()
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
user_input = {REPAIR_SOLUTION: "whut?"}
|
||||||
|
|
||||||
|
result = await repair_flow.async_step_confirm(user_input)
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
assert REPAIR_SOLUTION in result['errors']
|
||||||
|
assert result['errors'][REPAIR_SOLUTION] == 'invalid_choice'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_repair_flow_api_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_irm_kmi_api_coordinator_out_benelux: MagicMock,
|
||||||
|
mock_get_forecast_api_error_repair: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
repair_flow = await get_repair_flow(hass, mock_config_entry)
|
||||||
|
result = await repair_flow.async_step_init()
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
user_input = {REPAIR_SOLUTION: REPAIR_OPT_MOVE}
|
||||||
|
|
||||||
|
result = await repair_flow.async_step_confirm(user_input)
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
assert REPAIR_SOLUTION in result['errors']
|
||||||
|
assert result['errors'][REPAIR_SOLUTION] == 'api_error'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_repair_flow_out_of_benelux(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_irm_kmi_api_coordinator_out_benelux: MagicMock,
|
||||||
|
mock_irm_kmi_api_repair_out_of_benelux: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
repair_flow = await get_repair_flow(hass, mock_config_entry)
|
||||||
|
result = await repair_flow.async_step_init()
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
user_input = {REPAIR_SOLUTION: REPAIR_OPT_MOVE}
|
||||||
|
|
||||||
|
result = await repair_flow.async_step_confirm(user_input)
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
assert REPAIR_SOLUTION in result['errors']
|
||||||
|
assert result['errors'][REPAIR_SOLUTION] == 'out_of_benelux'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_repair_flow_delete_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_irm_kmi_api_coordinator_out_benelux: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
repair_flow = await get_repair_flow(hass, mock_config_entry)
|
||||||
|
result = await repair_flow.async_step_init()
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert hass.config_entries.async_entries(DOMAIN)[0].entry_id == mock_config_entry.entry_id
|
||||||
|
|
||||||
|
user_input = {REPAIR_SOLUTION: REPAIR_OPT_DELETE}
|
||||||
|
result = await repair_flow.async_step_confirm(user_input)
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result['title'] == ""
|
||||||
|
assert result['data'] == {}
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
|
Loading…
Add table
Reference in a new issue