Home Assistant Unofficial Reference 2024.12.1
switch.py
Go to the documentation of this file.
1 """Switch for Shelly."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from typing import Any, cast
7 
8 from aioshelly.block_device import Block
9 from aioshelly.const import MODEL_2, MODEL_25, MODEL_WALL_DISPLAY, RPC_GENERATIONS
10 
12  DOMAIN as SWITCH_PLATFORM,
13  SwitchEntity,
14  SwitchEntityDescription,
15 )
16 from homeassistant.const import STATE_ON, EntityCategory
17 from homeassistant.core import HomeAssistant, State, callback
18 from homeassistant.helpers.entity_platform import AddEntitiesCallback
19 from homeassistant.helpers.entity_registry import RegistryEntry
20 from homeassistant.helpers.restore_state import RestoreEntity
21 
22 from .const import CONF_SLEEP_PERIOD, MOTION_MODELS
23 from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
24 from .entity import (
25  BlockEntityDescription,
26  RpcEntityDescription,
27  ShellyBlockEntity,
28  ShellyRpcAttributeEntity,
29  ShellyRpcEntity,
30  ShellySleepingBlockAttributeEntity,
31  async_setup_entry_attribute_entities,
32  async_setup_rpc_attribute_entities,
33 )
34 from .utils import (
35  async_remove_orphaned_entities,
36  async_remove_shelly_entity,
37  get_device_entry_gen,
38  get_rpc_key_ids,
39  get_virtual_component_ids,
40  is_block_channel_type_light,
41  is_rpc_channel_type_light,
42  is_rpc_thermostat_internal_actuator,
43  is_rpc_thermostat_mode,
44 )
45 
46 
47 @dataclass(frozen=True, kw_only=True)
49  """Class to describe a BLOCK switch."""
50 
51 
52 MOTION_SWITCH = BlockSwitchDescription(
53  key="sensor|motionActive",
54  name="Motion detection",
55  entity_category=EntityCategory.CONFIG,
56 )
57 
58 
59 @dataclass(frozen=True, kw_only=True)
61  """Class to describe a RPC virtual switch."""
62 
63 
64 RPC_VIRTUAL_SWITCH = RpcSwitchDescription(
65  key="boolean",
66  sub_key="value",
67 )
68 
69 RPC_SCRIPT_SWITCH = RpcSwitchDescription(
70  key="script",
71  sub_key="running",
72  entity_registry_enabled_default=False,
73  entity_category=EntityCategory.CONFIG,
74 )
75 
76 
78  hass: HomeAssistant,
79  config_entry: ShellyConfigEntry,
80  async_add_entities: AddEntitiesCallback,
81 ) -> None:
82  """Set up switches for device."""
83  if get_device_entry_gen(config_entry) in RPC_GENERATIONS:
84  return async_setup_rpc_entry(hass, config_entry, async_add_entities)
85 
86  return async_setup_block_entry(hass, config_entry, async_add_entities)
87 
88 
89 @callback
91  hass: HomeAssistant,
92  config_entry: ShellyConfigEntry,
93  async_add_entities: AddEntitiesCallback,
94 ) -> None:
95  """Set up entities for block device."""
96  coordinator = config_entry.runtime_data.block
97  assert coordinator
98 
99  # Add Shelly Motion as a switch
100  if coordinator.model in MOTION_MODELS:
102  hass,
103  config_entry,
104  async_add_entities,
105  {("sensor", "motionActive"): MOTION_SWITCH},
106  BlockSleepingMotionSwitch,
107  )
108  return
109 
110  if config_entry.data[CONF_SLEEP_PERIOD]:
111  return
112 
113  # In roller mode the relay blocks exist but do not contain required info
114  if (
115  coordinator.model in [MODEL_2, MODEL_25]
116  and coordinator.device.settings["mode"] != "relay"
117  ):
118  return
119 
120  relay_blocks = []
121  assert coordinator.device.blocks
122  for block in coordinator.device.blocks:
123  if (
124  block.type != "relay"
125  or block.channel is not None
127  coordinator.device.settings, int(block.channel)
128  )
129  ):
130  continue
131 
132  relay_blocks.append(block)
133  unique_id = f"{coordinator.mac}-{block.type}_{block.channel}"
134  async_remove_shelly_entity(hass, "light", unique_id)
135 
136  if not relay_blocks:
137  return
138 
139  async_add_entities(BlockRelaySwitch(coordinator, block) for block in relay_blocks)
140 
141 
142 @callback
144  hass: HomeAssistant,
145  config_entry: ShellyConfigEntry,
146  async_add_entities: AddEntitiesCallback,
147 ) -> None:
148  """Set up entities for RPC device."""
149  coordinator = config_entry.runtime_data.rpc
150  assert coordinator
151  switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch")
152 
153  switch_ids = []
154  for id_ in switch_key_ids:
155  if is_rpc_channel_type_light(coordinator.device.config, id_):
156  continue
157 
158  if coordinator.model == MODEL_WALL_DISPLAY:
159  # There are three configuration scenarios for WallDisplay:
160  # - relay mode (no thermostat)
161  # - thermostat mode using the internal relay as an actuator
162  # - thermostat mode using an external (from another device) relay as
163  # an actuator
164  if not is_rpc_thermostat_mode(id_, coordinator.device.status):
165  # The device is not in thermostat mode, we need to remove a climate
166  # entity
167  unique_id = f"{coordinator.mac}-thermostat:{id_}"
168  async_remove_shelly_entity(hass, "climate", unique_id)
169  elif is_rpc_thermostat_internal_actuator(coordinator.device.status):
170  # The internal relay is an actuator, skip this ID so as not to create
171  # a switch entity
172  continue
173 
174  switch_ids.append(id_)
175  unique_id = f"{coordinator.mac}-switch:{id_}"
176  async_remove_shelly_entity(hass, "light", unique_id)
177 
179  hass,
180  config_entry,
181  async_add_entities,
182  {"boolean": RPC_VIRTUAL_SWITCH},
183  RpcVirtualSwitch,
184  )
185 
187  hass,
188  config_entry,
189  async_add_entities,
190  {"script": RPC_SCRIPT_SWITCH},
191  RpcScriptSwitch,
192  )
193 
194  # the user can remove virtual components from the device configuration, so we need
195  # to remove orphaned entities
196  virtual_switch_ids = get_virtual_component_ids(
197  coordinator.device.config, SWITCH_PLATFORM
198  )
200  hass,
201  config_entry.entry_id,
202  coordinator.mac,
203  SWITCH_PLATFORM,
204  virtual_switch_ids,
205  "boolean",
206  )
207 
208  # if the script is removed, from the device configuration, we need
209  # to remove orphaned entities
211  hass,
212  config_entry.entry_id,
213  coordinator.mac,
214  SWITCH_PLATFORM,
215  coordinator.device.status,
216  "script",
217  )
218 
219  if not switch_ids:
220  return
221 
222  async_add_entities(RpcRelaySwitch(coordinator, id_) for id_ in switch_ids)
223 
224 
226  ShellySleepingBlockAttributeEntity, RestoreEntity, SwitchEntity
227 ):
228  """Entity that controls Motion Sensor on Block based Shelly devices."""
229 
230  entity_description: BlockSwitchDescription
231  _attr_translation_key = "motion_switch"
232 
233  def __init__(
234  self,
235  coordinator: ShellyBlockCoordinator,
236  block: Block | None,
237  attribute: str,
238  description: BlockSwitchDescription,
239  entry: RegistryEntry | None = None,
240  ) -> None:
241  """Initialize the sleeping sensor."""
242  super().__init__(coordinator, block, attribute, description, entry)
243  self.last_statelast_state: State | None = None
244 
245  @property
246  def is_on(self) -> bool | None:
247  """If motion is active."""
248  if self.blockblockblock is not None:
249  return bool(self.blockblockblock.motionActive)
250 
251  if self.last_statelast_state is None:
252  return None
253 
254  return self.last_statelast_state.state == STATE_ON
255 
256  async def async_turn_on(self, **kwargs: Any) -> None:
257  """Activate switch."""
258  await self.coordinatorcoordinator.device.set_shelly_motion_detection(True)
259  self.async_write_ha_stateasync_write_ha_state()
260 
261  async def async_turn_off(self, **kwargs: Any) -> None:
262  """Deactivate switch."""
263  await self.coordinatorcoordinator.device.set_shelly_motion_detection(False)
264  self.async_write_ha_stateasync_write_ha_state()
265 
266  async def async_added_to_hass(self) -> None:
267  """Handle entity which will be added."""
268  await super().async_added_to_hass()
269  if (last_state := await self.async_get_last_stateasync_get_last_state()) is not None:
270  self.last_statelast_state = last_state
271 
272 
274  """Entity that controls a relay on Block based Shelly devices."""
275 
276  def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
277  """Initialize relay switch."""
278  super().__init__(coordinator, block)
279  self.control_resultcontrol_result: dict[str, Any] | None = None
280 
281  @property
282  def is_on(self) -> bool:
283  """If switch is on."""
284  if self.control_resultcontrol_result:
285  return cast(bool, self.control_resultcontrol_result["ison"])
286 
287  return bool(self.blockblock.output)
288 
289  async def async_turn_on(self, **kwargs: Any) -> None:
290  """Turn on relay."""
291  self.control_resultcontrol_result = await self.set_stateset_state(turn="on")
292  self.async_write_ha_stateasync_write_ha_state()
293 
294  async def async_turn_off(self, **kwargs: Any) -> None:
295  """Turn off relay."""
296  self.control_resultcontrol_result = await self.set_stateset_state(turn="off")
297  self.async_write_ha_stateasync_write_ha_state()
298 
299  @callback
300  def _update_callback(self) -> None:
301  """When device updates, clear control result that overrides state."""
302  self.control_resultcontrol_result = None
303  super()._update_callback()
304 
305 
307  """Entity that controls a relay on RPC based Shelly devices."""
308 
309  def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
310  """Initialize relay switch."""
311  super().__init__(coordinator, f"switch:{id_}")
312  self._id_id = id_
313 
314  @property
315  def is_on(self) -> bool:
316  """If switch is on."""
317  return bool(self.statusstatus["output"])
318 
319  async def async_turn_on(self, **kwargs: Any) -> None:
320  """Turn on relay."""
321  await self.call_rpccall_rpc("Switch.Set", {"id": self._id_id, "on": True})
322 
323  async def async_turn_off(self, **kwargs: Any) -> None:
324  """Turn off relay."""
325  await self.call_rpccall_rpc("Switch.Set", {"id": self._id_id, "on": False})
326 
327 
329  """Entity that controls a virtual boolean component on RPC based Shelly devices."""
330 
331  entity_description: RpcSwitchDescription
332  _attr_has_entity_name = True
333 
334  @property
335  def is_on(self) -> bool:
336  """If switch is on."""
337  return bool(self.attribute_valueattribute_value)
338 
339  async def async_turn_on(self, **kwargs: Any) -> None:
340  """Turn on relay."""
341  await self.call_rpccall_rpc("Boolean.Set", {"id": self._id_id, "value": True})
342 
343  async def async_turn_off(self, **kwargs: Any) -> None:
344  """Turn off relay."""
345  await self.call_rpccall_rpc("Boolean.Set", {"id": self._id_id, "value": False})
346 
347 
349  """Entity that controls a script component on RPC based Shelly devices."""
350 
351  entity_description: RpcSwitchDescription
352  _attr_has_entity_name = True
353 
354  @property
355  def is_on(self) -> bool:
356  """If switch is on."""
357  return bool(self.statusstatus["running"])
358 
359  async def async_turn_on(self, **kwargs: Any) -> None:
360  """Turn on relay."""
361  await self.call_rpccall_rpc("Script.Start", {"id": self._id_id})
362 
363  async def async_turn_off(self, **kwargs: Any) -> None:
364  """Turn off relay."""
365  await self.call_rpccall_rpc("Script.Stop", {"id": self._id_id})
Any call_rpc(self, str method, Any params)
Definition: entity.py:384
None __init__(self, ShellyBlockCoordinator coordinator, Block block)
Definition: switch.py:276
None __init__(self, ShellyBlockCoordinator coordinator, Block|None block, str attribute, BlockSwitchDescription description, RegistryEntry|None entry=None)
Definition: switch.py:240
None __init__(self, ShellyRpcCoordinator coordinator, int id_)
Definition: switch.py:309
None async_setup_rpc_attribute_entities(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, Mapping[str, RpcEntityDescription] sensors, Callable sensor_class)
Definition: entity.py:164
None async_setup_entry_attribute_entities(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, Mapping[tuple[str, str], BlockEntityDescription] sensors, Callable sensor_class)
Definition: entity.py:39
None async_setup_rpc_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: switch.py:147
None async_setup_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: switch.py:81
None async_setup_block_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: switch.py:94
bool is_rpc_channel_type_light(dict[str, Any] config, int channel)
Definition: utils.py:387
bool is_rpc_thermostat_internal_actuator(dict[str, Any] status)
Definition: utils.py:395
list[str] get_virtual_component_ids(dict[str, Any] config, str platform)
Definition: utils.py:528
bool is_rpc_thermostat_mode(int ident, dict[str, Any] status)
Definition: utils.py:523
None async_remove_orphaned_entities(HomeAssistant hass, str config_entry_id, str mac, str platform, Iterable[str] keys, str|None key_suffix=None)
Definition: utils.py:555
list[int] get_rpc_key_ids(dict[str, Any] keys_dict, str key)
Definition: utils.py:369
int get_device_entry_gen(ConfigEntry entry)
Definition: utils.py:353
bool is_block_channel_type_light(dict[str, Any] settings, int channel)
Definition: utils.py:381
None async_remove_shelly_entity(HomeAssistant hass, str domain, str unique_id)
Definition: utils.py:67