diff --git a/homeassistant/components/govee_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index 0b108758c02dde..bdfd0f446dac46 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==2.2.0"] + "requirements": ["govee-local-api==2.3.0"] } diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index f9cabda90b7d72..ffb1c131d093ea 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -5,6 +5,7 @@ "codeowners": ["@MartinHjelmare", "@functionpointer"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mysensors", + "integration_type": "hub", "iot_class": "local_push", "loggers": ["mysensors"], "requirements": ["pymysensors==0.26.0"] diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index fe070e10321eba..025a5c37186a40 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -122,6 +122,7 @@ def __init__( # Tracks the last successful update to control when we report failure # to the base class. This is reset on successful data update. self._last_update_success_time: datetime | None = None + self._has_connected_locally: bool = False @cached_property def dock_device_info(self) -> DeviceInfo: @@ -191,7 +192,8 @@ async def update_map(self) -> None: async def _verify_api(self) -> None: """Verify that the api is reachable.""" if self._device.is_connected: - if self._device.is_local_connected: + self._has_connected_locally |= self._device.is_local_connected + if self._has_connected_locally: async_delete_issue( self.hass, DOMAIN, f"cloud_api_used_{self.duid_slug}" ) @@ -234,6 +236,7 @@ async def _update_device_prop(self) -> None: async def _async_update_data(self) -> DeviceState: """Update data via library.""" + await self._verify_api() try: # Update device props and standard api information await self._update_device_prop() diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 4a7000cecdd562..51f6e65407c1eb 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -20,7 +20,7 @@ "loggers": ["roborock"], "quality_scale": "silver", "requirements": [ - "python-roborock==3.20.1", + "python-roborock==3.21.0", "vacuum-map-parser-roborock==0.1.4" ] } diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 133713b33524d1..f7a986ee573e8d 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -3,12 +3,16 @@ import logging from pyvesync import VeSync -from pyvesync.utils.errors import VeSyncLoginError +from pyvesync.utils.errors import ( + VeSyncAPIResponseError, + VeSyncLoginError, + VeSyncServerError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntry @@ -59,7 +63,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await manager.login() except VeSyncLoginError as err: - raise ConfigEntryAuthFailed from err + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, translation_key="invalid_auth" + ) from err + except VeSyncServerError as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, translation_key="server_error" + ) from err + except VeSyncAPIResponseError as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, translation_key="api_response_error" + ) from err hass.data[DOMAIN] = {} hass.data[DOMAIN][VS_MANAGER] = manager diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index 6435a9676bbc8a..4f72c6c790a679 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -5,7 +5,9 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "api_response_error": "API response error", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "server_error": "Server error occurred" }, "step": { "reauth_confirm": { diff --git a/requirements_all.txt b/requirements_all.txt index 5d8b7425362f61..ed80a3f1be11de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1118,7 +1118,7 @@ gotailwind==0.3.0 govee-ble==0.44.0 # homeassistant.components.govee_light_local -govee-local-api==2.2.0 +govee-local-api==2.3.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 @@ -2575,7 +2575,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==3.20.1 +python-roborock==3.21.0 # homeassistant.components.smarttub python-smarttub==0.0.46 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14dad4957581f9..e262747ba96cb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ gotailwind==0.3.0 govee-ble==0.44.0 # homeassistant.components.govee_light_local -govee-local-api==2.2.0 +govee-local-api==2.3.0 # homeassistant.components.gpsd gps3==0.33.3 @@ -2159,7 +2159,7 @@ python-pooldose==0.8.1 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==3.20.1 +python-roborock==3.21.0 # homeassistant.components.smarttub python-smarttub==0.0.46 diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 034d8b3c1f93f4..8aad5fde10e250 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -415,6 +415,63 @@ async def test_cloud_api_repair( assert len(issue_registry.issues) == 0 +@pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) +async def test_cloud_api_repair_cleared_on_update( + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + fake_vacuum: FakeDevice, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that a repair is created then cleared if the device is reachable locally again.""" + + # Fake that the device is only reachable via cloud + fake_vacuum.is_connected = True + fake_vacuum.is_local_connected = False + + # Load the integration and verify that a repair issue is created + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + await hass.async_block_till_done() + assert mock_roborock_entry.state is ConfigEntryState.LOADED + + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 1 + + # Fake that the device is reachable locally again. + fake_vacuum.is_local_connected = True + + # Refresh the coordinator using an arbitrary sensor, which should + # clear the repair issue. + sensor_entity_id = "sensor.roborock_s7_maxv_battery" + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: sensor_entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + # Verify that the repair issue is cleared + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 0 + + # Fake the device is cloud only again. Refreshing the coordinator + # should not recreate the repair issue. + fake_vacuum.is_local_connected = False + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: sensor_entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + # Verify that the repair issue still does not exist + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 0 + + @pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) async def test_zeo_device_fails_setup( hass: HomeAssistant, diff --git a/tests/components/vesync/test_init.py b/tests/components/vesync/test_init.py index 97d27fe221cd12..1e10e2f9faaf1f 100644 --- a/tests/components/vesync/test_init.py +++ b/tests/components/vesync/test_init.py @@ -2,8 +2,13 @@ from unittest.mock import AsyncMock, patch +import pytest from pyvesync import VeSync -from pyvesync.utils.errors import VeSyncLoginError +from pyvesync.utils.errors import ( + VeSyncAPIResponseError, + VeSyncLoginError, + VeSyncServerError, +) from homeassistant.components.vesync import ( async_remove_config_entry_device, @@ -18,21 +23,30 @@ from tests.common import MockConfigEntry -async def test_async_setup_entry__not_login( +@pytest.mark.parametrize( + ("exception", "expected_state"), + [ + (VeSyncLoginError("Mock login failed"), ConfigEntryState.SETUP_ERROR), + (VeSyncAPIResponseError("Mock login failed"), ConfigEntryState.SETUP_RETRY), + (VeSyncServerError("Mock login failed"), ConfigEntryState.SETUP_RETRY), + ], +) +async def test_async_setup_entry_login_errors( hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync, + exception: Exception, + expected_state: ConfigEntryState, ) -> None: - """Test setup does not create config entry when not logged in.""" - manager.login = AsyncMock(side_effect=VeSyncLoginError("Mock login failed")) + """Test setup handles different login errors appropriately.""" + manager.login = AsyncMock(side_effect=exception) assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert manager.login.call_count == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state is ConfigEntryState.SETUP_ERROR - assert not hass.data.get(DOMAIN) + assert config_entry.state is expected_state async def test_async_setup_entry__no_devices(