Remove deprecated services from home_connect

This commit is contained in:
G Johansson
2025-08-25 16:47:31 +00:00
parent f6d23b9b34
commit 3b15a191dc
2 changed files with 4 additions and 463 deletions

View File

@@ -8,7 +8,6 @@ from typing import Any, cast
from aiohomeconnect.client import Client as HomeConnectClient
from aiohomeconnect.model import (
ArrayOfOptions,
CommandKey,
Option,
OptionKey,
ProgramKey,
@@ -21,7 +20,6 @@ from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import (
AFFECTS_TO_ACTIVE_PROGRAM,
@@ -29,18 +27,11 @@ from .const import (
ATTR_AFFECTS_TO,
ATTR_KEY,
ATTR_PROGRAM,
ATTR_UNIT,
ATTR_VALUE,
DOMAIN,
PROGRAM_ENUM_OPTIONS,
SERVICE_OPTION_ACTIVE,
SERVICE_OPTION_SELECTED,
SERVICE_PAUSE_PROGRAM,
SERVICE_RESUME_PROGRAM,
SERVICE_SELECT_PROGRAM,
SERVICE_SET_PROGRAM_AND_OPTIONS,
SERVICE_SETTING,
SERVICE_START_PROGRAM,
TRANSLATION_KEYS_PROGRAMS_MAP,
)
from .coordinator import HomeConnectConfigEntry
@@ -88,43 +79,6 @@ SERVICE_SETTING_SCHEMA = vol.Schema(
}
)
# DEPRECATED: Remove in 2025.9.0
SERVICE_OPTION_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): str,
vol.Required(ATTR_KEY): vol.All(
vol.Coerce(OptionKey),
vol.NotIn([OptionKey.UNKNOWN]),
),
vol.Required(ATTR_VALUE): vol.Any(str, int, bool),
vol.Optional(ATTR_UNIT): str,
}
)
# DEPRECATED: Remove in 2025.9.0
SERVICE_PROGRAM_SCHEMA = vol.Any(
{
vol.Required(ATTR_DEVICE_ID): str,
vol.Required(ATTR_PROGRAM): vol.All(
vol.Coerce(ProgramKey),
vol.NotIn([ProgramKey.UNKNOWN]),
),
vol.Required(ATTR_KEY): vol.All(
vol.Coerce(OptionKey),
vol.NotIn([OptionKey.UNKNOWN]),
),
vol.Required(ATTR_VALUE): vol.Any(int, str),
vol.Optional(ATTR_UNIT): str,
},
{
vol.Required(ATTR_DEVICE_ID): str,
vol.Required(ATTR_PROGRAM): vol.All(
vol.Coerce(ProgramKey),
vol.NotIn([ProgramKey.UNKNOWN]),
),
},
)
def _require_program_or_at_least_one_option(data: dict) -> dict:
if ATTR_PROGRAM not in data and not any(
@@ -165,8 +119,6 @@ SERVICE_PROGRAM_AND_OPTIONS_SCHEMA = vol.All(
_require_program_or_at_least_one_option,
)
SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
async def _get_client_and_ha_id(
hass: HomeAssistant, device_id: str
@@ -216,205 +168,6 @@ async def _get_client_and_ha_id(
return entry.runtime_data.client, ha_id
async def _async_service_program(call: ServiceCall, start: bool) -> None:
"""Execute calls to services taking a program."""
program = call.data[ATTR_PROGRAM]
client, ha_id = await _get_client_and_ha_id(call.hass, call.data[ATTR_DEVICE_ID])
option_key = call.data.get(ATTR_KEY)
options = (
[
Option(
option_key,
call.data[ATTR_VALUE],
unit=call.data.get(ATTR_UNIT),
)
]
if option_key is not None
else None
)
async_create_issue(
call.hass,
DOMAIN,
"deprecated_set_program_and_option_actions",
breaks_in_ha_version="2025.9.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_set_program_and_option_actions",
translation_placeholders={
"new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS,
"remove_release": "2025.9.0",
"deprecated_action_yaml": "\n".join(
[
"```yaml",
f"action: {DOMAIN}.{SERVICE_START_PROGRAM if start else SERVICE_SELECT_PROGRAM}",
"data:",
f" {ATTR_DEVICE_ID}: DEVICE_ID",
f" {ATTR_PROGRAM}: {program}",
*([f" {ATTR_KEY}: {options[0].key}"] if options else []),
*([f" {ATTR_VALUE}: {options[0].value}"] if options else []),
*(
[f" {ATTR_UNIT}: {options[0].unit}"]
if options and options[0].unit
else []
),
"```",
]
),
"new_action_yaml": "\n ".join(
[
"```yaml",
f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}",
"data:",
f" {ATTR_DEVICE_ID}: DEVICE_ID",
f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if start else AFFECTS_TO_SELECTED_PROGRAM}",
f" {ATTR_PROGRAM}: {bsh_key_to_translation_key(program.value)}",
*(
[
f" {bsh_key_to_translation_key(options[0].key)}: {options[0].value}"
]
if options
else []
),
"```",
]
),
"repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)",
},
)
try:
if start:
await client.start_program(ha_id, program_key=program, options=options)
else:
await client.set_selected_program(
ha_id, program_key=program, options=options
)
except HomeConnectError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="start_program" if start else "select_program",
translation_placeholders={
**get_dict_from_home_connect_error(err),
"program": program,
},
) from err
async def _async_service_set_program_options(call: ServiceCall, active: bool) -> None:
"""Execute calls to services taking a program."""
option_key = call.data[ATTR_KEY]
value = call.data[ATTR_VALUE]
unit = call.data.get(ATTR_UNIT)
client, ha_id = await _get_client_and_ha_id(call.hass, call.data[ATTR_DEVICE_ID])
async_create_issue(
call.hass,
DOMAIN,
"deprecated_set_program_and_option_actions",
breaks_in_ha_version="2025.9.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_set_program_and_option_actions",
translation_placeholders={
"new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS,
"remove_release": "2025.9.0",
"deprecated_action_yaml": "\n".join(
[
"```yaml",
f"action: {DOMAIN}.{SERVICE_OPTION_ACTIVE if active else SERVICE_OPTION_SELECTED}",
"data:",
f" {ATTR_DEVICE_ID}: DEVICE_ID",
f" {ATTR_KEY}: {option_key}",
f" {ATTR_VALUE}: {value}",
*([f" {ATTR_UNIT}: {unit}"] if unit else []),
"```",
]
),
"new_action_yaml": "\n ".join(
[
"```yaml",
f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}",
"data:",
f" {ATTR_DEVICE_ID}: DEVICE_ID",
f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if active else AFFECTS_TO_SELECTED_PROGRAM}",
f" {bsh_key_to_translation_key(option_key)}: {value}",
"```",
]
),
"repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)",
},
)
try:
if active:
await client.set_active_program_option(
ha_id,
option_key=option_key,
value=value,
unit=unit,
)
else:
await client.set_selected_program_option(
ha_id,
option_key=option_key,
value=value,
unit=unit,
)
except HomeConnectError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_options_active_program"
if active
else "set_options_selected_program",
translation_placeholders={
**get_dict_from_home_connect_error(err),
"key": option_key,
"value": str(value),
},
) from err
async def _async_service_command(call: ServiceCall, command_key: CommandKey) -> None:
"""Execute calls to services executing a command."""
client, ha_id = await _get_client_and_ha_id(call.hass, call.data[ATTR_DEVICE_ID])
async_create_issue(
call.hass,
DOMAIN,
"deprecated_command_actions",
breaks_in_ha_version="2025.9.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_command_actions",
)
try:
await client.put_command(ha_id, command_key=command_key, value=True)
except HomeConnectError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="execute_command",
translation_placeholders={
**get_dict_from_home_connect_error(err),
"command": command_key.value,
},
) from err
async def async_service_option_active(call: ServiceCall) -> None:
"""Service for setting an option for an active program."""
await _async_service_set_program_options(call, True)
async def async_service_option_selected(call: ServiceCall) -> None:
"""Service for setting an option for a selected program."""
await _async_service_set_program_options(call, False)
async def async_service_setting(call: ServiceCall) -> None:
"""Service for changing a setting."""
key = call.data[ATTR_KEY]
@@ -435,21 +188,6 @@ async def async_service_setting(call: ServiceCall) -> None:
) from err
async def async_service_pause_program(call: ServiceCall) -> None:
"""Service for pausing a program."""
await _async_service_command(call, CommandKey.BSH_COMMON_PAUSE_PROGRAM)
async def async_service_resume_program(call: ServiceCall) -> None:
"""Service for resuming a paused program."""
await _async_service_command(call, CommandKey.BSH_COMMON_RESUME_PROGRAM)
async def async_service_select_program(call: ServiceCall) -> None:
"""Service for selecting a program."""
await _async_service_program(call, False)
async def async_service_set_program_and_options(call: ServiceCall) -> None:
"""Service for setting a program and options."""
data = dict(call.data)
@@ -517,54 +255,13 @@ async def async_service_set_program_and_options(call: ServiceCall) -> None:
) from err
async def async_service_start_program(call: ServiceCall) -> None:
"""Service for starting a program."""
await _async_service_program(call, True)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register custom actions."""
hass.services.async_register(
DOMAIN,
SERVICE_OPTION_ACTIVE,
async_service_option_active,
schema=SERVICE_OPTION_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_OPTION_SELECTED,
async_service_option_selected,
schema=SERVICE_OPTION_SCHEMA,
)
hass.services.async_register(
DOMAIN, SERVICE_SETTING, async_service_setting, schema=SERVICE_SETTING_SCHEMA
)
hass.services.async_register(
DOMAIN,
SERVICE_PAUSE_PROGRAM,
async_service_pause_program,
schema=SERVICE_COMMAND_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_RESUME_PROGRAM,
async_service_resume_program,
schema=SERVICE_COMMAND_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_SELECT_PROGRAM,
async_service_select_program,
schema=SERVICE_PROGRAM_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_START_PROGRAM,
async_service_start_program,
schema=SERVICE_PROGRAM_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_SET_PROGRAM_AND_OPTIONS,

View File

@@ -1,11 +1,10 @@
"""Tests for the Home Connect actions."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus
from typing import Any
from unittest.mock import MagicMock
from aiohomeconnect.model import HomeAppliance, OptionKey, ProgramKey, SettingKey
from aiohomeconnect.model import HomeAppliance, SettingKey
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -14,37 +13,10 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.issue_registry as ir
from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator
DEPRECATED_SERVICE_KV_CALL_PARAMS = [
{
"domain": DOMAIN,
"service": "set_option_active",
"service_data": {
"device_id": "DEVICE_ID",
"key": OptionKey.BSH_COMMON_FINISH_IN_RELATIVE.value,
"value": 43200,
"unit": "seconds",
},
"blocking": True,
},
{
"domain": DOMAIN,
"service": "set_option_selected",
"service_data": {
"device_id": "DEVICE_ID",
"key": OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE.value,
"value": "LaundryCare.Washer.EnumType.Temperature.GC40",
},
"blocking": True,
},
]
SERVICE_KV_CALL_PARAMS = [
*DEPRECATED_SERVICE_KV_CALL_PARAMS,
{
"domain": DOMAIN,
"service": "change_setting",
@@ -57,70 +29,17 @@ SERVICE_KV_CALL_PARAMS = [
},
]
SERVICE_COMMAND_CALL_PARAMS = [
{
"domain": DOMAIN,
"service": "pause_program",
"service_data": {
"device_id": "DEVICE_ID",
},
"blocking": True,
},
{
"domain": DOMAIN,
"service": "resume_program",
"service_data": {
"device_id": "DEVICE_ID",
},
"blocking": True,
},
]
SERVICE_PROGRAM_CALL_PARAMS = [
{
"domain": DOMAIN,
"service": "select_program",
"service_data": {
"device_id": "DEVICE_ID",
"program": ProgramKey.LAUNDRY_CARE_WASHER_COTTON.value,
"key": OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE.value,
"value": "LaundryCare.Washer.EnumType.Temperature.GC40",
},
"blocking": True,
},
{
"domain": DOMAIN,
"service": "start_program",
"service_data": {
"device_id": "DEVICE_ID",
"program": ProgramKey.LAUNDRY_CARE_WASHER_COTTON.value,
"key": OptionKey.BSH_COMMON_FINISH_IN_RELATIVE.value,
"value": 43200,
"unit": "seconds",
},
"blocking": True,
},
]
SERVICE_APPLIANCE_METHOD_MAPPING = {
"set_option_active": "set_active_program_option",
"set_option_selected": "set_selected_program_option",
"change_setting": "set_setting",
"pause_program": "put_command",
"resume_program": "put_command",
"select_program": "set_selected_program",
"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.*",
}
@@ -173,7 +92,7 @@ SERVICES_SET_PROGRAM_AND_OPTIONS = [
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
@pytest.mark.parametrize(
"service_call",
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
SERVICE_KV_CALL_PARAMS,
)
async def test_key_value_services(
hass: HomeAssistant,
@@ -202,81 +121,6 @@ async def test_key_value_services(
)
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
@pytest.mark.parametrize(
("service_call", "issue_id"),
[
*zip(
DEPRECATED_SERVICE_KV_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
["deprecated_set_program_and_option_actions"]
* (
len(DEPRECATED_SERVICE_KV_CALL_PARAMS)
+ len(SERVICE_PROGRAM_CALL_PARAMS)
),
strict=True,
),
*zip(
SERVICE_COMMAND_CALL_PARAMS,
["deprecated_command_actions"] * len(SERVICE_COMMAND_CALL_PARAMS),
strict=True,
),
],
)
async def test_programs_and_options_actions_deprecation(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
device_registry: dr.DeviceRegistry,
issue_registry: ir.IssueRegistry,
client: MagicMock,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
appliance: HomeAppliance,
service_call: dict[str, Any],
issue_id: str,
) -> None:
"""Test deprecated service keys."""
assert await integration_setup(client)
assert config_entry.state is 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("appliance", ["Washer"], indirect=True)
@pytest.mark.parametrize(
("service_call", "called_method"),
@@ -360,7 +204,7 @@ async def test_set_program_and_options_exceptions(
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
@pytest.mark.parametrize(
"service_call",
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
SERVICE_KV_CALL_PARAMS,
)
async def test_services_exception_device_id(
hass: HomeAssistant,
@@ -430,7 +274,7 @@ async def test_services_appliance_not_found(
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
@pytest.mark.parametrize(
"service_call",
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
SERVICE_KV_CALL_PARAMS,
)
async def test_services_exception(
hass: HomeAssistant,