Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Shelly integration."""
2 
3 from __future__ import annotations
4 
5 from typing import Final
6 
7 from aioshelly.block_device import BlockDevice
8 from aioshelly.common import ConnectionOptions
9 from aioshelly.const import DEFAULT_COAP_PORT, RPC_GENERATIONS
10 from aioshelly.exceptions import (
11  DeviceConnectionError,
12  InvalidAuthError,
13  MacAddressMismatchError,
14 )
15 from aioshelly.rpc_device import RpcDevice
16 import voluptuous as vol
17 
18 from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
19 from homeassistant.core import HomeAssistant
20 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
21 from homeassistant.helpers import (
22  config_validation as cv,
23  device_registry as dr,
24  entity_registry as er,
25  issue_registry as ir,
26 )
27 from homeassistant.helpers.aiohttp_client import async_get_clientsession
28 from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
29 from homeassistant.helpers.typing import ConfigType
30 
31 from .const import (
32  BLOCK_EXPECTED_SLEEP_PERIOD,
33  BLOCK_WRONG_SLEEP_PERIOD,
34  CONF_COAP_PORT,
35  CONF_SLEEP_PERIOD,
36  DOMAIN,
37  FIRMWARE_UNSUPPORTED_ISSUE_ID,
38  LOGGER,
39  MODELS_WITH_WRONG_SLEEP_PERIOD,
40  PUSH_UPDATE_ISSUE_ID,
41 )
42 from .coordinator import (
43  ShellyBlockCoordinator,
44  ShellyConfigEntry,
45  ShellyEntryData,
46  ShellyRestCoordinator,
47  ShellyRpcCoordinator,
48  ShellyRpcPollingCoordinator,
49 )
50 from .utils import (
51  async_create_issue_unsupported_firmware,
52  get_coap_context,
53  get_device_entry_gen,
54  get_http_port,
55  get_ws_context,
56 )
57 
58 PLATFORMS: Final = [
59  Platform.BINARY_SENSOR,
60  Platform.BUTTON,
61  Platform.CLIMATE,
62  Platform.COVER,
63  Platform.EVENT,
64  Platform.LIGHT,
65  Platform.NUMBER,
66  Platform.SELECT,
67  Platform.SENSOR,
68  Platform.SWITCH,
69  Platform.TEXT,
70  Platform.UPDATE,
71  Platform.VALVE,
72 ]
73 BLOCK_SLEEPING_PLATFORMS: Final = [
74  Platform.BINARY_SENSOR,
75  Platform.CLIMATE,
76  Platform.NUMBER,
77  Platform.SENSOR,
78  Platform.SWITCH,
79 ]
80 RPC_SLEEPING_PLATFORMS: Final = [
81  Platform.BINARY_SENSOR,
82  Platform.SENSOR,
83  Platform.UPDATE,
84 ]
85 
86 COAP_SCHEMA: Final = vol.Schema(
87  {
88  vol.Optional(CONF_COAP_PORT, default=DEFAULT_COAP_PORT): cv.port,
89  }
90 )
91 CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: COAP_SCHEMA}, extra=vol.ALLOW_EXTRA)
92 
93 
94 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
95  """Set up the Shelly component."""
96  if (conf := config.get(DOMAIN)) is not None:
97  hass.data[DOMAIN] = {CONF_COAP_PORT: conf[CONF_COAP_PORT]}
98 
99  return True
100 
101 
102 async def async_setup_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool:
103  """Set up Shelly from a config entry."""
104  # The custom component for Shelly devices uses shelly domain as well as core
105  # integration. If the user removes the custom component but doesn't remove the
106  # config entry, core integration will try to configure that config entry with an
107  # error. The config entry data for this custom component doesn't contain host
108  # value, so if host isn't present, config entry will not be configured.
109  if not entry.data.get(CONF_HOST):
110  LOGGER.warning(
111  (
112  "The config entry %s probably comes from a custom integration, please"
113  " remove it if you want to use core Shelly integration"
114  ),
115  entry.title,
116  )
117  return False
118 
119  if get_device_entry_gen(entry) in RPC_GENERATIONS:
120  return await _async_setup_rpc_entry(hass, entry)
121 
122  return await _async_setup_block_entry(hass, entry)
123 
124 
126  hass: HomeAssistant, entry: ShellyConfigEntry
127 ) -> bool:
128  """Set up Shelly block based device from a config entry."""
129  options = ConnectionOptions(
130  entry.data[CONF_HOST],
131  entry.data.get(CONF_USERNAME),
132  entry.data.get(CONF_PASSWORD),
133  device_mac=entry.unique_id,
134  )
135 
136  coap_context = await get_coap_context(hass)
137 
138  device = await BlockDevice.create(
140  coap_context,
141  options,
142  )
143 
144  dev_reg = dr.async_get(hass)
145  device_entry = None
146  if entry.unique_id is not None:
147  device_entry = dev_reg.async_get_device(
148  connections={(CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))},
149  )
150  # https://github.com/home-assistant/core/pull/48076
151  if device_entry and entry.entry_id not in device_entry.config_entries:
152  device_entry = None
153 
154  sleep_period = entry.data.get(CONF_SLEEP_PERIOD)
155  runtime_data = entry.runtime_data = ShellyEntryData(BLOCK_SLEEPING_PLATFORMS)
156 
157  # Some old firmware have a wrong sleep period hardcoded value.
158  # Following code block will force the right value for affected devices
159  if (
160  sleep_period == BLOCK_WRONG_SLEEP_PERIOD
161  and entry.data["model"] in MODELS_WITH_WRONG_SLEEP_PERIOD
162  ):
163  LOGGER.warning(
164  "Updating stored sleep period for %s: from %s to %s",
165  entry.title,
166  sleep_period,
167  BLOCK_EXPECTED_SLEEP_PERIOD,
168  )
169  data = {**entry.data}
170  data[CONF_SLEEP_PERIOD] = sleep_period = BLOCK_EXPECTED_SLEEP_PERIOD
171  hass.config_entries.async_update_entry(entry, data=data)
172 
173  if sleep_period == 0:
174  # Not a sleeping device, finish setup
175  LOGGER.debug("Setting up online block device %s", entry.title)
176  runtime_data.platforms = PLATFORMS
177  try:
178  await device.initialize()
179  if not device.firmware_supported:
181  await device.shutdown()
182  raise ConfigEntryNotReady
183  except (DeviceConnectionError, MacAddressMismatchError) as err:
184  await device.shutdown()
185  raise ConfigEntryNotReady(repr(err)) from err
186  except InvalidAuthError as err:
187  await device.shutdown()
188  raise ConfigEntryAuthFailed(repr(err)) from err
189 
190  runtime_data.block = ShellyBlockCoordinator(hass, entry, device)
191  runtime_data.block.async_setup()
192  runtime_data.rest = ShellyRestCoordinator(hass, device, entry)
193  await hass.config_entries.async_forward_entry_setups(
194  entry, runtime_data.platforms
195  )
196  elif (
197  sleep_period is None
198  or device_entry is None
199  or not er.async_entries_for_device(er.async_get(hass), device_entry.id)
200  ):
201  # Need to get sleep info or first time sleeping device setup, wait for device
202  # If there are no entities for the device, it means we added the device, but
203  # Home Assistant was restarted before the device was online. In this case we
204  # cannot restore the entities, so we need to wait for the device to be online.
205  LOGGER.debug(
206  "Setup for device %s will resume when device is online", entry.title
207  )
208  runtime_data.block = ShellyBlockCoordinator(hass, entry, device)
209  runtime_data.block.async_setup(runtime_data.platforms)
210  else:
211  # Restore sensors for sleeping device
212  LOGGER.debug("Setting up offline block device %s", entry.title)
213  runtime_data.block = ShellyBlockCoordinator(hass, entry, device)
214  runtime_data.block.async_setup()
215  await hass.config_entries.async_forward_entry_setups(
216  entry, runtime_data.platforms
217  )
218 
219  ir.async_delete_issue(
220  hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id)
221  )
222  return True
223 
224 
225 async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool:
226  """Set up Shelly RPC based device from a config entry."""
227  options = ConnectionOptions(
228  entry.data[CONF_HOST],
229  entry.data.get(CONF_USERNAME),
230  entry.data.get(CONF_PASSWORD),
231  device_mac=entry.unique_id,
232  port=get_http_port(entry.data),
233  )
234 
235  ws_context = await get_ws_context(hass)
236 
237  device = await RpcDevice.create(
239  ws_context,
240  options,
241  )
242 
243  dev_reg = dr.async_get(hass)
244  device_entry = None
245  if entry.unique_id is not None:
246  device_entry = dev_reg.async_get_device(
247  connections={(CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))},
248  )
249  # https://github.com/home-assistant/core/pull/48076
250  if device_entry and entry.entry_id not in device_entry.config_entries:
251  device_entry = None
252 
253  sleep_period = entry.data.get(CONF_SLEEP_PERIOD)
254  runtime_data = entry.runtime_data = ShellyEntryData(RPC_SLEEPING_PLATFORMS)
255 
256  if sleep_period == 0:
257  # Not a sleeping device, finish setup
258  LOGGER.debug("Setting up online RPC device %s", entry.title)
259  runtime_data.platforms = PLATFORMS
260  try:
261  await device.initialize()
262  if not device.firmware_supported:
264  await device.shutdown()
265  raise ConfigEntryNotReady
266  except (DeviceConnectionError, MacAddressMismatchError) as err:
267  await device.shutdown()
268  raise ConfigEntryNotReady(repr(err)) from err
269  except InvalidAuthError as err:
270  await device.shutdown()
271  raise ConfigEntryAuthFailed(repr(err)) from err
272 
273  runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device)
274  runtime_data.rpc.async_setup()
275  runtime_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device)
276  await hass.config_entries.async_forward_entry_setups(
277  entry, runtime_data.platforms
278  )
279  elif (
280  sleep_period is None
281  or device_entry is None
282  or not er.async_entries_for_device(er.async_get(hass), device_entry.id)
283  ):
284  # Need to get sleep info or first time sleeping device setup, wait for device
285  # If there are no entities for the device, it means we added the device, but
286  # Home Assistant was restarted before the device was online. In this case we
287  # cannot restore the entities, so we need to wait for the device to be online.
288  LOGGER.debug(
289  "Setup for device %s will resume when device is online", entry.title
290  )
291  runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device)
292  runtime_data.rpc.async_setup(runtime_data.platforms)
293  # Try to connect to the device, if we reached here from config flow
294  # and user woke up the device when adding it, we can continue setup
295  # otherwise we will wait for the device to wake up
296  if sleep_period:
297  await runtime_data.rpc.async_device_online("setup")
298  else:
299  # Restore sensors for sleeping device
300  LOGGER.debug("Setting up offline RPC device %s", entry.title)
301  runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device)
302  runtime_data.rpc.async_setup()
303  await hass.config_entries.async_forward_entry_setups(
304  entry, runtime_data.platforms
305  )
306 
307  ir.async_delete_issue(
308  hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id)
309  )
310  return True
311 
312 
313 async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool:
314  """Unload a config entry."""
315  # delete push update issue if it exists
316  LOGGER.debug(
317  "Deleting issue %s", PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id)
318  )
319  ir.async_delete_issue(
320  hass, DOMAIN, PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id)
321  )
322 
323  runtime_data = entry.runtime_data
324 
325  if runtime_data.rpc:
326  await runtime_data.rpc.shutdown()
327 
328  if runtime_data.block:
329  await runtime_data.block.shutdown()
330 
331  return await hass.config_entries.async_unload_platforms(
332  entry, runtime_data.platforms
333  )
WsServer get_ws_context(HomeAssistant hass)
Definition: utils.py:274
COAP get_coap_context(HomeAssistant hass)
Definition: utils.py:221
None async_create_issue_unsupported_firmware(HomeAssistant hass, ConfigEntry entry)
Definition: utils.py:465
int get_device_entry_gen(ConfigEntry entry)
Definition: utils.py:353
int get_http_port(MappingProxyType[str, Any] data)
Definition: utils.py:492
bool async_setup_entry(HomeAssistant hass, ShellyConfigEntry entry)
Definition: __init__.py:102
bool _async_setup_block_entry(HomeAssistant hass, ShellyConfigEntry entry)
Definition: __init__.py:127
bool _async_setup_rpc_entry(HomeAssistant hass, ShellyConfigEntry entry)
Definition: __init__.py:225
bool async_unload_entry(HomeAssistant hass, ShellyConfigEntry entry)
Definition: __init__.py:313
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:94
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)