mirror of
https://github.com/home-assistant/core.git
synced 2025-11-15 14:00:24 +00:00
Add Home Connect action with recognized programs and options (#130662)
* Added recognized options to Home Connect actions * Fix ruff * Fix strings.json * Fix dishwasher typo * Improved test_bsh_key_transformations * Add missing return types * Added descriptions * Remove custom options * Fixes * Merge the 4 services (select, start, set options for active or selected program) And deprecate the original ones * Delete stale snapshots * Clean up logic after service validation * Make deprecated actions issues fixable And delete issue on entry unload * Fixes and improvements Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Improvements Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Fix name and descriptions * Add `affects_to` to strings and service.yaml * Add missing periods at strings * Fix Co-authored-by: Norbert Rittel <norbert@rittel.de> * Add tests to check if the flow removes the deprecated action issue --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Norbert Rittel <norbert@rittel.de>
This commit is contained in:
committed by
GitHub
parent
d99044572a
commit
2bfe96dded
@@ -1,6 +1,7 @@
|
||||
"""Test the integration init functionality."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -10,6 +11,7 @@ from aiohomeconnect.model.error import HomeConnectError, UnauthorizedError
|
||||
import pytest
|
||||
import requests_mock
|
||||
import respx
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.home_connect.const import DOMAIN
|
||||
@@ -22,6 +24,7 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
import homeassistant.helpers.issue_registry as ir
|
||||
from script.hassfest.translations import RE_TRANSLATION_KEY
|
||||
|
||||
from .conftest import (
|
||||
@@ -34,8 +37,9 @@ from .conftest import (
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
SERVICE_KV_CALL_PARAMS = [
|
||||
DEPRECATED_SERVICE_KV_CALL_PARAMS = [
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"service": "set_option_active",
|
||||
@@ -57,6 +61,10 @@ SERVICE_KV_CALL_PARAMS = [
|
||||
},
|
||||
"blocking": True,
|
||||
},
|
||||
]
|
||||
|
||||
SERVICE_KV_CALL_PARAMS = [
|
||||
*DEPRECATED_SERVICE_KV_CALL_PARAMS,
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"service": "change_setting",
|
||||
@@ -125,6 +133,62 @@ SERVICE_APPLIANCE_METHOD_MAPPING = {
|
||||
"start_program": "start_program",
|
||||
}
|
||||
|
||||
SERVICE_VALIDATION_ERROR_MAPPING = {
|
||||
"set_option_active": r"Error.*setting.*options.*active.*program.*",
|
||||
"set_option_selected": r"Error.*setting.*options.*selected.*program.*",
|
||||
"change_setting": r"Error.*assigning.*value.*setting.*",
|
||||
"pause_program": r"Error.*executing.*command.*",
|
||||
"resume_program": r"Error.*executing.*command.*",
|
||||
"select_program": r"Error.*selecting.*program.*",
|
||||
"start_program": r"Error.*starting.*program.*",
|
||||
}
|
||||
|
||||
|
||||
SERVICES_SET_PROGRAM_AND_OPTIONS = [
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"service": "set_program_and_options",
|
||||
"service_data": {
|
||||
"device_id": "DEVICE_ID",
|
||||
"affects_to": "selected_program",
|
||||
"program": "dishcare_dishwasher_program_eco_50",
|
||||
"b_s_h_common_option_start_in_relative": "00:30:00",
|
||||
},
|
||||
"blocking": True,
|
||||
},
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"service": "set_program_and_options",
|
||||
"service_data": {
|
||||
"device_id": "DEVICE_ID",
|
||||
"affects_to": "active_program",
|
||||
"program": "consumer_products_coffee_maker_program_beverage_coffee",
|
||||
"consumer_products_coffee_maker_option_bean_amount": "consumer_products_coffee_maker_enum_type_bean_amount_normal",
|
||||
},
|
||||
"blocking": True,
|
||||
},
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"service": "set_program_and_options",
|
||||
"service_data": {
|
||||
"device_id": "DEVICE_ID",
|
||||
"affects_to": "active_program",
|
||||
"consumer_products_coffee_maker_option_coffee_milk_ratio": 60,
|
||||
},
|
||||
"blocking": True,
|
||||
},
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"service": "set_program_and_options",
|
||||
"service_data": {
|
||||
"device_id": "DEVICE_ID",
|
||||
"affects_to": "selected_program",
|
||||
"consumer_products_coffee_maker_option_fill_quantity": 35,
|
||||
},
|
||||
"blocking": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_entry_setup(
|
||||
hass: HomeAssistant,
|
||||
@@ -244,7 +308,7 @@ async def test_client_error(
|
||||
"service_call",
|
||||
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
async def test_services(
|
||||
async def test_key_value_services(
|
||||
service_call: dict[str, Any],
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
@@ -273,11 +337,188 @@ async def test_services(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_call",
|
||||
DEPRECATED_SERVICE_KV_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
async def test_programs_and_options_actions_deprecation(
|
||||
service_call: dict[str, Any],
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test deprecated service keys."""
|
||||
issue_id = "deprecated_set_program_and_option_actions"
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
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_ha_id)},
|
||||
)
|
||||
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
await hass.services.async_call(**service_call)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert issue
|
||||
|
||||
_client = await hass_client()
|
||||
resp = await _client.post(
|
||||
"/api/repairs/issues/fix",
|
||||
json={"handler": DOMAIN, "issue_id": issue.issue_id},
|
||||
)
|
||||
assert resp.status == HTTPStatus.OK
|
||||
flow_id = (await resp.json())["flow_id"]
|
||||
resp = await _client.post(f"/api/repairs/issues/fix/{flow_id}")
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
await hass.services.async_call(**service_call)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(issue_registry.issues) == 1
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Assert the issue is no longer present
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_call", "called_method"),
|
||||
zip(
|
||||
SERVICES_SET_PROGRAM_AND_OPTIONS,
|
||||
[
|
||||
"set_selected_program",
|
||||
"start_program",
|
||||
"set_active_program_options",
|
||||
"set_selected_program_options",
|
||||
],
|
||||
strict=True,
|
||||
),
|
||||
)
|
||||
async def test_set_program_and_options(
|
||||
service_call: dict[str, Any],
|
||||
called_method: str,
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test recognized options."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
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_ha_id)},
|
||||
)
|
||||
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
await hass.services.async_call(**service_call)
|
||||
await hass.async_block_till_done()
|
||||
method_mock: MagicMock = getattr(client, called_method)
|
||||
assert method_mock.call_count == 1
|
||||
assert method_mock.call_args == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_call", "error_regex"),
|
||||
zip(
|
||||
SERVICES_SET_PROGRAM_AND_OPTIONS,
|
||||
[
|
||||
r"Error.*selecting.*program.*",
|
||||
r"Error.*starting.*program.*",
|
||||
r"Error.*setting.*options.*active.*program.*",
|
||||
r"Error.*setting.*options.*selected.*program.*",
|
||||
],
|
||||
strict=True,
|
||||
),
|
||||
)
|
||||
async def test_set_program_and_options_exceptions(
|
||||
service_call: dict[str, Any],
|
||||
error_regex: str,
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client_with_exception: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
) -> None:
|
||||
"""Test recognized options."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
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, appliance_ha_id)},
|
||||
)
|
||||
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
with pytest.raises(HomeAssistantError, match=error_regex):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
|
||||
async def test_required_program_or_at_least_an_option(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
) -> None:
|
||||
"Test that the set_program_and_options does raise an exception if no program nor options are set."
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
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_ha_id)},
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_program_and_options",
|
||||
{
|
||||
"device_id": device_entry.id,
|
||||
"affects_to": "selected_program",
|
||||
},
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_call",
|
||||
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
async def test_services_exception(
|
||||
async def test_services_exception_device_id(
|
||||
service_call: dict[str, Any],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -348,6 +589,40 @@ async def test_services_appliance_not_found(
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_call",
|
||||
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
async def test_services_exception(
|
||||
service_call: dict[str, Any],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client_with_exception: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Raise a ValueError when device id does not match."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
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, appliance_ha_id)},
|
||||
)
|
||||
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
|
||||
service_name = service_call["service"]
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=SERVICE_VALIDATION_ERROR_MAPPING[service_name],
|
||||
):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
|
||||
async def test_entity_migration(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
||||
Reference in New Issue
Block a user