Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for BSH Home Connect appliances."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 import logging
7 import re
8 from typing import Any, cast
9 
10 from requests import HTTPError
11 import voluptuous as vol
12 
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import ATTR_DEVICE_ID, Platform
15 from homeassistant.core import HomeAssistant, callback
16 from homeassistant.helpers import (
17  config_entry_oauth2_flow,
18  config_validation as cv,
19  device_registry as dr,
20 )
21 from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
22 from homeassistant.helpers.typing import ConfigType
23 from homeassistant.util import Throttle
24 
25 from . import api
26 from .const import (
27  ATTR_KEY,
28  ATTR_PROGRAM,
29  ATTR_UNIT,
30  ATTR_VALUE,
31  BSH_PAUSE,
32  BSH_RESUME,
33  DOMAIN,
34  OLD_NEW_UNIQUE_ID_SUFFIX_MAP,
35  SERVICE_OPTION_ACTIVE,
36  SERVICE_OPTION_SELECTED,
37  SERVICE_PAUSE_PROGRAM,
38  SERVICE_RESUME_PROGRAM,
39  SERVICE_SELECT_PROGRAM,
40  SERVICE_SETTING,
41  SERVICE_START_PROGRAM,
42 )
43 
44 type HomeConnectConfigEntry = ConfigEntry[api.ConfigEntryAuth]
45 
46 _LOGGER = logging.getLogger(__name__)
47 
48 RE_CAMEL_CASE = re.compile(r"(?<!^)(?=[A-Z])|(?=\d)(?<=\D)")
49 
50 SCAN_INTERVAL = timedelta(minutes=1)
51 
52 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
53 
54 SERVICE_SETTING_SCHEMA = vol.Schema(
55  {
56  vol.Required(ATTR_DEVICE_ID): str,
57  vol.Required(ATTR_KEY): str,
58  vol.Required(ATTR_VALUE): vol.Any(str, int, bool),
59  }
60 )
61 
62 SERVICE_OPTION_SCHEMA = vol.Schema(
63  {
64  vol.Required(ATTR_DEVICE_ID): str,
65  vol.Required(ATTR_KEY): str,
66  vol.Required(ATTR_VALUE): vol.Any(str, int, bool),
67  vol.Optional(ATTR_UNIT): str,
68  }
69 )
70 
71 SERVICE_PROGRAM_SCHEMA = vol.Any(
72  {
73  vol.Required(ATTR_DEVICE_ID): str,
74  vol.Required(ATTR_PROGRAM): str,
75  vol.Required(ATTR_KEY): str,
76  vol.Required(ATTR_VALUE): vol.Any(int, str),
77  vol.Optional(ATTR_UNIT): str,
78  },
79  {
80  vol.Required(ATTR_DEVICE_ID): str,
81  vol.Required(ATTR_PROGRAM): str,
82  },
83 )
84 
85 SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
86 
87 PLATFORMS = [
88  Platform.BINARY_SENSOR,
89  Platform.LIGHT,
90  Platform.NUMBER,
91  Platform.SELECT,
92  Platform.SENSOR,
93  Platform.SWITCH,
94  Platform.TIME,
95 ]
96 
97 
99  hass: HomeAssistant,
100  device_id: str | None = None,
101  device_entry: dr.DeviceEntry | None = None,
102  entry: HomeConnectConfigEntry | None = None,
103 ) -> api.HomeConnectAppliance:
104  """Return a Home Connect appliance instance given a device id or a device entry."""
105  if device_id is not None and device_entry is None:
106  device_registry = dr.async_get(hass)
107  device_entry = device_registry.async_get(device_id)
108  assert device_entry, "Either a device id or a device entry must be provided"
109 
110  ha_id = next(
111  (
112  identifier[1]
113  for identifier in device_entry.identifiers
114  if identifier[0] == DOMAIN
115  ),
116  None,
117  )
118  assert ha_id
119 
120  def find_appliance(
121  entry: HomeConnectConfigEntry,
122  ) -> api.HomeConnectAppliance | None:
123  for device in entry.runtime_data.devices:
124  appliance = device.appliance
125  if appliance.haId == ha_id:
126  return appliance
127  return None
128 
129  if entry is None:
130  for entry_id in device_entry.config_entries:
131  entry = hass.config_entries.async_get_entry(entry_id)
132  assert entry
133  if entry.domain == DOMAIN:
134  entry = cast(HomeConnectConfigEntry, entry)
135  if (appliance := find_appliance(entry)) is not None:
136  return appliance
137  elif (appliance := find_appliance(entry)) is not None:
138  return appliance
139  raise ValueError(f"Appliance for device id {device_entry.id} not found")
140 
141 
142 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
143  """Set up Home Connect component."""
144 
145  async def _async_service_program(call, method):
146  """Execute calls to services taking a program."""
147  program = call.data[ATTR_PROGRAM]
148  device_id = call.data[ATTR_DEVICE_ID]
149 
150  options = []
151 
152  option_key = call.data.get(ATTR_KEY)
153  if option_key is not None:
154  option = {ATTR_KEY: option_key, ATTR_VALUE: call.data[ATTR_VALUE]}
155 
156  option_unit = call.data.get(ATTR_UNIT)
157  if option_unit is not None:
158  option[ATTR_UNIT] = option_unit
159 
160  options.append(option)
161 
162  appliance = _get_appliance(hass, device_id)
163  await hass.async_add_executor_job(getattr(appliance, method), program, options)
164 
165  async def _async_service_command(call, command):
166  """Execute calls to services executing a command."""
167  device_id = call.data[ATTR_DEVICE_ID]
168 
169  appliance = _get_appliance(hass, device_id)
170  await hass.async_add_executor_job(appliance.execute_command, command)
171 
172  async def _async_service_key_value(call, method):
173  """Execute calls to services taking a key and value."""
174  key = call.data[ATTR_KEY]
175  value = call.data[ATTR_VALUE]
176  unit = call.data.get(ATTR_UNIT)
177  device_id = call.data[ATTR_DEVICE_ID]
178 
179  appliance = _get_appliance(hass, device_id)
180  if unit is not None:
181  await hass.async_add_executor_job(
182  getattr(appliance, method),
183  key,
184  value,
185  unit,
186  )
187  else:
188  await hass.async_add_executor_job(
189  getattr(appliance, method),
190  key,
191  value,
192  )
193 
194  async def async_service_option_active(call):
195  """Service for setting an option for an active program."""
196  await _async_service_key_value(call, "set_options_active_program")
197 
198  async def async_service_option_selected(call):
199  """Service for setting an option for a selected program."""
200  await _async_service_key_value(call, "set_options_selected_program")
201 
202  async def async_service_setting(call):
203  """Service for changing a setting."""
204  await _async_service_key_value(call, "set_setting")
205 
206  async def async_service_pause_program(call):
207  """Service for pausing a program."""
208  await _async_service_command(call, BSH_PAUSE)
209 
210  async def async_service_resume_program(call):
211  """Service for resuming a paused program."""
212  await _async_service_command(call, BSH_RESUME)
213 
214  async def async_service_select_program(call):
215  """Service for selecting a program."""
216  await _async_service_program(call, "select_program")
217 
218  async def async_service_start_program(call):
219  """Service for starting a program."""
220  await _async_service_program(call, "start_program")
221 
222  hass.services.async_register(
223  DOMAIN,
224  SERVICE_OPTION_ACTIVE,
225  async_service_option_active,
226  schema=SERVICE_OPTION_SCHEMA,
227  )
228  hass.services.async_register(
229  DOMAIN,
230  SERVICE_OPTION_SELECTED,
231  async_service_option_selected,
232  schema=SERVICE_OPTION_SCHEMA,
233  )
234  hass.services.async_register(
235  DOMAIN, SERVICE_SETTING, async_service_setting, schema=SERVICE_SETTING_SCHEMA
236  )
237  hass.services.async_register(
238  DOMAIN,
239  SERVICE_PAUSE_PROGRAM,
240  async_service_pause_program,
241  schema=SERVICE_COMMAND_SCHEMA,
242  )
243  hass.services.async_register(
244  DOMAIN,
245  SERVICE_RESUME_PROGRAM,
246  async_service_resume_program,
247  schema=SERVICE_COMMAND_SCHEMA,
248  )
249  hass.services.async_register(
250  DOMAIN,
251  SERVICE_SELECT_PROGRAM,
252  async_service_select_program,
253  schema=SERVICE_PROGRAM_SCHEMA,
254  )
255  hass.services.async_register(
256  DOMAIN,
257  SERVICE_START_PROGRAM,
258  async_service_start_program,
259  schema=SERVICE_PROGRAM_SCHEMA,
260  )
261 
262  return True
263 
264 
265 async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry) -> bool:
266  """Set up Home Connect from a config entry."""
267  implementation = (
268  await config_entry_oauth2_flow.async_get_config_entry_implementation(
269  hass, entry
270  )
271  )
272 
273  entry.runtime_data = api.ConfigEntryAuth(hass, entry, implementation)
274 
275  await update_all_devices(hass, entry)
276 
277  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
278 
279  return True
280 
281 
283  hass: HomeAssistant, entry: HomeConnectConfigEntry
284 ) -> bool:
285  """Unload a config entry."""
286  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
287 
288 
289 @Throttle(SCAN_INTERVAL)
291  hass: HomeAssistant, entry: HomeConnectConfigEntry
292 ) -> None:
293  """Update all the devices."""
294  hc_api = entry.runtime_data
295 
296  try:
297  await hass.async_add_executor_job(hc_api.get_devices)
298  for device in hc_api.devices:
299  await hass.async_add_executor_job(device.initialize)
300  except HTTPError as err:
301  _LOGGER.warning("Cannot update devices: %s", err.response.status_code)
302 
303 
305  hass: HomeAssistant, entry: HomeConnectConfigEntry
306 ) -> bool:
307  """Migrate old entry."""
308  _LOGGER.debug("Migrating from version %s", entry.version)
309 
310  if entry.version == 1 and entry.minor_version == 1:
311 
312  @callback
313  def update_unique_id(
314  entity_entry: RegistryEntry,
315  ) -> dict[str, Any] | None:
316  """Update unique ID of entity entry."""
317  for old_id_suffix, new_id_suffix in OLD_NEW_UNIQUE_ID_SUFFIX_MAP.items():
318  if entity_entry.unique_id.endswith(f"-{old_id_suffix}"):
319  return {
320  "new_unique_id": entity_entry.unique_id.replace(
321  old_id_suffix, new_id_suffix
322  )
323  }
324  return None
325 
326  await async_migrate_entries(hass, entry.entry_id, update_unique_id)
327 
328  hass.config_entries.async_update_entry(entry, minor_version=2)
329 
330  _LOGGER.debug("Migration to version %s successful", entry.version)
331  return True
332 
333 
334 def get_dict_from_home_connect_error(err: api.HomeConnectError) -> dict[str, Any]:
335  """Return a dict from a Home Connect error."""
336  return {
337  "description": cast(dict[str, Any], err.args[0]).get("description", "?")
338  if len(err.args) > 0 and isinstance(err.args[0], dict)
339  else err.args[0]
340  if len(err.args) > 0 and isinstance(err.args[0], str)
341  else "?",
342  }
343 
344 
345 def bsh_key_to_translation_key(bsh_key: str) -> str:
346  """Convert a BSH key to a translation key format.
347 
348  This function takes a BSH key, such as `Dishcare.Dishwasher.Program.Eco50`,
349  and converts it to a translation key format, such as `dishcare_dishwasher_bsh_key_eco50`.
350  """
351  return "_".join(
352  RE_CAMEL_CASE.sub("_", split) for split in bsh_key.split(".")
353  ).lower()
None async_migrate_entries(HomeAssistant hass, dict[str, AdapterDetails] adapters, str default_adapter)
Definition: __init__.py:249
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
dict[str, str]|None update_unique_id(er.RegistryEntry entity_entry, str unique_id)
Definition: __init__.py:168
dict[str, Any] get_dict_from_home_connect_error(api.HomeConnectError err)
Definition: __init__.py:334
bool async_migrate_entry(HomeAssistant hass, HomeConnectConfigEntry entry)
Definition: __init__.py:306
bool async_unload_entry(HomeAssistant hass, HomeConnectConfigEntry entry)
Definition: __init__.py:284
api.HomeConnectAppliance _get_appliance(HomeAssistant hass, str|None device_id=None, dr.DeviceEntry|None device_entry=None, HomeConnectConfigEntry|None entry=None)
Definition: __init__.py:103
str bsh_key_to_translation_key(str bsh_key)
Definition: __init__.py:345
None update_all_devices(HomeAssistant hass, HomeConnectConfigEntry entry)
Definition: __init__.py:292
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:142
bool async_setup_entry(HomeAssistant hass, HomeConnectConfigEntry entry)
Definition: __init__.py:265