mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
Centralize exception handling in Plugwise (#82694)
This commit is contained in:
parent
fd3e996a1e
commit
13458dc722
@ -12,11 +12,15 @@ from plugwise.exceptions import (
|
|||||||
UnsupportedDeviceError,
|
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.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.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
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):
|
class PlugwiseData(NamedTuple):
|
||||||
@ -29,15 +33,15 @@ class PlugwiseData(NamedTuple):
|
|||||||
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
||||||
"""Class to manage fetching Plugwise data from single endpoint."""
|
"""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."""
|
"""Initialize the coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
name=api.smile_name or DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=DEFAULT_SCAN_INTERVAL.get(
|
update_interval=timedelta(seconds=60),
|
||||||
str(api.smile_type), timedelta(seconds=60)
|
|
||||||
),
|
|
||||||
# Don't refresh immediately, give the device time to process
|
# Don't refresh immediately, give the device time to process
|
||||||
# the change in state before we query it.
|
# the change in state before we query it.
|
||||||
request_refresh_debouncer=Debouncer(
|
request_refresh_debouncer=Debouncer(
|
||||||
@ -47,22 +51,41 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
|||||||
immediate=False,
|
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:
|
async def _async_update_data(self) -> PlugwiseData:
|
||||||
"""Fetch data from Plugwise."""
|
"""Fetch data from Plugwise."""
|
||||||
try:
|
try:
|
||||||
|
if not self._connected:
|
||||||
|
await self._connect()
|
||||||
data = await self.api.async_update()
|
data = await self.api.async_update()
|
||||||
except InvalidAuthentication as err:
|
except InvalidAuthentication as err:
|
||||||
raise UpdateFailed("Authentication failed") from err
|
raise ConfigEntryError("Invalid username or Smile ID") from err
|
||||||
except (InvalidXMLError, ResponseError) as err:
|
except (InvalidXMLError, ResponseError) as err:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
"Invalid XML data, or error indication received for the Plugwise Adam/Smile/Stretch"
|
"Invalid XML data, or error indication received for the Plugwise Adam/Smile/Stretch"
|
||||||
) from err
|
) from err
|
||||||
except UnsupportedDeviceError as 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:
|
except ConnectionFailedError as err:
|
||||||
raise UpdateFailed("Failed to connect") from err
|
raise UpdateFailed("Failed to connect to the Plugwise Smile") from err
|
||||||
return PlugwiseData(
|
return PlugwiseData(
|
||||||
gateway=cast(GatewayData, data[0]),
|
gateway=cast(GatewayData, data[0]),
|
||||||
devices=cast(dict[str, DeviceData], data[1]),
|
devices=cast(dict[str, DeviceData], data[1]),
|
||||||
|
@ -3,30 +3,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
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 import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN, LOGGER, PLATFORMS_GATEWAY, Platform
|
||||||
DEFAULT_PORT,
|
|
||||||
DEFAULT_USERNAME,
|
|
||||||
DOMAIN,
|
|
||||||
LOGGER,
|
|
||||||
PLATFORMS_GATEWAY,
|
|
||||||
Platform,
|
|
||||||
)
|
|
||||||
from .coordinator import PlugwiseDataUpdateCoordinator
|
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."""
|
"""Set up Plugwise Smiles from a config entry."""
|
||||||
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
||||||
|
|
||||||
websession = async_get_clientsession(hass, verify_ssl=False)
|
coordinator = PlugwiseDataUpdateCoordinator(hass, entry)
|
||||||
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)
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
migrate_sensor_entities(hass, coordinator)
|
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 = dr.async_get(hass)
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=entry.entry_id,
|
config_entry_id=entry.entry_id,
|
||||||
identifiers={(DOMAIN, str(api.gateway_id))},
|
identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
|
||||||
manufacturer="Plugwise",
|
manufacturer="Plugwise",
|
||||||
model=api.smile_model,
|
model=coordinator.api.smile_model,
|
||||||
name=api.smile_name,
|
name=coordinator.api.smile_name,
|
||||||
sw_version=api.smile_version[0],
|
sw_version=coordinator.api.smile_version[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_GATEWAY)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_GATEWAY)
|
||||||
|
@ -75,7 +75,7 @@ def mock_smile_adam() -> Generator[None, MagicMock, None]:
|
|||||||
chosen_env = "adam_multiple_devices_per_zone"
|
chosen_env = "adam_multiple_devices_per_zone"
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.plugwise.gateway.Smile", autospec=True
|
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
|
||||||
) as smile_mock:
|
) as smile_mock:
|
||||||
smile = smile_mock.return_value
|
smile = smile_mock.return_value
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ def mock_smile_adam_2() -> Generator[None, MagicMock, None]:
|
|||||||
chosen_env = "m_adam_heating"
|
chosen_env = "m_adam_heating"
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.plugwise.gateway.Smile", autospec=True
|
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
|
||||||
) as smile_mock:
|
) as smile_mock:
|
||||||
smile = smile_mock.return_value
|
smile = smile_mock.return_value
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ def mock_smile_adam_3() -> Generator[None, MagicMock, None]:
|
|||||||
chosen_env = "m_adam_cooling"
|
chosen_env = "m_adam_cooling"
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.plugwise.gateway.Smile", autospec=True
|
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
|
||||||
) as smile_mock:
|
) as smile_mock:
|
||||||
smile = smile_mock.return_value
|
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."""
|
"""Create a Mock Anna environment for testing exceptions."""
|
||||||
chosen_env = "anna_heatpump_heating"
|
chosen_env = "anna_heatpump_heating"
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.plugwise.gateway.Smile", autospec=True
|
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
|
||||||
) as smile_mock:
|
) as smile_mock:
|
||||||
smile = smile_mock.return_value
|
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."""
|
"""Create a 2nd Mock Anna environment for testing exceptions."""
|
||||||
chosen_env = "m_anna_heatpump_cooling"
|
chosen_env = "m_anna_heatpump_cooling"
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.plugwise.gateway.Smile", autospec=True
|
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
|
||||||
) as smile_mock:
|
) as smile_mock:
|
||||||
smile = smile_mock.return_value
|
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."""
|
"""Create a 3rd Mock Anna environment for testing exceptions."""
|
||||||
chosen_env = "m_anna_heatpump_idle"
|
chosen_env = "m_anna_heatpump_idle"
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.plugwise.gateway.Smile", autospec=True
|
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
|
||||||
) as smile_mock:
|
) as smile_mock:
|
||||||
smile = smile_mock.return_value
|
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."""
|
"""Create a Mock P1 DSMR environment for testing exceptions."""
|
||||||
chosen_env = "p1v3_full_option"
|
chosen_env = "p1v3_full_option"
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.plugwise.gateway.Smile", autospec=True
|
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
|
||||||
) as smile_mock:
|
) as smile_mock:
|
||||||
smile = smile_mock.return_value
|
smile = smile_mock.return_value
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ def mock_stretch() -> Generator[None, MagicMock, None]:
|
|||||||
"""Create a Mock Stretch environment for testing exceptions."""
|
"""Create a Mock Stretch environment for testing exceptions."""
|
||||||
chosen_env = "stretch_v31"
|
chosen_env = "stretch_v31"
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.plugwise.gateway.Smile", autospec=True
|
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
|
||||||
) as smile_mock:
|
) as smile_mock:
|
||||||
smile = smile_mock.return_value
|
smile = smile_mock.return_value
|
||||||
|
|
||||||
|
@ -59,33 +59,6 @@ async def test_gateway_config_entry_not_ready(
|
|||||||
mock_smile_anna: MagicMock,
|
mock_smile_anna: MagicMock,
|
||||||
side_effect: Exception,
|
side_effect: Exception,
|
||||||
entry_state: ConfigEntryState,
|
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:
|
) -> None:
|
||||||
"""Test the Plugwise configuration entry not ready."""
|
"""Test the Plugwise configuration entry not ready."""
|
||||||
mock_smile_anna.async_update.side_effect = side_effect
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_smile_anna.connect.mock_calls) == 1
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user