Skip to content

Commit 2acad4a

Browse files
authored
Home connect number platform with temperature set points entities (#126145)
1 parent 65ee4e1 commit 2acad4a

File tree

6 files changed

+359
-0
lines changed

6 files changed

+359
-0
lines changed

homeassistant/components/home_connect/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
PLATFORMS = [
8383
Platform.BINARY_SENSOR,
8484
Platform.LIGHT,
85+
Platform.NUMBER,
8586
Platform.SENSOR,
8687
Platform.SWITCH,
8788
Platform.TIME,

homeassistant/components/home_connect/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,17 @@
9595
SERVICE_SETTING = "change_setting"
9696
SERVICE_START_PROGRAM = "start_program"
9797

98+
ATTR_ALLOWED_VALUES = "allowedvalues"
9899
ATTR_AMBIENT = "ambient"
99100
ATTR_BSH_KEY = "bsh_key"
101+
ATTR_CONSTRAINTS = "constraints"
100102
ATTR_DESC = "desc"
101103
ATTR_DEVICE = "device"
102104
ATTR_KEY = "key"
103105
ATTR_PROGRAM = "program"
104106
ATTR_SENSOR_TYPE = "sensor_type"
105107
ATTR_SIGN = "sign"
108+
ATTR_STEPSIZE = "stepsize"
106109
ATTR_UNIT = "unit"
107110
ATTR_VALUE = "value"
108111

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Provides number enties for Home Connect."""
2+
3+
import logging
4+
5+
from homeconnect.api import HomeConnectError
6+
7+
from homeassistant.components.number import (
8+
ATTR_MAX,
9+
ATTR_MIN,
10+
NumberDeviceClass,
11+
NumberEntity,
12+
NumberEntityDescription,
13+
)
14+
from homeassistant.config_entries import ConfigEntry
15+
from homeassistant.core import HomeAssistant
16+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
17+
18+
from .api import ConfigEntryAuth
19+
from .const import ATTR_CONSTRAINTS, ATTR_STEPSIZE, ATTR_UNIT, ATTR_VALUE, DOMAIN
20+
from .entity import HomeConnectEntity
21+
22+
_LOGGER = logging.getLogger(__name__)
23+
24+
25+
NUMBERS = (
26+
NumberEntityDescription(
27+
key="Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
28+
device_class=NumberDeviceClass.TEMPERATURE,
29+
translation_key="refrigerator_setpoint_temperature",
30+
),
31+
NumberEntityDescription(
32+
key="Refrigeration.FridgeFreezer.Setting.SetpointTemperatureFreezer",
33+
device_class=NumberDeviceClass.TEMPERATURE,
34+
translation_key="freezer_setpoint_temperature",
35+
),
36+
NumberEntityDescription(
37+
key="Refrigeration.Common.Setting.BottleCooler.SetpointTemperature",
38+
device_class=NumberDeviceClass.TEMPERATURE,
39+
translation_key="bottle_cooler_setpoint_temperature",
40+
),
41+
NumberEntityDescription(
42+
key="Refrigeration.Common.Setting.ChillerLeft.SetpointTemperature",
43+
device_class=NumberDeviceClass.TEMPERATURE,
44+
translation_key="chiller_left_setpoint_temperature",
45+
),
46+
NumberEntityDescription(
47+
key="Refrigeration.Common.Setting.ChillerCommon.SetpointTemperature",
48+
device_class=NumberDeviceClass.TEMPERATURE,
49+
translation_key="chiller_setpoint_temperature",
50+
),
51+
NumberEntityDescription(
52+
key="Refrigeration.Common.Setting.ChillerRight.SetpointTemperature",
53+
device_class=NumberDeviceClass.TEMPERATURE,
54+
translation_key="chiller_right_setpoint_temperature",
55+
),
56+
NumberEntityDescription(
57+
key="Refrigeration.Common.Setting.WineCompartment.SetpointTemperature",
58+
device_class=NumberDeviceClass.TEMPERATURE,
59+
translation_key="wine_compartment_setpoint_temperature",
60+
),
61+
NumberEntityDescription(
62+
key="Refrigeration.Common.Setting.WineCompartment2.SetpointTemperature",
63+
device_class=NumberDeviceClass.TEMPERATURE,
64+
translation_key="wine_compartment_2_setpoint_temperature",
65+
),
66+
NumberEntityDescription(
67+
key="Refrigeration.Common.Setting.WineCompartment3.SetpointTemperature",
68+
device_class=NumberDeviceClass.TEMPERATURE,
69+
translation_key="wine_compartment_3_setpoint_temperature",
70+
),
71+
)
72+
73+
74+
async def async_setup_entry(
75+
hass: HomeAssistant,
76+
config_entry: ConfigEntry,
77+
async_add_entities: AddEntitiesCallback,
78+
) -> None:
79+
"""Set up the Home Connect number."""
80+
81+
def get_entities() -> list[HomeConnectNumberEntity]:
82+
"""Get a list of entities."""
83+
hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
84+
return [
85+
HomeConnectNumberEntity(device, description)
86+
for description in NUMBERS
87+
for device in hc_api.devices
88+
if description.key in device.appliance.status
89+
]
90+
91+
async_add_entities(await hass.async_add_executor_job(get_entities), True)
92+
93+
94+
class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
95+
"""Number setting class for Home Connect."""
96+
97+
async def async_set_native_value(self, value: float) -> None:
98+
"""Set the native value of the entity."""
99+
_LOGGER.debug(
100+
"Tried to set value %s to %s for %s",
101+
value,
102+
self.bsh_key,
103+
self.entity_id,
104+
)
105+
try:
106+
await self.hass.async_add_executor_job(
107+
self.device.appliance.set_setting,
108+
self.bsh_key,
109+
value,
110+
)
111+
except HomeConnectError as err:
112+
_LOGGER.error(
113+
"Error setting value %s to %s for %s: %s",
114+
value,
115+
self.bsh_key,
116+
self.entity_id,
117+
err,
118+
)
119+
120+
async def async_fetch_constraints(self) -> None:
121+
"""Fetch the max and min values and step for the number entity."""
122+
try:
123+
data = await self.hass.async_add_executor_job(
124+
self.device.appliance.get, f"/settings/{self.bsh_key}"
125+
)
126+
except HomeConnectError as err:
127+
_LOGGER.error("An error occurred: %s", err)
128+
return
129+
if not data or not (constraints := data.get(ATTR_CONSTRAINTS)):
130+
return
131+
self._attr_native_max_value = constraints.get(ATTR_MAX)
132+
self._attr_native_min_value = constraints.get(ATTR_MIN)
133+
self._attr_native_step = constraints.get(ATTR_STEPSIZE)
134+
self._attr_native_unit_of_measurement = data.get(ATTR_UNIT)
135+
136+
async def async_update(self) -> None:
137+
"""Update the number setting status."""
138+
if not (data := self.device.appliance.status.get(self.bsh_key)):
139+
_LOGGER.error("No value for %s", self.bsh_key)
140+
self._attr_native_value = None
141+
return
142+
self._attr_native_value = data.get(ATTR_VALUE, None)
143+
_LOGGER.debug("Updated, new value: %s", self._attr_native_value)
144+
145+
if (
146+
not hasattr(self, "_attr_native_min_value")
147+
or self._attr_native_min_value is None
148+
or not hasattr(self, "_attr_native_max_value")
149+
or self._attr_native_max_value is None
150+
or not hasattr(self, "_attr_native_step")
151+
or self._attr_native_step is None
152+
):
153+
await self.async_fetch_constraints()

homeassistant/components/home_connect/strings.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,35 @@
188188
"name": "Internal light"
189189
}
190190
},
191+
"number": {
192+
"refrigerator_setpoint_temperature": {
193+
"name": "Refrigerator temperature"
194+
},
195+
"freezer_setpoint_temperature": {
196+
"name": "Freezer temperature"
197+
},
198+
"bottle_cooler_setpoint_temperature": {
199+
"name": "Bottle cooler temperature"
200+
},
201+
"chiller_left_setpoint_temperature": {
202+
"name": "Chiller left temperature"
203+
},
204+
"chiller_setpoint_temperature": {
205+
"name": "Chiller temperature"
206+
},
207+
"chiller_right_setpoint_temperature": {
208+
"name": "Chiller right temperature"
209+
},
210+
"wine_compartment_setpoint_temperature": {
211+
"name": "Wine compartment temperature"
212+
},
213+
"wine_compartment_2_setpoint_temperature": {
214+
"name": "Wine compartment 2 temperature"
215+
},
216+
"wine_compartment_3_setpoint_temperature": {
217+
"name": "Wine compartment 3 temperature"
218+
}
219+
},
191220
"sensor": {
192221
"program_progress": {
193222
"name": "Program progress"

tests/components/home_connect/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def mock_problematic_appliance(request: pytest.FixtureRequest) -> Mock:
178178
)
179179
mock.name = app
180180
type(mock).status = PropertyMock(return_value={})
181+
mock.get.side_effect = HomeConnectError
181182
mock.get_programs_active.side_effect = HomeConnectError
182183
mock.get_programs_available.side_effect = HomeConnectError
183184
mock.start_program.side_effect = HomeConnectError
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""Tests for home_connect number entities."""
2+
3+
from collections.abc import Awaitable, Callable, Generator
4+
import random
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 (
11+
ATTR_CONSTRAINTS,
12+
ATTR_STEPSIZE,
13+
ATTR_UNIT,
14+
ATTR_VALUE,
15+
)
16+
from homeassistant.components.number import (
17+
ATTR_MAX,
18+
ATTR_MIN,
19+
ATTR_VALUE as SERVICE_ATTR_VALUE,
20+
DEFAULT_MIN_VALUE,
21+
DOMAIN as NUMBER_DOMAIN,
22+
SERVICE_SET_VALUE,
23+
)
24+
from homeassistant.config_entries import ConfigEntryState
25+
from homeassistant.const import ATTR_ENTITY_ID, Platform
26+
from homeassistant.core import HomeAssistant
27+
28+
from .conftest import get_all_appliances
29+
30+
from tests.common import MockConfigEntry
31+
32+
33+
@pytest.fixture
34+
def platforms() -> list[str]:
35+
"""Fixture to specify platforms to test."""
36+
return [Platform.NUMBER]
37+
38+
39+
async def test_number(
40+
bypass_throttle: Generator[None],
41+
hass: HomeAssistant,
42+
config_entry: MockConfigEntry,
43+
integration_setup: Callable[[], Awaitable[bool]],
44+
setup_credentials: None,
45+
get_appliances: Mock,
46+
) -> None:
47+
"""Test number entity."""
48+
get_appliances.side_effect = get_all_appliances
49+
assert config_entry.state is ConfigEntryState.NOT_LOADED
50+
assert await integration_setup()
51+
assert config_entry.state is ConfigEntryState.LOADED
52+
53+
54+
@pytest.mark.parametrize("appliance", ["Refrigerator"], indirect=True)
55+
@pytest.mark.parametrize(
56+
(
57+
"entity_id",
58+
"setting_key",
59+
"min_value",
60+
"max_value",
61+
"step_size",
62+
"unit_of_measurement",
63+
),
64+
[
65+
(
66+
f"{NUMBER_DOMAIN.lower()}.refrigerator_refrigerator_temperature",
67+
"Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
68+
7,
69+
15,
70+
0.1,
71+
"°C",
72+
),
73+
],
74+
)
75+
@pytest.mark.usefixtures("bypass_throttle")
76+
async def test_number_entity_functionality(
77+
appliance: Mock,
78+
entity_id: str,
79+
setting_key: str,
80+
bypass_throttle: Generator[None],
81+
min_value: int,
82+
max_value: int,
83+
step_size: float,
84+
unit_of_measurement: str,
85+
hass: HomeAssistant,
86+
config_entry: MockConfigEntry,
87+
integration_setup: Callable[[], Awaitable[bool]],
88+
setup_credentials: None,
89+
get_appliances: MagicMock,
90+
) -> None:
91+
"""Test number entity functionality."""
92+
appliance.get.side_effect = [
93+
{
94+
ATTR_CONSTRAINTS: {
95+
ATTR_MIN: min_value,
96+
ATTR_MAX: max_value,
97+
ATTR_STEPSIZE: step_size,
98+
},
99+
ATTR_UNIT: unit_of_measurement,
100+
}
101+
]
102+
get_appliances.return_value = [appliance]
103+
current_value = min_value
104+
appliance.status.update({setting_key: {ATTR_VALUE: current_value}})
105+
106+
assert config_entry.state is ConfigEntryState.NOT_LOADED
107+
assert await integration_setup()
108+
assert config_entry.state is ConfigEntryState.LOADED
109+
assert hass.states.is_state(entity_id, str(current_value))
110+
state = hass.states.get(entity_id)
111+
assert state.attributes["min"] == min_value
112+
assert state.attributes["max"] == max_value
113+
assert state.attributes["step"] == step_size
114+
assert state.attributes["unit_of_measurement"] == unit_of_measurement
115+
116+
new_value = random.randint(min_value + 1, max_value)
117+
await hass.services.async_call(
118+
NUMBER_DOMAIN,
119+
SERVICE_SET_VALUE,
120+
{
121+
ATTR_ENTITY_ID: entity_id,
122+
SERVICE_ATTR_VALUE: new_value,
123+
},
124+
blocking=True,
125+
)
126+
appliance.set_setting.assert_called_once_with(setting_key, new_value)
127+
128+
129+
@pytest.mark.parametrize("problematic_appliance", ["Refrigerator"], indirect=True)
130+
@pytest.mark.parametrize(
131+
("entity_id", "setting_key", "mock_attr"),
132+
[
133+
(
134+
f"{NUMBER_DOMAIN.lower()}.refrigerator_refrigerator_temperature",
135+
"Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
136+
"set_setting",
137+
),
138+
],
139+
)
140+
@pytest.mark.usefixtures("bypass_throttle")
141+
async def test_number_entity_error(
142+
problematic_appliance: Mock,
143+
entity_id: str,
144+
setting_key: str,
145+
mock_attr: str,
146+
hass: HomeAssistant,
147+
config_entry: MockConfigEntry,
148+
integration_setup: Callable[[], Awaitable[bool]],
149+
setup_credentials: None,
150+
get_appliances: MagicMock,
151+
) -> None:
152+
"""Test number entity error."""
153+
get_appliances.return_value = [problematic_appliance]
154+
155+
assert config_entry.state is ConfigEntryState.NOT_LOADED
156+
problematic_appliance.status.update({setting_key: {}})
157+
assert await integration_setup()
158+
assert config_entry.state is ConfigEntryState.LOADED
159+
160+
with pytest.raises(HomeConnectError):
161+
getattr(problematic_appliance, mock_attr)()
162+
163+
await hass.services.async_call(
164+
NUMBER_DOMAIN,
165+
SERVICE_SET_VALUE,
166+
{
167+
ATTR_ENTITY_ID: entity_id,
168+
SERVICE_ATTR_VALUE: DEFAULT_MIN_VALUE,
169+
},
170+
blocking=True,
171+
)
172+
assert getattr(problematic_appliance, mock_attr).call_count == 2

0 commit comments

Comments
 (0)