Delete deprecated program switches from Home Connect (#144606)

pull/144610/head
J. Diego Rodríguez Royo 2025-05-10 10:53:16 +02:00 committed by GitHub
parent 45c0a19a68
commit 86cf01a901
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 4 additions and 457 deletions

View File

@ -3,31 +3,18 @@
import logging
from typing import Any, cast
from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey
from aiohomeconnect.model import OptionKey, SettingKey
from aiohomeconnect.model.error import HomeConnectError
from aiohomeconnect.model.program import EnumerateProgram
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from .common import setup_home_connect_entry
from .const import BSH_POWER_OFF, BSH_POWER_ON, BSH_POWER_STANDBY, DOMAIN
from .coordinator import (
HomeConnectApplianceData,
HomeConnectConfigEntry,
HomeConnectCoordinator,
)
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
from .entity import HomeConnectEntity, HomeConnectOptionEntity
from .utils import get_dict_from_home_connect_error
@ -154,11 +141,6 @@ def _get_entities_for_appliance(
) -> list[HomeConnectEntity]:
"""Get a list of entities."""
entities: list[HomeConnectEntity] = []
entities.extend(
HomeConnectProgramSwitch(entry.runtime_data, appliance, program)
for program in appliance.programs
if program.key != ProgramKey.UNKNOWN
)
if SettingKey.BSH_COMMON_POWER_STATE in appliance.settings:
entities.append(
HomeConnectPowerSwitch(
@ -247,142 +229,6 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
self._attr_is_on = self.appliance.settings[SettingKey(self.bsh_key)].value
class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
"""Switch class for Home Connect."""
def __init__(
self,
coordinator: HomeConnectCoordinator,
appliance: HomeConnectApplianceData,
program: EnumerateProgram,
) -> None:
"""Initialize the entity."""
desc = " ".join(["Program", program.key.split(".")[-1]])
if appliance.info.type == "WasherDryer":
desc = " ".join(
["Program", program.key.split(".")[-3], program.key.split(".")[-1]]
)
self.program = program
super().__init__(
coordinator,
appliance,
SwitchEntityDescription(
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
entity_registry_enabled_default=False,
),
)
self._attr_name = f"{appliance.info.name} {desc}"
self._attr_unique_id = f"{appliance.info.ha_id}-{desc}"
self._attr_has_entity_name = False
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
automations = automations_with_entity(self.hass, self.entity_id)
scripts = scripts_with_entity(self.hass, self.entity_id)
items = automations + scripts
if not items:
return
entity_reg: er.EntityRegistry = er.async_get(self.hass)
entity_automations = [
automation_entity
for automation_id in automations
if (automation_entity := entity_reg.async_get(automation_id))
]
entity_scripts = [
script_entity
for script_id in scripts
if (script_entity := entity_reg.async_get(script_id))
]
items_list = [
f"- [{item.original_name}](/config/automation/edit/{item.unique_id})"
for item in entity_automations
] + [
f"- [{item.original_name}](/config/script/edit/{item.unique_id})"
for item in entity_scripts
]
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
breaks_in_ha_version="2025.6.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_program_switch_in_automations_scripts",
translation_placeholders={
"entity_id": self.entity_id,
"items": "\n".join(items_list),
},
)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
async_delete_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
)
async_delete_issue(
self.hass, DOMAIN, f"deprecated_program_switch_{self.entity_id}"
)
def create_action_handler_issue(self) -> None:
"""Create deprecation issue."""
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_{self.entity_id}",
breaks_in_ha_version="2025.6.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_program_switch",
translation_placeholders={
"entity_id": self.entity_id,
},
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start the program."""
self.create_action_handler_issue()
try:
await self.coordinator.client.start_program(
self.appliance.info.ha_id, program_key=self.program.key
)
except HomeConnectError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="start_program",
translation_placeholders={
**get_dict_from_home_connect_error(err),
"program": self.program.key,
},
) from err
async def async_turn_off(self, **kwargs: Any) -> None:
"""Stop the program."""
self.create_action_handler_issue()
try:
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
except HomeConnectError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="stop_program",
translation_placeholders={
**get_dict_from_home_connect_error(err),
},
) from err
def update_native_value(self) -> None:
"""Update the switch's status based on if the program related to this entity is currently active."""
event = self.appliance.events.get(EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM)
self._attr_is_on = bool(event and event.value == self.program.key)
class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
"""Power switch class for Home Connect."""

View File

@ -1,16 +1,12 @@
"""Tests for home_connect sensor entities."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from aiohomeconnect.model import (
ArrayOfEvents,
ArrayOfPrograms,
ArrayOfSettings,
Event,
EventKey,
EventMessage,
EventType,
GetSetting,
@ -26,19 +22,16 @@ from aiohomeconnect.model.error import (
HomeConnectError,
SelectedProgramNotSetError,
)
from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption
from aiohomeconnect.model.program import ProgramDefinitionOption
from aiohomeconnect.model.setting import SettingConstraints
import pytest
from homeassistant.components import automation, script
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.home_connect.const import (
BSH_POWER_OFF,
BSH_POWER_ON,
BSH_POWER_STANDBY,
DOMAIN,
)
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
@ -52,15 +45,9 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.setup import async_setup_component
from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator
@pytest.fixture
@ -80,17 +67,6 @@ async def test_paired_depaired_devices_flow(
appliance: HomeAppliance,
) -> None:
"""Test that removed devices are correctly removed from and added to hass on API events."""
client.get_available_program = AsyncMock(
return_value=ProgramDefinition(
ProgramKey.UNKNOWN,
options=[
ProgramDefinitionOption(
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE,
"Boolean",
)
],
)
)
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED
@ -140,7 +116,6 @@ async def test_paired_depaired_devices_flow(
(
SettingKey.BSH_COMMON_POWER_STATE,
SettingKey.BSH_COMMON_CHILD_LOCK,
"Program Cotton",
),
)
],
@ -162,7 +137,6 @@ async def test_connected_devices(
not be obtained while disconnected and once connected, the entities are added.
"""
get_settings_original_mock = client.get_settings
get_all_programs_mock = client.get_all_programs
async def get_settings_side_effect(ha_id: str):
if ha_id == appliance.ha_id:
@ -171,19 +145,10 @@ async def test_connected_devices(
)
return await get_settings_original_mock.side_effect(ha_id)
async def get_all_programs_side_effect(ha_id: str):
if ha_id == appliance.ha_id:
raise HomeConnectApiError(
"SDK.Error.HomeAppliance.Connection.Initialization.Failed"
)
return await get_all_programs_mock.side_effect(ha_id)
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect)
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED
client.get_settings = get_settings_original_mock
client.get_all_programs = get_all_programs_mock
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
assert device
@ -226,7 +191,6 @@ async def test_switch_entity_availability(
entity_ids = [
"switch.dishwasher_power",
"switch.dishwasher_child_lock",
"switch.dishwasher_program_eco50",
]
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED
@ -321,82 +285,6 @@ async def test_switch_functionality(
assert hass.states.is_state(entity_id, state)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("entity_id", "program_key", "initial_state", "appliance"),
[
(
"switch.dryer_program_mix",
ProgramKey.LAUNDRY_CARE_DRYER_MIX,
STATE_OFF,
"Dryer",
),
(
"switch.dryer_program_cotton",
ProgramKey.LAUNDRY_CARE_DRYER_COTTON,
STATE_ON,
"Dryer",
),
],
indirect=["appliance"],
)
async def test_program_switch_functionality(
hass: HomeAssistant,
client: MagicMock,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
entity_id: str,
program_key: ProgramKey,
initial_state: str,
appliance: HomeAppliance,
) -> None:
"""Test switch functionality."""
async def mock_stop_program(ha_id: str) -> None:
"""Mock stop program."""
await client.add_events(
[
EventMessage(
ha_id,
EventType.NOTIFY,
ArrayOfEvents(
[
Event(
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
raw_key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM.value,
timestamp=0,
level="",
handling="",
value=ProgramKey.UNKNOWN,
)
]
),
),
]
)
client.stop_program = AsyncMock(side_effect=mock_stop_program)
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED
assert hass.states.is_state(entity_id, initial_state)
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}
)
await hass.async_block_till_done()
assert hass.states.is_state(entity_id, STATE_ON)
client.start_program.assert_awaited_once_with(
appliance.ha_id, program_key=program_key
)
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}
)
await hass.async_block_till_done()
assert hass.states.is_state(entity_id, STATE_OFF)
client.stop_program.assert_awaited_once_with(appliance.ha_id)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
(
@ -406,18 +294,6 @@ async def test_program_switch_functionality(
"exception_match",
),
[
(
"switch.dishwasher_program_eco50",
SERVICE_TURN_ON,
"start_program",
r"Error.*start.*program.*",
),
(
"switch.dishwasher_program_eco50",
SERVICE_TURN_OFF,
"stop_program",
r"Error.*stop.*program.*",
),
(
"switch.dishwasher_power",
SERVICE_TURN_OFF,
@ -455,15 +331,6 @@ async def test_switch_exception_handling(
exception_match: str,
) -> None:
"""Test exception handling."""
client_with_exception.get_all_programs.side_effect = None
client_with_exception.get_all_programs.return_value = ArrayOfPrograms(
[
EnumerateProgram(
key=ProgramKey.DISHCARE_DISHWASHER_ECO_50,
raw_key=ProgramKey.DISHCARE_DISHWASHER_ECO_50.value,
)
]
)
client_with_exception.get_settings.side_effect = None
client_with_exception.get_settings.return_value = ArrayOfSettings(
[
@ -780,172 +647,6 @@ async def test_power_switch_service_validation_errors(
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
"service",
[SERVICE_TURN_ON, SERVICE_TURN_OFF],
)
async def test_create_program_switch_deprecation_issue(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
client: MagicMock,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
service: str,
) -> None:
"""Test that we create an issue when an automation or script is using a program switch entity or the entity is used by the user."""
entity_id = "switch.washer_program_mix"
automation_script_issue_id = f"deprecated_program_switch_{entity_id}"
action_handler_issue_id = f"deprecated_program_switch_{entity_id}"
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"alias": "test",
"trigger": {"platform": "state", "entity_id": entity_id},
"action": {
"action": "automation.turn_on",
"target": {
"entity_id": "automation.test",
},
},
}
},
)
assert await async_setup_component(
hass,
script.DOMAIN,
{
script.DOMAIN: {
"test": {
"sequence": [
{
"action": "switch.turn_on",
"entity_id": entity_id,
},
],
}
}
},
)
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{
ATTR_ENTITY_ID: entity_id,
},
blocking=True,
)
assert automations_with_entity(hass, entity_id)[0] == "automation.test"
assert scripts_with_entity(hass, entity_id)[0] == "script.test"
assert len(issue_registry.issues) == 2
assert issue_registry.async_get_issue(DOMAIN, automation_script_issue_id)
assert issue_registry.async_get_issue(DOMAIN, action_handler_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, automation_script_issue_id)
assert not issue_registry.async_get_issue(DOMAIN, action_handler_issue_id)
assert len(issue_registry.issues) == 0
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
"service",
[SERVICE_TURN_ON, SERVICE_TURN_OFF],
)
async def test_program_switch_deprecation_issue_fix(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
client: MagicMock,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
service: str,
) -> None:
"""Test we can fix the issues created when a program switch entity is in an automation or in a script or when is used."""
entity_id = "switch.washer_program_mix"
automation_script_issue_id = f"deprecated_program_switch_{entity_id}"
action_handler_issue_id = f"deprecated_program_switch_{entity_id}"
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"alias": "test",
"trigger": {"platform": "state", "entity_id": entity_id},
"action": {
"action": "automation.turn_on",
"target": {
"entity_id": "automation.test",
},
},
}
},
)
assert await async_setup_component(
hass,
script.DOMAIN,
{
script.DOMAIN: {
"test": {
"sequence": [
{
"action": "switch.turn_on",
"entity_id": entity_id,
},
],
}
}
},
)
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{
ATTR_ENTITY_ID: entity_id,
},
blocking=True,
)
assert automations_with_entity(hass, entity_id)[0] == "automation.test"
assert scripts_with_entity(hass, entity_id)[0] == "script.test"
assert len(issue_registry.issues) == 2
assert issue_registry.async_get_issue(DOMAIN, automation_script_issue_id)
assert issue_registry.async_get_issue(DOMAIN, action_handler_issue_id)
for issue in issue_registry.issues.copy().values():
_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 the issue is no longer present
assert not issue_registry.async_get_issue(DOMAIN, automation_script_issue_id)
assert not issue_registry.async_get_issue(DOMAIN, action_handler_issue_id)
assert len(issue_registry.issues) == 0
@pytest.mark.parametrize(
(
"set_active_program_options_side_effect",