mirror of
https://github.com/home-assistant/core.git
synced 2025-11-13 04:50:17 +00:00
Migrate from homeconnect dependency to aiohomeconnect (#136116)
* Migrate from homeconnect dependency to aiohomeconnect * Reload the integration if there is an API error on event stream * fix typos at coordinator tests * Setup config entry at coordinator tests * fix ruff * Bump aiohomeconnect to version 0.11.4 * Fix set program options * Use context based updates at coordinator * Improved how `context_callbacks` cache is invalidated * fix * fixes and improvements at coordinator Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove stale Entity inheritance * Small improvement for light subscriptions * Remove non-needed function It had its purpose before some refactoring before the firs commit, no is no needed as is only used at HomeConnectEntity constructor * Static methods and variables at conftest * Refresh the data after an event stream interruption * Cleaned debug logs * Fetch programs at coordinator * Improvements Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Simplify obtaining power settings from coordinator data Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove unnecessary statement * use `is UNDEFINED` instead of `isinstance` * Request power setting only when it is strictly necessary * Bump aiohomeconnect to 0.12.1 * use raw keys for diagnostics * Use keyword arguments where needed * Remove unnecessary statements Co-authored-by: Martin Hjelmare <marhje52@gmail.com> --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
committed by
GitHub
parent
4e3e1e91b7
commit
b637129208
@@ -2,27 +2,18 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from aiohomeconnect.const import OAUTH2_TOKEN
|
||||
from aiohomeconnect.model import SettingKey, StatusKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
import pytest
|
||||
from requests import HTTPError
|
||||
import requests_mock
|
||||
import respx
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.home_connect import (
|
||||
SCAN_INTERVAL,
|
||||
bsh_key_to_translation_key,
|
||||
)
|
||||
from homeassistant.components.home_connect.const import (
|
||||
BSH_CHILD_LOCK_STATE,
|
||||
BSH_OPERATION_STATE,
|
||||
BSH_POWER_STATE,
|
||||
BSH_REMOTE_START_ALLOWANCE_STATE,
|
||||
COOKING_LIGHTING,
|
||||
DOMAIN,
|
||||
OAUTH2_TOKEN,
|
||||
)
|
||||
from homeassistant.components.home_connect.const import DOMAIN
|
||||
from homeassistant.components.home_connect.utils import bsh_key_to_translation_key
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
@@ -39,7 +30,6 @@ from .conftest import (
|
||||
FAKE_ACCESS_TOKEN,
|
||||
FAKE_REFRESH_TOKEN,
|
||||
SERVER_ACCESS_TOKEN,
|
||||
get_all_appliances,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -126,28 +116,26 @@ SERVICE_PROGRAM_CALL_PARAMS = [
|
||||
]
|
||||
|
||||
SERVICE_APPLIANCE_METHOD_MAPPING = {
|
||||
"set_option_active": "set_options_active_program",
|
||||
"set_option_selected": "set_options_selected_program",
|
||||
"set_option_active": "set_active_program_option",
|
||||
"set_option_selected": "set_selected_program_option",
|
||||
"change_setting": "set_setting",
|
||||
"pause_program": "execute_command",
|
||||
"resume_program": "execute_command",
|
||||
"select_program": "select_program",
|
||||
"pause_program": "put_command",
|
||||
"resume_program": "put_command",
|
||||
"select_program": "set_selected_program",
|
||||
"start_program": "start_program",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_api_setup(
|
||||
async def test_entry_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test setup and unload."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
@@ -156,72 +144,60 @@ async def test_api_setup(
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_update_throttle(
|
||||
appliance: Mock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
) -> None:
|
||||
"""Test to check Throttle functionality."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
get_appliances_call_count = get_appliances.call_count
|
||||
|
||||
# First re-load after 1 minute is not blocked.
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
freezer.tick(SCAN_INTERVAL.seconds + 0.1)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert get_appliances.call_count == get_appliances_call_count + 1
|
||||
|
||||
# Second re-load is blocked by Throttle.
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
freezer.tick(SCAN_INTERVAL.seconds - 0.1)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert get_appliances.call_count == get_appliances_call_count + 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_exception_handling(
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
problematic_appliance: Mock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test exception handling."""
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("token_expiration_time", [12345])
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
@respx.mock
|
||||
async def test_token_refresh_success(
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
hass: HomeAssistant,
|
||||
platforms: list[Platform],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
requests_mock: requests_mock.Mocker,
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test where token is expired and the refresh attempt succeeds."""
|
||||
|
||||
assert config_entry.data["token"]["access_token"] == FAKE_ACCESS_TOKEN
|
||||
|
||||
requests_mock.post(OAUTH2_TOKEN, json=SERVER_ACCESS_TOKEN)
|
||||
requests_mock.get("/api/homeappliances", json={"data": {"homeappliances": []}})
|
||||
|
||||
aioclient_mock.post(
|
||||
OAUTH2_TOKEN,
|
||||
json=SERVER_ACCESS_TOKEN,
|
||||
)
|
||||
assert await integration_setup()
|
||||
appliances = client.get_home_appliances.return_value
|
||||
|
||||
async def mock_get_home_appliances():
|
||||
await client._auth.async_get_access_token()
|
||||
return appliances
|
||||
|
||||
client.get_home_appliances.return_value = None
|
||||
client.get_home_appliances.side_effect = mock_get_home_appliances
|
||||
|
||||
def init_side_effect(auth) -> MagicMock:
|
||||
client._auth = auth
|
||||
return client
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
with (
|
||||
patch("homeassistant.components.home_connect.PLATFORMS", platforms),
|
||||
patch("homeassistant.components.home_connect.HomeConnectClient") as client_mock,
|
||||
):
|
||||
client_mock.side_effect = MagicMock(side_effect=init_side_effect)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
# Verify token request
|
||||
@@ -240,45 +216,43 @@ async def test_token_refresh_success(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_http_error(
|
||||
async def test_client_error(
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test HTTP errors during setup integration."""
|
||||
get_appliances.side_effect = HTTPError(response=MagicMock())
|
||||
"""Test client errors during setup integration."""
|
||||
client_with_exception.get_home_appliances.return_value = None
|
||||
client_with_exception.get_home_appliances.side_effect = HomeConnectError()
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
assert get_appliances.call_count == 1
|
||||
assert not await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
assert client_with_exception.get_home_appliances.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_call",
|
||||
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_services(
|
||||
service_call: list[dict[str, Any]],
|
||||
service_call: dict[str, Any],
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
) -> None:
|
||||
"""Create and test services."""
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, appliance.haId)},
|
||||
identifiers={(DOMAIN, appliance_ha_id)},
|
||||
)
|
||||
|
||||
service_name = service_call["service"]
|
||||
@@ -286,8 +260,7 @@ async def test_services(
|
||||
await hass.services.async_call(**service_call)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
getattr(appliance, SERVICE_APPLIANCE_METHOD_MAPPING[service_name]).call_count
|
||||
== 1
|
||||
getattr(client, SERVICE_APPLIANCE_METHOD_MAPPING[service_name]).call_count == 1
|
||||
)
|
||||
|
||||
|
||||
@@ -295,26 +268,24 @@ async def test_services(
|
||||
"service_call",
|
||||
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_services_exception(
|
||||
service_call: list[dict[str, Any]],
|
||||
service_call: dict[str, Any],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
problematic_appliance: Mock,
|
||||
client_with_exception: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Raise a HomeAssistantError when there is an API error."""
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, problematic_appliance.haId)},
|
||||
identifiers={(DOMAIN, appliance_ha_id)},
|
||||
)
|
||||
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
@@ -323,25 +294,47 @@ async def test_services_exception(
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_services_appliance_not_found(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Raise a ServiceValidationError when device id does not match."""
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
service_call = SERVICE_KV_CALL_PARAMS[0]
|
||||
|
||||
service_call["service_data"]["device_id"] = "DOES_NOT_EXISTS"
|
||||
|
||||
with pytest.raises(ServiceValidationError, match=r"Device entry.*not found"):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
unrelated_config_entry = MockConfigEntry(
|
||||
domain="TEST",
|
||||
)
|
||||
unrelated_config_entry.add_to_hass(hass)
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=unrelated_config_entry.entry_id,
|
||||
identifiers={("RANDOM", "ABCD")},
|
||||
)
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError, match=r"Home Connect config entry.*not found"
|
||||
):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={("RANDOM", "ABCD")},
|
||||
)
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
|
||||
with pytest.raises(ServiceValidationError, match=r"Appliance.*not found"):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
@@ -351,7 +344,7 @@ async def test_entity_migration(
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry_v1_1: MockConfigEntry,
|
||||
appliance: Mock,
|
||||
appliance_ha_id: str,
|
||||
platforms: list[Platform],
|
||||
) -> None:
|
||||
"""Test entity migration."""
|
||||
@@ -360,34 +353,39 @@ async def test_entity_migration(
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry_v1_1.entry_id,
|
||||
identifiers={(DOMAIN, appliance.haId)},
|
||||
identifiers={(DOMAIN, appliance_ha_id)},
|
||||
)
|
||||
|
||||
test_entities = [
|
||||
(
|
||||
SENSOR_DOMAIN,
|
||||
"Operation State",
|
||||
BSH_OPERATION_STATE,
|
||||
StatusKey.BSH_COMMON_OPERATION_STATE,
|
||||
),
|
||||
(
|
||||
SWITCH_DOMAIN,
|
||||
"ChildLock",
|
||||
BSH_CHILD_LOCK_STATE,
|
||||
SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
),
|
||||
(
|
||||
SWITCH_DOMAIN,
|
||||
"Power",
|
||||
BSH_POWER_STATE,
|
||||
SettingKey.BSH_COMMON_POWER_STATE,
|
||||
),
|
||||
(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
"Remote Start",
|
||||
BSH_REMOTE_START_ALLOWANCE_STATE,
|
||||
StatusKey.BSH_COMMON_REMOTE_CONTROL_START_ALLOWED,
|
||||
),
|
||||
(
|
||||
LIGHT_DOMAIN,
|
||||
"Light",
|
||||
COOKING_LIGHTING,
|
||||
SettingKey.COOKING_COMMON_LIGHTING,
|
||||
),
|
||||
( # An already migrated entity
|
||||
SWITCH_DOMAIN,
|
||||
SettingKey.REFRIGERATION_COMMON_VACATION_MODE,
|
||||
SettingKey.REFRIGERATION_COMMON_VACATION_MODE,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -395,7 +393,7 @@ async def test_entity_migration(
|
||||
entity_registry.async_get_or_create(
|
||||
domain,
|
||||
DOMAIN,
|
||||
f"{appliance.haId}-{old_unique_id_suffix}",
|
||||
f"{appliance_ha_id}-{old_unique_id_suffix}",
|
||||
device_id=device_entry.id,
|
||||
config_entry=config_entry_v1_1,
|
||||
)
|
||||
@@ -406,7 +404,7 @@ async def test_entity_migration(
|
||||
|
||||
for domain, _, expected_unique_id_suffix in test_entities:
|
||||
assert entity_registry.async_get_entity_id(
|
||||
domain, DOMAIN, f"{appliance.haId}-{expected_unique_id_suffix}"
|
||||
domain, DOMAIN, f"{appliance_ha_id}-{expected_unique_id_suffix}"
|
||||
)
|
||||
assert config_entry_v1_1.minor_version == 2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user