mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Add coordinator error handling for Peblar Rocksolid EV Chargers (#133809)
This commit is contained in:
parent
83f5ca5a30
commit
ed7da35de4
@ -2,12 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from peblar import (
|
||||
Peblar,
|
||||
PeblarApi,
|
||||
PeblarAuthenticationError,
|
||||
PeblarConnectionError,
|
||||
PeblarError,
|
||||
PeblarEVInterface,
|
||||
PeblarMeter,
|
||||
@ -16,12 +20,13 @@ from peblar import (
|
||||
PeblarVersions,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from tests.components.peblar.conftest import PeblarSystemInformation
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
@ -59,6 +64,49 @@ class PeblarData:
|
||||
system: PeblarSystem
|
||||
|
||||
|
||||
def _coordinator_exception_handler[
|
||||
_DataUpdateCoordinatorT: PeblarDataUpdateCoordinator
|
||||
| PeblarVersionDataUpdateCoordinator
|
||||
| PeblarUserConfigurationDataUpdateCoordinator,
|
||||
**_P,
|
||||
](
|
||||
func: Callable[Concatenate[_DataUpdateCoordinatorT, _P], Coroutine[Any, Any, Any]],
|
||||
) -> Callable[Concatenate[_DataUpdateCoordinatorT, _P], Coroutine[Any, Any, Any]]:
|
||||
"""Handle exceptions within the update handler of a coordinator."""
|
||||
|
||||
async def handler(
|
||||
self: _DataUpdateCoordinatorT, *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> Any:
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except PeblarAuthenticationError as error:
|
||||
if self.config_entry and self.config_entry.state is ConfigEntryState.LOADED:
|
||||
# This is not the first refresh, so let's reload
|
||||
# the config entry to ensure we trigger a re-authentication
|
||||
# flow (or recover in case of API token changes).
|
||||
self.hass.config_entries.async_schedule_reload(
|
||||
self.config_entry.entry_id
|
||||
)
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_error",
|
||||
) from error
|
||||
except PeblarConnectionError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
translation_placeholders={"error": str(error)},
|
||||
) from error
|
||||
except PeblarError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unknown_error",
|
||||
translation_placeholders={"error": str(error)},
|
||||
) from error
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
class PeblarVersionDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[PeblarVersionInformation]
|
||||
):
|
||||
@ -77,15 +125,13 @@ class PeblarVersionDataUpdateCoordinator(
|
||||
update_interval=timedelta(hours=2),
|
||||
)
|
||||
|
||||
@_coordinator_exception_handler
|
||||
async def _async_update_data(self) -> PeblarVersionInformation:
|
||||
"""Fetch data from the Peblar device."""
|
||||
try:
|
||||
return PeblarVersionInformation(
|
||||
current=await self.peblar.current_versions(),
|
||||
available=await self.peblar.available_versions(),
|
||||
)
|
||||
except PeblarError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
return PeblarVersionInformation(
|
||||
current=await self.peblar.current_versions(),
|
||||
available=await self.peblar.available_versions(),
|
||||
)
|
||||
|
||||
|
||||
class PeblarDataUpdateCoordinator(DataUpdateCoordinator[PeblarData]):
|
||||
@ -104,16 +150,14 @@ class PeblarDataUpdateCoordinator(DataUpdateCoordinator[PeblarData]):
|
||||
update_interval=timedelta(seconds=10),
|
||||
)
|
||||
|
||||
@_coordinator_exception_handler
|
||||
async def _async_update_data(self) -> PeblarData:
|
||||
"""Fetch data from the Peblar device."""
|
||||
try:
|
||||
return PeblarData(
|
||||
ev=await self.api.ev_interface(),
|
||||
meter=await self.api.meter(),
|
||||
system=await self.api.system(),
|
||||
)
|
||||
except PeblarError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
return PeblarData(
|
||||
ev=await self.api.ev_interface(),
|
||||
meter=await self.api.meter(),
|
||||
system=await self.api.system(),
|
||||
)
|
||||
|
||||
|
||||
class PeblarUserConfigurationDataUpdateCoordinator(
|
||||
@ -134,9 +178,7 @@ class PeblarUserConfigurationDataUpdateCoordinator(
|
||||
update_interval=timedelta(minutes=5),
|
||||
)
|
||||
|
||||
@_coordinator_exception_handler
|
||||
async def _async_update_data(self) -> PeblarUserConfiguration:
|
||||
"""Fetch data from the Peblar device."""
|
||||
try:
|
||||
return await self.peblar.user_configuration()
|
||||
except PeblarError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
return await self.peblar.user_configuration()
|
||||
|
119
tests/components/peblar/test_coordinator.py
Normal file
119
tests/components/peblar/test_coordinator.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Tests for the Peblar coordinators."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from peblar import PeblarAuthenticationError, PeblarConnectionError, PeblarError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.peblar.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True),
|
||||
pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error", "log_message"),
|
||||
[
|
||||
(
|
||||
PeblarConnectionError("Could not connect"),
|
||||
(
|
||||
"An error occurred while communicating with the Peblar device: "
|
||||
"Could not connect"
|
||||
),
|
||||
),
|
||||
(
|
||||
PeblarError("Unknown error"),
|
||||
(
|
||||
"An unknown error occurred while communicating "
|
||||
"with the Peblar device: Unknown error"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_coordinator_error_handler(
|
||||
hass: HomeAssistant,
|
||||
mock_peblar: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
error: Exception,
|
||||
log_message: str,
|
||||
) -> None:
|
||||
"""Test the coordinators."""
|
||||
entity_id = "sensor.peblar_ev_charger_power"
|
||||
|
||||
# Ensure we are set up and the coordinator is working.
|
||||
# Confirming this through a sensor entity, that is available.
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# Mock an error in the coordinator.
|
||||
mock_peblar.rest_api.return_value.meter.side_effect = error
|
||||
freezer.tick(timedelta(seconds=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Ensure the sensor entity is now unavailable.
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Ensure the error is logged
|
||||
assert log_message in caplog.text
|
||||
|
||||
# Recover
|
||||
mock_peblar.rest_api.return_value.meter.side_effect = None
|
||||
freezer.tick(timedelta(seconds=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Ensure the sensor entity is now available.
|
||||
assert (state := hass.states.get("sensor.peblar_ev_charger_power"))
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_coordinator_error_handler_authentication_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_peblar: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the coordinator error handler with an authentication error."""
|
||||
|
||||
# Ensure the sensor entity is now available.
|
||||
assert (state := hass.states.get("sensor.peblar_ev_charger_power"))
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# Mock an authentication in the coordinator
|
||||
mock_peblar.rest_api.return_value.meter.side_effect = PeblarAuthenticationError(
|
||||
"Authentication error"
|
||||
)
|
||||
mock_peblar.login.side_effect = PeblarAuthenticationError("Authentication error")
|
||||
freezer.tick(timedelta(seconds=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Ensure the sensor entity is now unavailable.
|
||||
assert (state := hass.states.get("sensor.peblar_ev_charger_power"))
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Ensure we have triggered a reauthentication flow
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow["step_id"] == "reauth_confirm"
|
||||
assert flow["handler"] == DOMAIN
|
||||
|
||||
assert "context" in flow
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == mock_config_entry.entry_id
|
Loading…
x
Reference in New Issue
Block a user