Skip to content

Commit 275bbc8

Browse files
Diegorro98joostlek
andauthored
Add Time platform with alarm clock to Home Connect (#126155)
Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent beafcf7 commit 275bbc8

File tree

4 files changed

+256
-1
lines changed

4 files changed

+256
-1
lines changed

homeassistant/components/home_connect/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,13 @@
7979

8080
SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
8181

82-
PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
82+
PLATFORMS = [
83+
Platform.BINARY_SENSOR,
84+
Platform.LIGHT,
85+
Platform.SENSOR,
86+
Platform.SWITCH,
87+
Platform.TIME,
88+
]
8389

8490

8591
def _get_appliance_by_device_id(

homeassistant/components/home_connect/strings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,11 @@
357357
"door_assistant_freezer": {
358358
"name": "Freezer door assistant"
359359
}
360+
},
361+
"time": {
362+
"alarm_clock": {
363+
"name": "Alarm clock"
364+
}
360365
}
361366
}
362367
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Provides time enties for Home Connect."""
2+
3+
from datetime import time
4+
import logging
5+
6+
from homeconnect.api import HomeConnectError
7+
8+
from homeassistant.components.time import TimeEntity, TimeEntityDescription
9+
from homeassistant.config_entries import ConfigEntry
10+
from homeassistant.core import HomeAssistant
11+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
12+
13+
from .api import ConfigEntryAuth
14+
from .const import ATTR_VALUE, DOMAIN
15+
from .entity import HomeConnectEntity
16+
17+
_LOGGER = logging.getLogger(__name__)
18+
19+
20+
TIME_ENTITIES = (
21+
TimeEntityDescription(
22+
key="BSH.Common.Setting.AlarmClock",
23+
translation_key="alarm_clock",
24+
),
25+
)
26+
27+
28+
async def async_setup_entry(
29+
hass: HomeAssistant,
30+
config_entry: ConfigEntry,
31+
async_add_entities: AddEntitiesCallback,
32+
) -> None:
33+
"""Set up the Home Connect switch."""
34+
35+
def get_entities() -> list[HomeConnectTimeEntity]:
36+
"""Get a list of entities."""
37+
hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
38+
return [
39+
HomeConnectTimeEntity(device, description)
40+
for description in TIME_ENTITIES
41+
for device in hc_api.devices
42+
if description.key in device.appliance.status
43+
]
44+
45+
async_add_entities(await hass.async_add_executor_job(get_entities), True)
46+
47+
48+
def seconds_to_time(seconds: int) -> time:
49+
"""Convert seconds to a time object."""
50+
minutes, sec = divmod(seconds, 60)
51+
hours, minutes = divmod(minutes, 60)
52+
return time(hour=hours, minute=minutes, second=sec)
53+
54+
55+
def time_to_seconds(t: time) -> int:
56+
"""Convert a time object to seconds."""
57+
return t.hour * 3600 + t.minute * 60 + t.second
58+
59+
60+
class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
61+
"""Time setting class for Home Connect."""
62+
63+
async def async_set_value(self, value: time) -> None:
64+
"""Set the native value of the entity."""
65+
_LOGGER.debug(
66+
"Tried to set value %s to %s for %s",
67+
value,
68+
self.bsh_key,
69+
self.entity_id,
70+
)
71+
try:
72+
await self.hass.async_add_executor_job(
73+
self.device.appliance.set_setting,
74+
self.bsh_key,
75+
time_to_seconds(value),
76+
)
77+
except HomeConnectError as err:
78+
_LOGGER.error(
79+
"Error setting value %s to %s for %s: %s",
80+
value,
81+
self.bsh_key,
82+
self.entity_id,
83+
err,
84+
)
85+
86+
async def async_update(self) -> None:
87+
"""Update the Time setting status."""
88+
data = self.device.appliance.status.get(self.bsh_key)
89+
if data is None:
90+
_LOGGER.error("No value for %s", self.bsh_key)
91+
self._attr_native_value = None
92+
return
93+
seconds = data.get(ATTR_VALUE, None)
94+
if seconds is not None:
95+
self._attr_native_value = seconds_to_time(seconds)
96+
else:
97+
self._attr_native_value = None
98+
_LOGGER.debug("Updated, new value: %s", self._attr_native_value)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for home_connect time entities."""
2+
3+
from collections.abc import Awaitable, Callable, Generator
4+
from datetime import time
5+
from unittest.mock import MagicMock, Mock
6+
7+
from homeconnect.api import HomeConnectError
8+
import pytest
9+
10+
from homeassistant.components.home_connect.const import ATTR_VALUE
11+
from homeassistant.components.time import DOMAIN as TIME_DOMAIN, SERVICE_SET_VALUE
12+
from homeassistant.config_entries import ConfigEntryState
13+
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, Platform
14+
from homeassistant.core import HomeAssistant
15+
16+
from .conftest import get_all_appliances
17+
18+
from tests.common import MockConfigEntry
19+
20+
21+
@pytest.fixture
22+
def platforms() -> list[str]:
23+
"""Fixture to specify platforms to test."""
24+
return [Platform.TIME]
25+
26+
27+
async def test_time(
28+
bypass_throttle: Generator[None],
29+
hass: HomeAssistant,
30+
config_entry: MockConfigEntry,
31+
integration_setup: Callable[[], Awaitable[bool]],
32+
setup_credentials: None,
33+
get_appliances: Mock,
34+
) -> None:
35+
"""Test time entity."""
36+
get_appliances.side_effect = get_all_appliances
37+
assert config_entry.state is ConfigEntryState.NOT_LOADED
38+
assert await integration_setup()
39+
assert config_entry.state is ConfigEntryState.LOADED
40+
41+
42+
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
43+
@pytest.mark.parametrize(
44+
("entity_id", "setting_key", "setting_value", "expected_state"),
45+
[
46+
(
47+
f"{TIME_DOMAIN}.oven_alarm_clock",
48+
"BSH.Common.Setting.AlarmClock",
49+
{ATTR_VALUE: 59},
50+
str(time(second=59)),
51+
),
52+
(
53+
f"{TIME_DOMAIN}.oven_alarm_clock",
54+
"BSH.Common.Setting.AlarmClock",
55+
{ATTR_VALUE: None},
56+
"unknown",
57+
),
58+
(
59+
f"{TIME_DOMAIN}.oven_alarm_clock",
60+
"BSH.Common.Setting.AlarmClock",
61+
None,
62+
"unknown",
63+
),
64+
],
65+
)
66+
@pytest.mark.usefixtures("bypass_throttle")
67+
async def test_time_entity_functionality(
68+
appliance: Mock,
69+
entity_id: str,
70+
setting_key: str,
71+
setting_value: dict,
72+
expected_state: str,
73+
bypass_throttle: Generator[None],
74+
hass: HomeAssistant,
75+
config_entry: MockConfigEntry,
76+
integration_setup: Callable[[], Awaitable[bool]],
77+
setup_credentials: None,
78+
get_appliances: MagicMock,
79+
) -> None:
80+
"""Test time entity functionality."""
81+
get_appliances.return_value = [appliance]
82+
appliance.status.update({setting_key: setting_value})
83+
84+
assert config_entry.state is ConfigEntryState.NOT_LOADED
85+
assert await integration_setup()
86+
assert config_entry.state is ConfigEntryState.LOADED
87+
assert hass.states.is_state(entity_id, expected_state)
88+
89+
new_value = 30
90+
assert hass.states.get(entity_id).state != new_value
91+
await hass.services.async_call(
92+
TIME_DOMAIN,
93+
SERVICE_SET_VALUE,
94+
{
95+
ATTR_ENTITY_ID: entity_id,
96+
ATTR_TIME: time(second=new_value),
97+
},
98+
blocking=True,
99+
)
100+
appliance.set_setting.assert_called_once_with(setting_key, new_value)
101+
102+
103+
@pytest.mark.parametrize("problematic_appliance", ["Oven"], indirect=True)
104+
@pytest.mark.parametrize(
105+
("entity_id", "setting_key", "mock_attr"),
106+
[
107+
(
108+
f"{TIME_DOMAIN}.oven_alarm_clock",
109+
"BSH.Common.Setting.AlarmClock",
110+
"set_setting",
111+
),
112+
],
113+
)
114+
@pytest.mark.usefixtures("bypass_throttle")
115+
async def test_time_entity_error(
116+
problematic_appliance: Mock,
117+
entity_id: str,
118+
setting_key: str,
119+
mock_attr: str,
120+
hass: HomeAssistant,
121+
config_entry: MockConfigEntry,
122+
integration_setup: Callable[[], Awaitable[bool]],
123+
setup_credentials: None,
124+
get_appliances: MagicMock,
125+
) -> None:
126+
"""Test time entity error."""
127+
get_appliances.return_value = [problematic_appliance]
128+
129+
assert config_entry.state is ConfigEntryState.NOT_LOADED
130+
problematic_appliance.status.update({setting_key: {}})
131+
assert await integration_setup()
132+
assert config_entry.state is ConfigEntryState.LOADED
133+
134+
with pytest.raises(HomeConnectError):
135+
getattr(problematic_appliance, mock_attr)()
136+
137+
await hass.services.async_call(
138+
TIME_DOMAIN,
139+
SERVICE_SET_VALUE,
140+
{
141+
ATTR_ENTITY_ID: entity_id,
142+
ATTR_TIME: time(minute=1),
143+
},
144+
blocking=True,
145+
)
146+
assert getattr(problematic_appliance, mock_attr).call_count == 2

0 commit comments

Comments
 (0)