Skip to content

Commit 15647f2

Browse files
authored
Add miele select platform to support sabbath mode (home-assistant#156866)
1 parent c961126 commit 15647f2

File tree

5 files changed

+379
-0
lines changed

5 files changed

+379
-0
lines changed

homeassistant/components/miele/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Platform.CLIMATE,
2929
Platform.FAN,
3030
Platform.LIGHT,
31+
Platform.SELECT,
3132
Platform.SENSOR,
3233
Platform.SWITCH,
3334
Platform.VACUUM,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""Platform for Miele select entity."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from dataclasses import dataclass
7+
from enum import IntEnum
8+
import logging
9+
from typing import Final
10+
11+
from aiohttp import ClientResponseError
12+
13+
from homeassistant.components.select import SelectEntity, SelectEntityDescription
14+
from homeassistant.core import HomeAssistant
15+
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
16+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
17+
from homeassistant.helpers.typing import StateType
18+
19+
from .const import DOMAIN, MieleAppliance
20+
from .coordinator import MieleConfigEntry
21+
from .entity import MieleDevice, MieleEntity
22+
23+
PARALLEL_UPDATES = 1
24+
25+
_LOGGER = logging.getLogger(__name__)
26+
27+
28+
class MieleModes(IntEnum):
29+
"""Modes for fridge/freezer."""
30+
31+
NORMAL = 0
32+
SABBATH = 1
33+
PARTY = 2
34+
HOLIDAY = 3
35+
36+
37+
@dataclass(frozen=True, kw_only=True)
38+
class MieleSelectDescription(SelectEntityDescription):
39+
"""Class describing Miele select entities."""
40+
41+
value_fn: Callable[[MieleDevice], StateType]
42+
43+
44+
@dataclass
45+
class MieleSelectDefinition:
46+
"""Class for defining select entities."""
47+
48+
types: tuple[MieleAppliance, ...]
49+
description: MieleSelectDescription
50+
51+
52+
SELECT_TYPES: Final[tuple[MieleSelectDefinition, ...]] = (
53+
MieleSelectDefinition(
54+
types=(
55+
MieleAppliance.FREEZER,
56+
MieleAppliance.FRIDGE,
57+
MieleAppliance.FRIDGE_FREEZER,
58+
),
59+
description=MieleSelectDescription(
60+
key="fridge_freezer_modes",
61+
value_fn=lambda value: 1,
62+
translation_key="fridge_freezer_mode",
63+
),
64+
),
65+
)
66+
67+
68+
async def async_setup_entry(
69+
hass: HomeAssistant,
70+
config_entry: MieleConfigEntry,
71+
async_add_entities: AddConfigEntryEntitiesCallback,
72+
) -> None:
73+
"""Set up the select platform."""
74+
coordinator = config_entry.runtime_data
75+
added_devices: set[str] = set()
76+
77+
def _async_add_new_devices() -> None:
78+
nonlocal added_devices
79+
new_devices_set, current_devices = coordinator.async_add_devices(added_devices)
80+
added_devices = current_devices
81+
82+
async_add_entities(
83+
MieleSelectMode(coordinator, device_id, definition.description)
84+
for device_id, device in coordinator.data.devices.items()
85+
for definition in SELECT_TYPES
86+
if device_id in new_devices_set and device.device_type in definition.types
87+
)
88+
89+
config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
90+
_async_add_new_devices()
91+
92+
93+
class MieleSelectMode(MieleEntity, SelectEntity):
94+
"""Representation of a Select mode entity."""
95+
96+
entity_description: MieleSelectDescription
97+
98+
@property
99+
def options(self) -> list[str]:
100+
"""Return the list of available options."""
101+
return sorted(
102+
{MieleModes(x).name.lower() for x in self.action.modes}
103+
| {self.current_option}
104+
)
105+
106+
@property
107+
def current_option(self) -> str:
108+
"""Retrieve currently selected option."""
109+
# There is no direct mapping from Miele 3rd party API, so we infer the
110+
# current mode based on available modes in action.modes
111+
112+
action_modes = set(self.action.modes)
113+
if action_modes in ({1}, {1, 2}, {1, 3}, {1, 2, 3}):
114+
return MieleModes.NORMAL.name.lower()
115+
116+
if action_modes in ({0}, {0, 2}, {0, 3}, {0, 2, 3}):
117+
return MieleModes.SABBATH.name.lower()
118+
119+
if action_modes in ({0, 1}, {0, 1, 3}):
120+
return MieleModes.PARTY.name.lower()
121+
122+
if action_modes == {0, 1, 2}:
123+
return MieleModes.HOLIDAY.name.lower()
124+
125+
return MieleModes.NORMAL.name.lower()
126+
127+
async def async_select_option(self, option: str) -> None:
128+
"""Set the selected option."""
129+
new_mode = MieleModes[option.upper()].value
130+
if new_mode not in self.action.modes:
131+
_LOGGER.debug("Option '%s' is not available for %s", option, self.entity_id)
132+
raise ServiceValidationError(
133+
translation_domain=DOMAIN,
134+
translation_key="invalid_option",
135+
translation_placeholders={
136+
"option": option,
137+
"entity": self.entity_id,
138+
},
139+
)
140+
try:
141+
await self.api.send_action(
142+
self._device_id,
143+
{"modes": new_mode},
144+
)
145+
except ClientResponseError as err:
146+
_LOGGER.debug("Error setting select state for %s: %s", self.entity_id, err)
147+
raise HomeAssistantError(
148+
translation_domain=DOMAIN,
149+
translation_key="set_state_error",
150+
translation_placeholders={
151+
"entity": self.entity_id,
152+
},
153+
) from err
154+
155+
# Refresh data as API does not push changes for modes updates
156+
await self.coordinator.async_request_refresh()

homeassistant/components/miele/strings.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,17 @@
187187
"name": "[%key:component::light::title%]"
188188
}
189189
},
190+
"select": {
191+
"fridge_freezer_mode": {
192+
"name": "Mode",
193+
"state": {
194+
"holiday": "Holiday",
195+
"normal": "Normal",
196+
"party": "Party",
197+
"sabbath": "Sabbath"
198+
}
199+
}
200+
},
190201
"sensor": {
191202
"core_target_temperature": {
192203
"name": "Core target temperature"
@@ -1085,6 +1096,9 @@
10851096
"get_programs_error": {
10861097
"message": "'Get programs' action failed: {status} / {message}"
10871098
},
1099+
"invalid_option": {
1100+
"message": "Invalid option: \"{option}\" on {entity}."
1101+
},
10881102
"invalid_target": {
10891103
"message": "Invalid device targeted."
10901104
},
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# serializer version: 1
2+
# name: test_select_states[platforms0-freezer][select.freezer_mode-entry]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': dict({
8+
'options': list([
9+
'normal',
10+
'sabbath',
11+
]),
12+
}),
13+
'config_entry_id': <ANY>,
14+
'config_subentry_id': <ANY>,
15+
'device_class': None,
16+
'device_id': <ANY>,
17+
'disabled_by': None,
18+
'domain': 'select',
19+
'entity_category': None,
20+
'entity_id': 'select.freezer_mode',
21+
'has_entity_name': True,
22+
'hidden_by': None,
23+
'icon': None,
24+
'id': <ANY>,
25+
'labels': set({
26+
}),
27+
'name': None,
28+
'options': dict({
29+
}),
30+
'original_device_class': None,
31+
'original_icon': None,
32+
'original_name': 'Mode',
33+
'platform': 'miele',
34+
'previous_unique_id': None,
35+
'suggested_object_id': None,
36+
'supported_features': 0,
37+
'translation_key': 'fridge_freezer_mode',
38+
'unique_id': 'Dummy_Appliance_1-fridge_freezer_modes',
39+
'unit_of_measurement': None,
40+
})
41+
# ---
42+
# name: test_select_states[platforms0-freezer][select.freezer_mode-state]
43+
StateSnapshot({
44+
'attributes': ReadOnlyDict({
45+
'friendly_name': 'Freezer Mode',
46+
'options': list([
47+
'normal',
48+
'sabbath',
49+
]),
50+
}),
51+
'context': <ANY>,
52+
'entity_id': 'select.freezer_mode',
53+
'last_changed': <ANY>,
54+
'last_reported': <ANY>,
55+
'last_updated': <ANY>,
56+
'state': 'normal',
57+
})
58+
# ---
59+
# name: test_select_states[platforms0-freezer][select.refrigerator_mode-entry]
60+
EntityRegistryEntrySnapshot({
61+
'aliases': set({
62+
}),
63+
'area_id': None,
64+
'capabilities': dict({
65+
'options': list([
66+
'normal',
67+
'sabbath',
68+
]),
69+
}),
70+
'config_entry_id': <ANY>,
71+
'config_subentry_id': <ANY>,
72+
'device_class': None,
73+
'device_id': <ANY>,
74+
'disabled_by': None,
75+
'domain': 'select',
76+
'entity_category': None,
77+
'entity_id': 'select.refrigerator_mode',
78+
'has_entity_name': True,
79+
'hidden_by': None,
80+
'icon': None,
81+
'id': <ANY>,
82+
'labels': set({
83+
}),
84+
'name': None,
85+
'options': dict({
86+
}),
87+
'original_device_class': None,
88+
'original_icon': None,
89+
'original_name': 'Mode',
90+
'platform': 'miele',
91+
'previous_unique_id': None,
92+
'suggested_object_id': None,
93+
'supported_features': 0,
94+
'translation_key': 'fridge_freezer_mode',
95+
'unique_id': 'Dummy_Appliance_2-fridge_freezer_modes',
96+
'unit_of_measurement': None,
97+
})
98+
# ---
99+
# name: test_select_states[platforms0-freezer][select.refrigerator_mode-state]
100+
StateSnapshot({
101+
'attributes': ReadOnlyDict({
102+
'friendly_name': 'Refrigerator Mode',
103+
'options': list([
104+
'normal',
105+
'sabbath',
106+
]),
107+
}),
108+
'context': <ANY>,
109+
'entity_id': 'select.refrigerator_mode',
110+
'last_changed': <ANY>,
111+
'last_reported': <ANY>,
112+
'last_updated': <ANY>,
113+
'state': 'normal',
114+
})
115+
# ---

0 commit comments

Comments
 (0)