Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/govee_light_local/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
1 change: 1 addition & 0 deletions homeassistant/components/mysensors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/roborock/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}"
)
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/roborock/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
20 changes: 17 additions & 3 deletions homeassistant/components/vesync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/vesync/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions tests/components/roborock/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 20 additions & 6 deletions tests/components/vesync/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
Loading