Move Home Connect alarm clock entity from time platform to number platform (#141400)

* Move alarm clock entity from time platform to number platform

* Deprecate alarm clock time entity

* Don't update unique id

* Fix tests

* Fixable issues

* improvement

* Make the issues persistent
This commit is contained in:
J. Diego Rodríguez Royo 2025-03-26 14:46:07 +01:00 committed by GitHub
parent c974285490
commit b5910dd7d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 294 additions and 1 deletions

View File

@ -26,6 +26,11 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
NUMBERS = ( NUMBERS = (
NumberEntityDescription(
key=SettingKey.BSH_COMMON_ALARM_CLOCK,
device_class=NumberDeviceClass.DURATION,
translation_key="alarm_clock",
),
NumberEntityDescription( NumberEntityDescription(
key=SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR, key=SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
device_class=NumberDeviceClass.TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE,

View File

@ -110,6 +110,28 @@
} }
}, },
"issues": { "issues": {
"deprecated_time_alarm_clock_in_automations_scripts": {
"title": "Deprecated alarm clock entity detected in some automations or scripts",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_time_alarm_clock_in_automations_scripts::title%]",
"description": "The alarm clock entity `{entity_id}`, which is deprecated because it's being moved to the `number` platform, is used in the following automations or scripts:\n{items}\n\nPlease, fix this issue by updating your automations or scripts to use the new `number` entity."
}
}
}
},
"deprecated_time_alarm_clock": {
"title": "Deprecated alarm clock entity",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_time_alarm_clock::title%]",
"description": "The alarm clock entity `{entity_id}` is deprecated because it's being moved to the `number` platform.\n\nPlease use the new `number` entity."
}
}
}
},
"deprecated_binary_common_door_sensor": { "deprecated_binary_common_door_sensor": {
"title": "Deprecated binary door sensor detected in some automations or scripts", "title": "Deprecated binary door sensor detected in some automations or scripts",
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue." "description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
@ -868,6 +890,9 @@
} }
}, },
"number": { "number": {
"alarm_clock": {
"name": "Alarm clock"
},
"refrigerator_setpoint_temperature": { "refrigerator_setpoint_temperature": {
"name": "Refrigerator temperature" "name": "Refrigerator temperature"
}, },

View File

@ -6,10 +6,18 @@ from typing import cast
from aiohomeconnect.model import SettingKey from aiohomeconnect.model import SettingKey
from aiohomeconnect.model.error import HomeConnectError from aiohomeconnect.model.error import HomeConnectError
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.time import TimeEntity, TimeEntityDescription from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from .common import setup_home_connect_entry from .common import setup_home_connect_entry
from .const import DOMAIN from .const import DOMAIN
@ -23,6 +31,7 @@ TIME_ENTITIES = (
TimeEntityDescription( TimeEntityDescription(
key=SettingKey.BSH_COMMON_ALARM_CLOCK, key=SettingKey.BSH_COMMON_ALARM_CLOCK,
translation_key="alarm_clock", translation_key="alarm_clock",
entity_registry_enabled_default=False,
), ),
) )
@ -67,8 +76,78 @@ def time_to_seconds(t: time) -> int:
class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity): class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
"""Time setting class for Home Connect.""" """Time setting class for Home Connect."""
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
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_time_alarm_clock_in_automations_scripts_{self.entity_id}",
breaks_in_ha_version="2025.10.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_time_alarm_clock",
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."""
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
async_delete_issue(
self.hass,
DOMAIN,
f"deprecated_time_alarm_clock_in_automations_scripts_{self.entity_id}",
)
async_delete_issue(
self.hass, DOMAIN, f"deprecated_time_alarm_clock_{self.entity_id}"
)
async def async_set_value(self, value: time) -> None: async def async_set_value(self, value: time) -> None:
"""Set the native value of the entity.""" """Set the native value of the entity."""
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_time_alarm_clock_{self.entity_id}",
breaks_in_ha_version="2025.10.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_time_alarm_clock",
translation_placeholders={
"entity_id": self.entity_id,
},
)
try: try:
await self.coordinator.client.set_setting( await self.coordinator.client.set_setting(
self.appliance.info.ha_id, self.appliance.info.ha_id,

View File

@ -2,6 +2,7 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from datetime import time from datetime import time
from http import HTTPStatus
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from aiohomeconnect.model import ( from aiohomeconnect.model import (
@ -16,15 +17,26 @@ from aiohomeconnect.model import (
from aiohomeconnect.model.error import HomeConnectApiError, HomeConnectError from aiohomeconnect.model.error import HomeConnectApiError, HomeConnectError
import pytest import pytest
from homeassistant.components.automation import (
DOMAIN as AUTOMATION_DOMAIN,
automations_with_entity,
)
from homeassistant.components.home_connect.const import DOMAIN from homeassistant.components.home_connect.const import DOMAIN
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN, scripts_with_entity
from homeassistant.components.time import DOMAIN as TIME_DOMAIN, SERVICE_SET_VALUE from homeassistant.components.time import DOMAIN as TIME_DOMAIN, SERVICE_SET_VALUE
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, STATE_UNAVAILABLE, Platform from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator
@pytest.fixture @pytest.fixture
@ -45,6 +57,7 @@ async def test_time(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True) @pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
async def test_paired_depaired_devices_flow( async def test_paired_depaired_devices_flow(
appliance: HomeAppliance, appliance: HomeAppliance,
@ -99,6 +112,7 @@ async def test_paired_depaired_devices_flow(
assert entity_registry.async_get(entity_entry.entity_id) assert entity_registry.async_get(entity_entry.entity_id)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True) @pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
async def test_connected_devices( async def test_connected_devices(
appliance: HomeAppliance, appliance: HomeAppliance,
@ -151,6 +165,7 @@ async def test_connected_devices(
assert len(new_entity_entries) > len(entity_entries) assert len(new_entity_entries) > len(entity_entries)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True) @pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
async def test_time_entity_availability( async def test_time_entity_availability(
hass: HomeAssistant, hass: HomeAssistant,
@ -204,6 +219,7 @@ async def test_time_entity_availability(
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True) @pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_id", "setting_key"), ("entity_id", "setting_key"),
@ -248,6 +264,7 @@ async def test_time_entity_functionality(
assert hass.states.is_state(entity_id, str(time(second=value))) assert hass.states.is_state(entity_id, str(time(second=value)))
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_id", "setting_key", "mock_attr"), ("entity_id", "setting_key", "mock_attr"),
[ [
@ -299,3 +316,170 @@ async def test_time_entity_error(
blocking=True, blocking=True,
) )
assert getattr(client_with_exception, mock_attr).call_count == 2 assert getattr(client_with_exception, mock_attr).call_count == 2
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
async def test_create_issue(
hass: HomeAssistant,
appliance: HomeAppliance,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test we create an issue when an automation or script is using a deprecated entity."""
entity_id = f"{TIME_DOMAIN}.oven_alarm_clock"
automation_script_issue_id = (
f"deprecated_time_alarm_clock_in_automations_scripts_{entity_id}"
)
action_handler_issue_id = f"deprecated_time_alarm_clock_{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 config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
await hass.services.async_call(
TIME_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: entity_id,
ATTR_TIME: time(minute=1),
},
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("appliance", ["Oven"], indirect=True)
async def test_issue_fix(
hass: HomeAssistant,
appliance: HomeAppliance,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
issue_registry: ir.IssueRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test we create an issue when an automation or script is using a deprecated entity."""
entity_id = f"{TIME_DOMAIN}.oven_alarm_clock"
automation_script_issue_id = (
f"deprecated_time_alarm_clock_in_automations_scripts_{entity_id}"
)
action_handler_issue_id = f"deprecated_time_alarm_clock_{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 config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
await hass.services.async_call(
TIME_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: entity_id,
ATTR_TIME: time(minute=1),
},
blocking=True,
)
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