mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 00:37:13 +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 __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Any, Concatenate
|
||||||
|
|
||||||
from peblar import (
|
from peblar import (
|
||||||
Peblar,
|
Peblar,
|
||||||
PeblarApi,
|
PeblarApi,
|
||||||
|
PeblarAuthenticationError,
|
||||||
|
PeblarConnectionError,
|
||||||
PeblarError,
|
PeblarError,
|
||||||
PeblarEVInterface,
|
PeblarEVInterface,
|
||||||
PeblarMeter,
|
PeblarMeter,
|
||||||
@ -16,12 +20,13 @@ from peblar import (
|
|||||||
PeblarVersions,
|
PeblarVersions,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
from tests.components.peblar.conftest import PeblarSystemInformation
|
from tests.components.peblar.conftest import PeblarSystemInformation
|
||||||
|
|
||||||
from .const import LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
@ -59,6 +64,49 @@ class PeblarData:
|
|||||||
system: PeblarSystem
|
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(
|
class PeblarVersionDataUpdateCoordinator(
|
||||||
DataUpdateCoordinator[PeblarVersionInformation]
|
DataUpdateCoordinator[PeblarVersionInformation]
|
||||||
):
|
):
|
||||||
@ -77,15 +125,13 @@ class PeblarVersionDataUpdateCoordinator(
|
|||||||
update_interval=timedelta(hours=2),
|
update_interval=timedelta(hours=2),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@_coordinator_exception_handler
|
||||||
async def _async_update_data(self) -> PeblarVersionInformation:
|
async def _async_update_data(self) -> PeblarVersionInformation:
|
||||||
"""Fetch data from the Peblar device."""
|
"""Fetch data from the Peblar device."""
|
||||||
try:
|
|
||||||
return PeblarVersionInformation(
|
return PeblarVersionInformation(
|
||||||
current=await self.peblar.current_versions(),
|
current=await self.peblar.current_versions(),
|
||||||
available=await self.peblar.available_versions(),
|
available=await self.peblar.available_versions(),
|
||||||
)
|
)
|
||||||
except PeblarError as err:
|
|
||||||
raise UpdateFailed(err) from err
|
|
||||||
|
|
||||||
|
|
||||||
class PeblarDataUpdateCoordinator(DataUpdateCoordinator[PeblarData]):
|
class PeblarDataUpdateCoordinator(DataUpdateCoordinator[PeblarData]):
|
||||||
@ -104,16 +150,14 @@ class PeblarDataUpdateCoordinator(DataUpdateCoordinator[PeblarData]):
|
|||||||
update_interval=timedelta(seconds=10),
|
update_interval=timedelta(seconds=10),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@_coordinator_exception_handler
|
||||||
async def _async_update_data(self) -> PeblarData:
|
async def _async_update_data(self) -> PeblarData:
|
||||||
"""Fetch data from the Peblar device."""
|
"""Fetch data from the Peblar device."""
|
||||||
try:
|
|
||||||
return PeblarData(
|
return PeblarData(
|
||||||
ev=await self.api.ev_interface(),
|
ev=await self.api.ev_interface(),
|
||||||
meter=await self.api.meter(),
|
meter=await self.api.meter(),
|
||||||
system=await self.api.system(),
|
system=await self.api.system(),
|
||||||
)
|
)
|
||||||
except PeblarError as err:
|
|
||||||
raise UpdateFailed(err) from err
|
|
||||||
|
|
||||||
|
|
||||||
class PeblarUserConfigurationDataUpdateCoordinator(
|
class PeblarUserConfigurationDataUpdateCoordinator(
|
||||||
@ -134,9 +178,7 @@ class PeblarUserConfigurationDataUpdateCoordinator(
|
|||||||
update_interval=timedelta(minutes=5),
|
update_interval=timedelta(minutes=5),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@_coordinator_exception_handler
|
||||||
async def _async_update_data(self) -> PeblarUserConfiguration:
|
async def _async_update_data(self) -> PeblarUserConfiguration:
|
||||||
"""Fetch data from the Peblar device."""
|
"""Fetch data from the Peblar device."""
|
||||||
try:
|
|
||||||
return await self.peblar.user_configuration()
|
return await self.peblar.user_configuration()
|
||||||
except PeblarError as err:
|
|
||||||
raise UpdateFailed(err) from err
|
|
||||||
|
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