Skip to content

Commit

Permalink
Start adding repairs and better error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jdejaegh committed Jan 7, 2024
1 parent 0eb96a6 commit 90d4dd3
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 63 deletions.
2 changes: 1 addition & 1 deletion custom_components/irm_kmi/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __init__(self,
@property
def frame_interval(self) -> float:
"""Return the interval between frames of the mjpeg stream."""
return 20
return 1

def camera_image(self,
width: int | None = None,
Expand Down
52 changes: 39 additions & 13 deletions custom_components/irm_kmi/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
"""Config flow to set up IRM KMI integration via the UI."""
import logging

import async_timeout
import voluptuous as vol
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
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.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (EntitySelector,
EntitySelectorConfig,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode)

from .api import IrmKmiApiClient
from .const import (CONF_DARK_MODE, 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)
OPTION_STYLE_STD, OUT_OF_BENELUX)
from .utils import get_config_value

_LOGGER = logging.getLogger(__name__)
Expand All @@ -34,23 +37,46 @@ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:

async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
"""Define the user step of the configuration flow."""
errors = {}

if user_input is not None:
_LOGGER.debug(f"Provided config user is: {user_input}")

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]},
)
if (zone := self.hass.states.get(user_input[CONF_ZONE])) is None:
errors[CONF_ZONE] = 'Zone does not exist'

# Check if zone is in Benelux
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['base'] = "Could not get data from the API"

if api_data.get('cityName', None) in OUT_OF_BENELUX:
errors[CONF_ZONE] = 'Zone is outside 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(
step_id="user",
errors=errors,
data_schema=vol.Schema({
vol.Required(CONF_ZONE):
EntitySelector(EntitySelectorConfig(domain=ZONE_DOMAIN)),
Expand Down
5 changes: 5 additions & 0 deletions custom_components/irm_kmi/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
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
IRM_KMI_TO_HA_CONDITION_MAP: Final = {
(0, 'd'): ATTR_CONDITION_SUNNY,
Expand Down
35 changes: 21 additions & 14 deletions custom_components/irm_kmi/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE
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.update_coordinator import (DataUpdateCoordinator,
UpdateFailed)

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 (LANGS, OPTION_STYLE_SATELLITE, OUT_OF_BENELUX,
STYLE_TO_PARAM_MAP)
Expand All @@ -39,7 +39,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
# Name of the data. For logging purposes.
name="IRM KMI weather",
# 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._zone = get_config_value(entry, CONF_ZONE)
Expand Down Expand Up @@ -70,17 +70,24 @@ async def _async_update_data(self) -> ProcessedCoordinatorData:
raise UpdateFailed(f"Error communicating with API: {err}")

if api_data.get('cityName', None) in OUT_OF_BENELUX:
if self.data is None:
error_text = f"Zone '{self._zone}' is out of Benelux and forecast is only available in the Benelux"
_LOGGER.error(error_text)
raise ConfigEntryError(error_text)
else:
# TODO create a repair when this triggers
_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 "
f"this")
disable_from_config(self.hass, self._config_entry)
return ProcessedCoordinatorData()
# TODO create a repair when this triggers
_LOGGER.info(f"Config state: {self._config_entry.state}")
_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 "
f"this")
disable_from_config(self.hass, self._config_entry)

issue_registry.async_create_issue(
self.hass,
DOMAIN,
"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)

Expand Down
93 changes: 93 additions & 0 deletions custom_components/irm_kmi/repairs.py
Original file line number Diff line number Diff line change
@@ -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,
) -> RepairsFlow:
"""Create flow."""
return OutOfBeneluxRepairFlow(data)
23 changes: 21 additions & 2 deletions custom_components/irm_kmi/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
"daily_in_deprecated_forecast": "Use for daily 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": {
Expand All @@ -47,8 +53,21 @@
},
"issues": {
"zone_moved": {
"title": "Zone moved",
"description": "Hey!"
"title": "{zone} is outside of Benelux",
"fix_flow": {
"step": {
"confirm": {
"title": "Title for the confirm step",
"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"
}
}
}
}
}
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
aiohttp==3.9.1
async_timeout==4.0.3
homeassistant==2023.12.3
homeassistant==2024.1.2
voluptuous==0.13.1
pytz==2023.3.post1
svgwrite==1.4.3
11 changes: 6 additions & 5 deletions requirements_tests.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
homeassistant==2023.12.3
pytest==7.4.3
pytest_homeassistant_custom_component==0.13.85
freezegun==1.2.2
Pillow==10.1.0
homeassistant==2024.1.2
pytest
pytest_homeassistant_custom_component @ git+https://github.com/MatthewFlamm/pytest-homeassistant-custom-component
freezegun
Pillow==10.1.0
isort
27 changes: 26 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from pytest_homeassistant_custom_component.common import (MockConfigEntry,
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 (
CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, DOMAIN,
OPTION_DEPRECATED_FORECAST_NOT_USED, OPTION_STYLE_STD)
Expand Down Expand Up @@ -60,6 +61,30 @@ def mock_setup_entry() -> Generator[None, None, None]:
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_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
"""Return a mocked IrmKmi api client."""
Expand Down
Loading

0 comments on commit 90d4dd3

Please sign in to comment.