diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 164135a607b..10e4836f73a 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -36,6 +36,12 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon_off="mdi:hvac-off", entity_category=EntityCategory.DIAGNOSTIC, ), + PlugwiseBinarySensorEntityDescription( + key="cooling_enabled", + name="Cooling enabled", + icon="mdi:snowflake-thermometer", + entity_category=EntityCategory.DIAGNOSTIC, + ), PlugwiseBinarySensorEntityDescription( key="dhw_state", name="DHW state", diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 09b919e9d1d..3bbd5725b29 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -4,9 +4,12 @@ from __future__ import annotations from typing import Any from plugwise.exceptions import ( + ConnectionFailedError, InvalidAuthentication, InvalidSetupError, - PlugwiseException, + InvalidXMLError, + ResponseError, + UnsupportedDeviceError, ) from plugwise.smile import Smile import voluptuous as vol @@ -32,7 +35,6 @@ from .const import ( DOMAIN, FLOW_SMILE, FLOW_STRETCH, - LOGGER, PW_TYPE, SMILE, STRETCH, @@ -175,14 +177,17 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): try: api = await validate_gw_input(self.hass, user_input) - except InvalidSetupError: - errors[CONF_BASE] = "invalid_setup" + except ConnectionFailedError: + errors[CONF_BASE] = "cannot_connect" except InvalidAuthentication: errors[CONF_BASE] = "invalid_auth" - except PlugwiseException: - errors[CONF_BASE] = "cannot_connect" + except InvalidSetupError: + errors[CONF_BASE] = "invalid_setup" + except (InvalidXMLError, ResponseError): + errors[CONF_BASE] = "response_error" + except UnsupportedDeviceError: + errors[CONF_BASE] = "unsupported" except Exception: # pylint: disable=broad-except - LOGGER.exception("Unexpected exception") errors[CONF_BASE] = "unknown" else: await self.async_set_unique_id( diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 6fd44efda84..5e5cab7d816 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -4,7 +4,13 @@ from typing import NamedTuple, cast from plugwise import Smile from plugwise.constants import DeviceData, GatewayData -from plugwise.exceptions import PlugwiseException, XMLDataMissingError +from plugwise.exceptions import ( + ConnectionFailedError, + InvalidAuthentication, + InvalidXMLError, + ResponseError, + UnsupportedDeviceError, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer @@ -47,12 +53,16 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): """Fetch data from Plugwise.""" try: data = await self.api.async_update() - except XMLDataMissingError as err: + except InvalidAuthentication as err: + raise UpdateFailed("Authentication failed") from err + except (InvalidXMLError, ResponseError) as err: raise UpdateFailed( - f"No XML data received for: {self.api.smile_name}" + "Invalid XML data, or error indication received for the Plugwise Adam/Smile/Stretch" ) from err - except PlugwiseException as err: - raise UpdateFailed(f"Updated failed for: {self.api.smile_name}") from err + except UnsupportedDeviceError as err: + raise UpdateFailed("Device with unsupported firmware") from err + except ConnectionFailedError as err: + raise UpdateFailed("Failed to connect") 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 16b6a977569..81cb81e9bbf 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -1,17 +1,21 @@ """Plugwise platform for Home Assistant Core.""" from __future__ import annotations -import asyncio from typing import Any -from aiohttp import ClientConnectionError -from plugwise.exceptions import InvalidAuthentication, PlugwiseException +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 +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 @@ -42,17 +46,16 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: connected = await api.connect() - except InvalidAuthentication: - LOGGER.error("Invalid username or Smile ID") - return False - except (ClientConnectionError, PlugwiseException) as err: + 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( - f"Error while communicating to device {api.smile_name}" - ) from err - except asyncio.TimeoutError as err: - raise ConfigEntryNotReady( - f"Timeout while connecting to Smile {api.smile_name}" + "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") diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 6bb1c941bf3..6a11b47da57 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.25.7"], + "requirements": ["plugwise==0.25.12"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 7278f6c4414..781a17a1d10 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -15,8 +15,10 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", - "unknown": "[%key:common::config_flow::error::unknown%]" + "invalid_setup": "Add your Adam instead of your Anna, see the documentation", + "response_error": "Invalid XML data, or error indication received", + "unknown": "[%key:common::config_flow::error::unknown%]", + "unsupported": "Device with unsupported firmware" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index aa5a318bbff..ecc5af9218d 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -7,8 +7,10 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", - "unknown": "Unexpected error" + "invalid_setup": "Add your Adam instead of your Anna, see the documentation", + "response_error": "Invalid XML data, or error indication received", + "unknown": "Unexpected error", + "unsupported": "Device with unsupported firmware" }, "step": { "user": { diff --git a/requirements_all.txt b/requirements_all.txt index cf2504c8513..e844738a1ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1329,7 +1329,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.7 +plugwise==0.25.12 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e96e13f4b9..6eae3f35265 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -956,7 +956,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.7 +plugwise==0.25.12 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index 546a11b2c68..1dc0fb6f2ad 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -3,7 +3,7 @@ "smile_name": "Smile Anna", "gateway_id": "015ae9ea3f964e668e490fa39da3870b", "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", - "cooling_present": true, + "cooling_present": false, "notifications": {} }, { @@ -21,10 +21,10 @@ }, "available": true, "binary_sensors": { + "cooling_enabled": false, "dhw_state": false, "heating_state": true, "compressor_state": true, - "cooling_state": false, "slave_boiler_state": false, "flame_state": false }, @@ -66,13 +66,11 @@ "name": "Anna", "vendor": "Plugwise", "thermostat": { - "setpoint_low": 20.5, - "setpoint_high": 24.0, + "setpoint": 20.5, "lower_bound": 4.0, "upper_bound": 30.0, "resolution": 0.1 }, - "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], @@ -81,11 +79,10 @@ "mode": "auto", "sensors": { "temperature": 19.3, + "setpoint": 20.5, "illuminance": 86.0, "cooling_activation_outdoor_temperature": 21.0, - "cooling_deactivation_threshold": 4.0, - "setpoint_low": 20.5, - "setpoint_high": 24.0 + "cooling_deactivation_threshold": 4.0 } } } diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index 6326a02fedb..cb9e340efb8 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -21,6 +21,7 @@ }, "available": true, "binary_sensors": { + "cooling_enabled": true, "dhw_state": false, "heating_state": false, "compressor_state": true, @@ -72,7 +73,6 @@ "upper_bound": 30.0, "resolution": 0.1 }, - "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index cd2747f423b..0a421be5343 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -21,6 +21,7 @@ }, "available": true, "binary_sensors": { + "cooling_enabled": true, "dhw_state": false, "heating_state": false, "compressor_state": false, @@ -72,7 +73,6 @@ "upper_bound": 30.0, "resolution": 0.1 }, - "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index 1e4df8fb673..f4f2a3f3c5f 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -26,7 +26,7 @@ async def test_anna_climate_binary_sensor_entities( assert state assert state.state == STATE_ON - state = hass.states.get("binary_sensor.opentherm_cooling") + state = hass.states.get("binary_sensor.opentherm_cooling_enabled") assert state assert state.state == STATE_OFF diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index ad5443a678c..5636523a919 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -202,7 +202,7 @@ async def test_anna_climate_entity_attributes( assert state.state == HVACMode.AUTO assert state.attributes["hvac_action"] == "heating" assert state.attributes["hvac_modes"] == [ - HVACMode.HEAT_COOL, + HVACMode.HEAT, HVACMode.AUTO, ] @@ -211,9 +211,8 @@ async def test_anna_climate_entity_attributes( assert state.attributes["current_temperature"] == 19.3 assert state.attributes["preset_mode"] == "home" - assert state.attributes["supported_features"] == 18 - assert state.attributes["target_temp_high"] == 24.0 - assert state.attributes["target_temp_low"] == 20.5 + assert state.attributes["supported_features"] == 17 + assert state.attributes["temperature"] == 20.5 assert state.attributes["min_temp"] == 4.0 assert state.attributes["max_temp"] == 30.0 assert state.attributes["target_temp_step"] == 0.1 @@ -286,7 +285,7 @@ async def test_anna_climate_entity_climate_changes( await hass.services.async_call( "climate", "set_hvac_mode", - {"entity_id": "climate.anna", "hvac_mode": "heat_cool"}, + {"entity_id": "climate.anna", "hvac_mode": "heat"}, blocking=True, ) diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 84d2335b16f..b569cb08ddf 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -5,7 +5,9 @@ from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, InvalidSetupError, - PlugwiseException, + InvalidXMLError, + ResponseError, + UnsupportedDeviceError, ) import pytest @@ -97,9 +99,12 @@ def mock_smile(): with patch( "homeassistant.components.plugwise.config_flow.Smile", ) as smile_mock: - smile_mock.PlugwiseException = PlugwiseException - smile_mock.InvalidAuthentication = InvalidAuthentication smile_mock.ConnectionFailedError = ConnectionFailedError + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.InvalidSetupError = InvalidSetupError + smile_mock.InvalidXMLError = InvalidXMLError + smile_mock.ResponseError = ResponseError + smile_mock.UnsupportedDeviceError = UnsupportedDeviceError smile_mock.return_value.connect.return_value = True yield smile_mock.return_value @@ -266,13 +271,15 @@ async def test_zercoconf_discovery_update_configuration( @pytest.mark.parametrize( - "side_effect,reason", + "side_effect, reason", [ + (ConnectionFailedError, "cannot_connect"), (InvalidAuthentication, "invalid_auth"), (InvalidSetupError, "invalid_setup"), - (ConnectionFailedError, "cannot_connect"), - (PlugwiseException, "cannot_connect"), + (InvalidXMLError, "response_error"), + (ResponseError, "response_error"), (RuntimeError, "unknown"), + (UnsupportedDeviceError, "unsupported"), ], ) async def test_flow_errors( diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 557680cb060..e37ee6b320c 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -1,12 +1,12 @@ """Tests for the Plugwise Climate integration.""" -import asyncio from unittest.mock import MagicMock -import aiohttp from plugwise.exceptions import ( ConnectionFailedError, - PlugwiseException, - XMLDataMissingError, + InvalidAuthentication, + InvalidXMLError, + ResponseError, + UnsupportedDeviceError, ) import pytest @@ -43,24 +43,52 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.NOT_LOADED +@pytest.mark.parametrize( + "side_effect, entry_state", + [ + (ConnectionFailedError, ConfigEntryState.SETUP_RETRY), + (InvalidAuthentication, ConfigEntryState.SETUP_ERROR), + (InvalidXMLError, ConfigEntryState.SETUP_RETRY), + (ResponseError, ConfigEntryState.SETUP_RETRY), + (UnsupportedDeviceError, ConfigEntryState.SETUP_ERROR), + ], +) +async def test_gateway_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + 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), - (PlugwiseException), - (XMLDataMissingError), - (asyncio.TimeoutError), - (aiohttp.ClientConnectionError), + (InvalidAuthentication), + (InvalidXMLError), + (ResponseError), + (UnsupportedDeviceError), ], ) -async def test_config_entry_not_ready( +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.connect.side_effect = side_effect + mock_smile_anna.async_update.side_effect = side_effect mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id)