diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 5e5cab7d816..30adca12819 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -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]), diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 81cb81e9bbf..57db57e0e8f 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -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) diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index aa34fc8bed6..0c70a2bed6d 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -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 diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index e37ee6b320c..cbca5d19ab5 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -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(