Centralize exception handling in Plugwise (#82694)

This commit is contained in:
Franck Nijhof 2022-11-25 15:56:58 +01:00 committed by GitHub
parent fd3e996a1e
commit 13458dc722
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 48 additions and 101 deletions

View File

@ -12,11 +12,15 @@ from plugwise.exceptions import (
UnsupportedDeviceError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_USERNAME, DOMAIN, LOGGER
class PlugwiseData(NamedTuple):
@ -29,15 +33,15 @@ class PlugwiseData(NamedTuple):
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
"""Class to manage fetching Plugwise data from single endpoint."""
def __init__(self, hass: HomeAssistant, api: Smile) -> None:
_connected: bool = False
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
LOGGER,
name=api.smile_name or DOMAIN,
update_interval=DEFAULT_SCAN_INTERVAL.get(
str(api.smile_type), timedelta(seconds=60)
),
name=DOMAIN,
update_interval=timedelta(seconds=60),
# Don't refresh immediately, give the device time to process
# the change in state before we query it.
request_refresh_debouncer=Debouncer(
@ -47,22 +51,41 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
immediate=False,
),
)
self.api = api
self.api = Smile(
host=entry.data[CONF_HOST],
username=entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
password=entry.data[CONF_PASSWORD],
port=entry.data.get(CONF_PORT, DEFAULT_PORT),
timeout=30,
websession=async_get_clientsession(hass, verify_ssl=False),
)
async def _connect(self) -> None:
"""Connect to the Plugwise Smile."""
self._connected = await self.api.connect()
self.api.get_all_devices()
self.name = self.api.smile_name
self.update_interval = DEFAULT_SCAN_INTERVAL.get(
str(self.api.smile_type), timedelta(seconds=60)
)
async def _async_update_data(self) -> PlugwiseData:
"""Fetch data from Plugwise."""
try:
if not self._connected:
await self._connect()
data = await self.api.async_update()
except InvalidAuthentication as err:
raise UpdateFailed("Authentication failed") from err
raise ConfigEntryError("Invalid username or Smile ID") from err
except (InvalidXMLError, ResponseError) as err:
raise UpdateFailed(
"Invalid XML data, or error indication received for the Plugwise Adam/Smile/Stretch"
) from err
except UnsupportedDeviceError as err:
raise UpdateFailed("Device with unsupported firmware") from err
raise ConfigEntryError("Device with unsupported firmware") from err
except ConnectionFailedError as err:
raise UpdateFailed("Failed to connect") from err
raise UpdateFailed("Failed to connect to the Plugwise Smile") from err
return PlugwiseData(
gateway=cast(GatewayData, data[0]),
devices=cast(dict[str, DeviceData], data[1]),

View File

@ -3,30 +3,11 @@ from __future__ import annotations
from typing import Any
from plugwise.exceptions import (
ConnectionFailedError,
InvalidAuthentication,
InvalidXMLError,
ResponseError,
UnsupportedDeviceError,
)
from plugwise.smile import Smile
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
DEFAULT_PORT,
DEFAULT_USERNAME,
DOMAIN,
LOGGER,
PLATFORMS_GATEWAY,
Platform,
)
from .const import DOMAIN, LOGGER, PLATFORMS_GATEWAY, Platform
from .coordinator import PlugwiseDataUpdateCoordinator
@ -34,37 +15,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Plugwise Smiles from a config entry."""
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
websession = async_get_clientsession(hass, verify_ssl=False)
api = Smile(
host=entry.data[CONF_HOST],
username=entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
password=entry.data[CONF_PASSWORD],
port=entry.data.get(CONF_PORT, DEFAULT_PORT),
timeout=30,
websession=websession,
)
try:
connected = await api.connect()
except ConnectionFailedError as err:
raise ConfigEntryNotReady("Failed to connect to the Plugwise Smile") from err
except InvalidAuthentication as err:
raise HomeAssistantError("Invalid username or Smile ID") from err
except (InvalidXMLError, ResponseError) as err:
raise ConfigEntryNotReady(
"Error while communicating to the Plugwise Smile"
) from err
except UnsupportedDeviceError as err:
raise HomeAssistantError("Device with unsupported firmware") from err
if not connected:
raise ConfigEntryNotReady("Unable to connect to Smile")
api.get_all_devices()
if entry.unique_id is None and api.smile_version[0] != "1.8.0":
hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname)
coordinator = PlugwiseDataUpdateCoordinator(hass, api)
coordinator = PlugwiseDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
migrate_sensor_entities(hass, coordinator)
@ -73,11 +24,11 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, str(api.gateway_id))},
identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
manufacturer="Plugwise",
model=api.smile_model,
name=api.smile_name,
sw_version=api.smile_version[0],
model=coordinator.api.smile_model,
name=coordinator.api.smile_name,
sw_version=coordinator.api.smile_version[0],
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_GATEWAY)

View File

@ -75,7 +75,7 @@ def mock_smile_adam() -> Generator[None, MagicMock, None]:
chosen_env = "adam_multiple_devices_per_zone"
with patch(
"homeassistant.components.plugwise.gateway.Smile", autospec=True
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
smile = smile_mock.return_value
@ -101,7 +101,7 @@ def mock_smile_adam_2() -> Generator[None, MagicMock, None]:
chosen_env = "m_adam_heating"
with patch(
"homeassistant.components.plugwise.gateway.Smile", autospec=True
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
smile = smile_mock.return_value
@ -127,7 +127,7 @@ def mock_smile_adam_3() -> Generator[None, MagicMock, None]:
chosen_env = "m_adam_cooling"
with patch(
"homeassistant.components.plugwise.gateway.Smile", autospec=True
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
smile = smile_mock.return_value
@ -152,7 +152,7 @@ def mock_smile_anna() -> Generator[None, MagicMock, None]:
"""Create a Mock Anna environment for testing exceptions."""
chosen_env = "anna_heatpump_heating"
with patch(
"homeassistant.components.plugwise.gateway.Smile", autospec=True
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
smile = smile_mock.return_value
@ -177,7 +177,7 @@ def mock_smile_anna_2() -> Generator[None, MagicMock, None]:
"""Create a 2nd Mock Anna environment for testing exceptions."""
chosen_env = "m_anna_heatpump_cooling"
with patch(
"homeassistant.components.plugwise.gateway.Smile", autospec=True
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
smile = smile_mock.return_value
@ -202,7 +202,7 @@ def mock_smile_anna_3() -> Generator[None, MagicMock, None]:
"""Create a 3rd Mock Anna environment for testing exceptions."""
chosen_env = "m_anna_heatpump_idle"
with patch(
"homeassistant.components.plugwise.gateway.Smile", autospec=True
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
smile = smile_mock.return_value
@ -227,7 +227,7 @@ def mock_smile_p1() -> Generator[None, MagicMock, None]:
"""Create a Mock P1 DSMR environment for testing exceptions."""
chosen_env = "p1v3_full_option"
with patch(
"homeassistant.components.plugwise.gateway.Smile", autospec=True
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
smile = smile_mock.return_value
@ -252,7 +252,7 @@ def mock_stretch() -> Generator[None, MagicMock, None]:
"""Create a Mock Stretch environment for testing exceptions."""
chosen_env = "stretch_v31"
with patch(
"homeassistant.components.plugwise.gateway.Smile", autospec=True
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
smile = smile_mock.return_value

View File

@ -59,33 +59,6 @@ async def test_gateway_config_entry_not_ready(
mock_smile_anna: MagicMock,
side_effect: Exception,
entry_state: ConfigEntryState,
) -> None:
"""Test the Plugwise configuration entry not ready."""
mock_smile_anna.connect.side_effect = side_effect
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 len(mock_smile_anna.connect.mock_calls) == 1
assert mock_config_entry.state is entry_state
@pytest.mark.parametrize(
"side_effect",
[
(ConnectionFailedError),
(InvalidAuthentication),
(InvalidXMLError),
(ResponseError),
(UnsupportedDeviceError),
],
)
async def test_coord_config_entry_not_ready(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_smile_anna: MagicMock,
side_effect: Exception,
) -> None:
"""Test the Plugwise configuration entry not ready."""
mock_smile_anna.async_update.side_effect = side_effect
@ -95,7 +68,7 @@ async def test_coord_config_entry_not_ready(
await hass.async_block_till_done()
assert len(mock_smile_anna.connect.mock_calls) == 1
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert mock_config_entry.state is entry_state
@pytest.mark.parametrize(