Home Assistant Unofficial Reference 2024.12.1
switch.py
Go to the documentation of this file.
1 """Switch platform for UniFi Network integration.
2 
3 Support for controlling power supply of clients which are powered over Ethernet (POE).
4 Support for controlling network access of clients selected in option flow.
5 Support for controlling deep packet inspection (DPI) restriction groups.
6 Support for controlling WLAN availability.
7 """
8 
9 from __future__ import annotations
10 
11 import asyncio
12 from collections.abc import Callable, Coroutine
13 from dataclasses import dataclass
14 from typing import TYPE_CHECKING, Any
15 
16 import aiounifi
17 from aiounifi.interfaces.api_handlers import ItemEvent
18 from aiounifi.interfaces.clients import Clients
19 from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups
20 from aiounifi.interfaces.outlets import Outlets
21 from aiounifi.interfaces.port_forwarding import PortForwarding
22 from aiounifi.interfaces.ports import Ports
23 from aiounifi.interfaces.traffic_rules import TrafficRules
24 from aiounifi.interfaces.wlans import Wlans
25 from aiounifi.models.api import ApiItemT
26 from aiounifi.models.client import Client, ClientBlockRequest
27 from aiounifi.models.device import DeviceSetOutletRelayRequest
28 from aiounifi.models.dpi_restriction_app import DPIRestrictionAppEnableRequest
29 from aiounifi.models.dpi_restriction_group import DPIRestrictionGroup
30 from aiounifi.models.event import Event, EventKey
31 from aiounifi.models.outlet import Outlet
32 from aiounifi.models.port import Port
33 from aiounifi.models.port_forward import PortForward, PortForwardEnableRequest
34 from aiounifi.models.traffic_rule import TrafficRule, TrafficRuleEnableRequest
35 from aiounifi.models.wlan import Wlan, WlanEnableRequest
36 
38  DOMAIN as SWITCH_DOMAIN,
39  SwitchDeviceClass,
40  SwitchEntity,
41  SwitchEntityDescription,
42 )
43 from homeassistant.const import EntityCategory
44 from homeassistant.core import HomeAssistant, callback
45 from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
46 from homeassistant.helpers.entity_platform import AddEntitiesCallback
48 
49 from . import UnifiConfigEntry
50 from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN
51 from .entity import (
52  HandlerT,
53  SubscriptionT,
54  UnifiEntity,
55  UnifiEntityDescription,
56  async_client_device_info_fn,
57  async_device_available_fn,
58  async_device_device_info_fn,
59  async_wlan_device_info_fn,
60 )
61 from .hub import UnifiHub
62 
63 CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED)
64 CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED)
65 
66 
67 @callback
68 def async_block_client_allowed_fn(hub: UnifiHub, obj_id: str) -> bool:
69  """Check if client is allowed."""
70  if obj_id in hub.config.option_supported_clients:
71  return True
72  return obj_id in hub.config.option_block_clients
73 
74 
75 @callback
76 def async_dpi_group_is_on_fn(hub: UnifiHub, dpi_group: DPIRestrictionGroup) -> bool:
77  """Calculate if all apps are enabled."""
78  api = hub.api
79  return all(
80  api.dpi_apps[app_id].enabled
81  for app_id in dpi_group.dpiapp_ids or []
82  if app_id in api.dpi_apps
83  )
84 
85 
86 @callback
87 def async_dpi_group_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo:
88  """Create device registry entry for DPI group."""
89  return DeviceInfo(
90  entry_type=DeviceEntryType.SERVICE,
91  identifiers={(SWITCH_DOMAIN, f"unifi_controller_{obj_id}")},
92  manufacturer=ATTR_MANUFACTURER,
93  model="UniFi Network",
94  name="UniFi Network",
95  )
96 
97 
98 @callback
99 def async_unifi_network_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo:
100  """Create device registry entry for the UniFi Network application."""
101  unique_id = hub.config.entry.unique_id
102  assert unique_id is not None
103  return DeviceInfo(
104  entry_type=DeviceEntryType.SERVICE,
105  identifiers={(SWITCH_DOMAIN, unique_id)},
106  manufacturer=ATTR_MANUFACTURER,
107  model="UniFi Network",
108  name="UniFi Network",
109  )
110 
111 
113  hub: UnifiHub, obj_id: str, target: bool
114 ) -> None:
115  """Control network access of client."""
116  await hub.api.request(ClientBlockRequest.create(obj_id, not target))
117 
118 
119 async def async_dpi_group_control_fn(hub: UnifiHub, obj_id: str, target: bool) -> None:
120  """Enable or disable DPI group."""
121  dpi_group = hub.api.dpi_groups[obj_id]
122  await asyncio.gather(
123  *[
124  hub.api.request(DPIRestrictionAppEnableRequest.create(app_id, target))
125  for app_id in dpi_group.dpiapp_ids or []
126  ]
127  )
128 
129 
130 @callback
131 def async_outlet_switching_supported_fn(hub: UnifiHub, obj_id: str) -> bool:
132  """Determine if an outlet supports switching."""
133  outlet = hub.api.outlets[obj_id]
134  return outlet.has_relay or outlet.caps in (1, 3)
135 
136 
137 async def async_outlet_control_fn(hub: UnifiHub, obj_id: str, target: bool) -> None:
138  """Control outlet relay."""
139  mac, _, index = obj_id.partition("_")
140  device = hub.api.devices[mac]
141  await hub.api.request(
142  DeviceSetOutletRelayRequest.create(device, int(index), target)
143  )
144 
145 
146 async def async_poe_port_control_fn(hub: UnifiHub, obj_id: str, target: bool) -> None:
147  """Control poe state."""
148  mac, _, index = obj_id.partition("_")
149  port = hub.api.ports[obj_id]
150  on_state = "auto" if port.raw["poe_caps"] != 8 else "passthrough"
151  state = on_state if target else "off"
152  hub.queue_poe_port_command(mac, int(index), state)
153 
154 
156  hub: UnifiHub, obj_id: str, target: bool
157 ) -> None:
158  """Control port forward state."""
159  port_forward = hub.api.port_forwarding[obj_id]
160  await hub.api.request(PortForwardEnableRequest.create(port_forward, target))
161 
162 
164  hub: UnifiHub, obj_id: str, target: bool
165 ) -> None:
166  """Control traffic rule state."""
167  traffic_rule = hub.api.traffic_rules[obj_id].raw
168  await hub.api.request(TrafficRuleEnableRequest.create(traffic_rule, target))
169  # Update the traffic rules so the UI is updated appropriately
170  await hub.api.traffic_rules.update()
171 
172 
173 async def async_wlan_control_fn(hub: UnifiHub, obj_id: str, target: bool) -> None:
174  """Control outlet relay."""
175  await hub.api.request(WlanEnableRequest.create(obj_id, target))
176 
177 
178 @dataclass(frozen=True, kw_only=True)
180  SwitchEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT]
181 ):
182  """Class describing UniFi switch entity."""
183 
184  control_fn: Callable[[UnifiHub, str, bool], Coroutine[Any, Any, None]]
185  is_on_fn: Callable[[UnifiHub, ApiItemT], bool]
186 
187  # Optional
188  custom_subscribe: Callable[[aiounifi.Controller], SubscriptionT] | None = None
189  """Callback for additional subscriptions to any UniFi handler."""
190  only_event_for_state_change: bool = False
191  """Use only UniFi events to trigger state changes."""
192 
193 
194 ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = (
195  UnifiSwitchEntityDescription[Clients, Client](
196  key="Block client",
197  translation_key="block_client",
198  device_class=SwitchDeviceClass.SWITCH,
199  entity_category=EntityCategory.CONFIG,
200  allowed_fn=async_block_client_allowed_fn,
201  api_handler_fn=lambda api: api.clients,
202  control_fn=async_block_client_control_fn,
203  device_info_fn=async_client_device_info_fn,
204  event_is_on=set(CLIENT_UNBLOCKED),
205  event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED,
206  is_on_fn=lambda hub, client: not client.blocked,
207  object_fn=lambda api, obj_id: api.clients[obj_id],
208  only_event_for_state_change=True,
209  unique_id_fn=lambda hub, obj_id: f"block-{obj_id}",
210  ),
211  UnifiSwitchEntityDescription[DPIRestrictionGroups, DPIRestrictionGroup](
212  key="DPI restriction",
213  translation_key="dpi_restriction",
214  has_entity_name=False,
215  entity_category=EntityCategory.CONFIG,
216  allowed_fn=lambda hub, obj_id: hub.config.option_dpi_restrictions,
217  api_handler_fn=lambda api: api.dpi_groups,
218  control_fn=async_dpi_group_control_fn,
219  custom_subscribe=lambda api: api.dpi_apps.subscribe,
220  device_info_fn=async_dpi_group_device_info_fn,
221  is_on_fn=async_dpi_group_is_on_fn,
222  name_fn=lambda group: group.name,
223  object_fn=lambda api, obj_id: api.dpi_groups[obj_id],
224  supported_fn=lambda hub, obj_id: bool(hub.api.dpi_groups[obj_id].dpiapp_ids),
225  unique_id_fn=lambda hub, obj_id: obj_id,
226  ),
227  UnifiSwitchEntityDescription[Outlets, Outlet](
228  key="Outlet control",
229  device_class=SwitchDeviceClass.OUTLET,
230  api_handler_fn=lambda api: api.outlets,
231  available_fn=async_device_available_fn,
232  control_fn=async_outlet_control_fn,
233  device_info_fn=async_device_device_info_fn,
234  is_on_fn=lambda hub, outlet: outlet.relay_state,
235  name_fn=lambda outlet: outlet.name,
236  object_fn=lambda api, obj_id: api.outlets[obj_id],
237  supported_fn=async_outlet_switching_supported_fn,
238  unique_id_fn=lambda hub, obj_id: f"outlet-{obj_id}",
239  ),
240  UnifiSwitchEntityDescription[PortForwarding, PortForward](
241  key="Port forward control",
242  translation_key="port_forward_control",
243  device_class=SwitchDeviceClass.SWITCH,
244  entity_category=EntityCategory.CONFIG,
245  api_handler_fn=lambda api: api.port_forwarding,
246  control_fn=async_port_forward_control_fn,
247  device_info_fn=async_unifi_network_device_info_fn,
248  is_on_fn=lambda hub, port_forward: port_forward.enabled,
249  name_fn=lambda port_forward: f"{port_forward.name}",
250  object_fn=lambda api, obj_id: api.port_forwarding[obj_id],
251  unique_id_fn=lambda hub, obj_id: f"port_forward-{obj_id}",
252  ),
253  UnifiSwitchEntityDescription[TrafficRules, TrafficRule](
254  key="Traffic rule control",
255  translation_key="traffic_rule_control",
256  device_class=SwitchDeviceClass.SWITCH,
257  entity_category=EntityCategory.CONFIG,
258  api_handler_fn=lambda api: api.traffic_rules,
259  control_fn=async_traffic_rule_control_fn,
260  device_info_fn=async_unifi_network_device_info_fn,
261  is_on_fn=lambda hub, traffic_rule: traffic_rule.enabled,
262  name_fn=lambda traffic_rule: traffic_rule.description,
263  object_fn=lambda api, obj_id: api.traffic_rules[obj_id],
264  unique_id_fn=lambda hub, obj_id: f"traffic_rule-{obj_id}",
265  ),
266  UnifiSwitchEntityDescription[Ports, Port](
267  key="PoE port control",
268  translation_key="poe_port_control",
269  device_class=SwitchDeviceClass.OUTLET,
270  entity_category=EntityCategory.CONFIG,
271  entity_registry_enabled_default=False,
272  api_handler_fn=lambda api: api.ports,
273  available_fn=async_device_available_fn,
274  control_fn=async_poe_port_control_fn,
275  device_info_fn=async_device_device_info_fn,
276  is_on_fn=lambda hub, port: port.poe_mode != "off",
277  name_fn=lambda port: f"{port.name} PoE",
278  object_fn=lambda api, obj_id: api.ports[obj_id],
279  supported_fn=lambda hub, obj_id: bool(hub.api.ports[obj_id].port_poe),
280  unique_id_fn=lambda hub, obj_id: f"poe-{obj_id}",
281  ),
282  UnifiSwitchEntityDescription[Wlans, Wlan](
283  key="WLAN control",
284  translation_key="wlan_control",
285  device_class=SwitchDeviceClass.SWITCH,
286  entity_category=EntityCategory.CONFIG,
287  api_handler_fn=lambda api: api.wlans,
288  control_fn=async_wlan_control_fn,
289  device_info_fn=async_wlan_device_info_fn,
290  is_on_fn=lambda hub, wlan: wlan.enabled,
291  object_fn=lambda api, obj_id: api.wlans[obj_id],
292  unique_id_fn=lambda hub, obj_id: f"wlan-{obj_id}",
293  ),
294 )
295 
296 
297 @callback
298 def async_update_unique_id(hass: HomeAssistant, config_entry: UnifiConfigEntry) -> None:
299  """Normalize switch unique ID to have a prefix rather than midfix.
300 
301  Introduced with release 2023.12.
302  """
303  hub = config_entry.runtime_data
304  ent_reg = er.async_get(hass)
305 
306  @callback
307  def update_unique_id(obj_id: str, type_name: str) -> None:
308  """Rework unique ID."""
309  new_unique_id = f"{type_name}-{obj_id}"
310  if ent_reg.async_get_entity_id(SWITCH_DOMAIN, UNIFI_DOMAIN, new_unique_id):
311  return
312 
313  prefix, _, suffix = obj_id.partition("_")
314  unique_id = f"{prefix}-{type_name}-{suffix}"
315  if entity_id := ent_reg.async_get_entity_id(
316  SWITCH_DOMAIN, UNIFI_DOMAIN, unique_id
317  ):
318  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
319 
320  for obj_id in hub.api.outlets:
321  update_unique_id(obj_id, "outlet")
322 
323  for obj_id in hub.api.ports:
324  update_unique_id(obj_id, "poe")
325 
326 
328  hass: HomeAssistant,
329  config_entry: UnifiConfigEntry,
330  async_add_entities: AddEntitiesCallback,
331 ) -> None:
332  """Set up switches for UniFi Network integration."""
333  async_update_unique_id(hass, config_entry)
334  config_entry.runtime_data.entity_loader.register_platform(
335  async_add_entities,
336  UnifiSwitchEntity,
337  ENTITY_DESCRIPTIONS,
338  requires_admin=True,
339  )
340 
341 
342 class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity):
343  """Base representation of a UniFi switch."""
344 
345  entity_description: UnifiSwitchEntityDescription[HandlerT, ApiItemT]
346 
347  @callback
348  def async_initiate_state(self) -> None:
349  """Initiate entity state."""
350  self.async_update_stateasync_update_stateasync_update_state(ItemEvent.ADDED, self._obj_id_obj_id, first_update=True)
351 
352  async def async_turn_on(self, **kwargs: Any) -> None:
353  """Turn on switch."""
354  await self.entity_descriptionentity_description.control_fn(self.hubhub, self._obj_id_obj_id, True)
355 
356  async def async_turn_off(self, **kwargs: Any) -> None:
357  """Turn off switch."""
358  await self.entity_descriptionentity_description.control_fn(self.hubhub, self._obj_id_obj_id, False)
359 
360  @callback
362  self, event: ItemEvent, obj_id: str, first_update: bool = False
363  ) -> None:
364  """Update entity state.
365 
366  Update attr_is_on.
367  """
368  if not first_update and self.entity_descriptionentity_description.only_event_for_state_change:
369  return
370 
371  description = self.entity_descriptionentity_description
372  obj = description.object_fn(self.apiapi, self._obj_id_obj_id)
373  if (is_on := description.is_on_fn(self.hubhub, obj)) != self.is_onis_on:
374  self._attr_is_on_attr_is_on = is_on
375 
376  @callback
377  def async_event_callback(self, event: Event) -> None:
378  """Event subscription callback."""
379  if event.mac != self._obj_id_obj_id:
380  return
381 
382  description = self.entity_descriptionentity_description
383  if TYPE_CHECKING:
384  assert description.event_to_subscribe is not None
385  assert description.event_is_on is not None
386 
387  if event.key in description.event_to_subscribe:
388  self._attr_is_on_attr_is_on = event.key in description.event_is_on
389  self._attr_available_attr_available_attr_available = description.available_fn(self.hubhub, self._obj_id_obj_id)
390  self.async_write_ha_stateasync_write_ha_state()
391 
392  async def async_added_to_hass(self) -> None:
393  """Register callbacks."""
394  await super().async_added_to_hass()
395 
396  if self.entity_descriptionentity_description.custom_subscribe is not None:
397  self.async_on_removeasync_on_remove(
398  self.entity_descriptionentity_description.custom_subscribe(self.apiapi)(
399  self.async_signalling_callbackasync_signalling_callback, ItemEvent.CHANGED
400  ),
401  )
None async_signalling_callback(self, ItemEvent event, str obj_id)
Definition: entity.py:212
None async_update_state(self, ItemEvent event, str obj_id)
Definition: entity.py:262
None async_update_state(self, ItemEvent event, str obj_id, bool first_update=False)
Definition: switch.py:363
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
dict[str, str]|None update_unique_id(er.RegistryEntry entity_entry, str unique_id)
Definition: __init__.py:168
bool async_outlet_switching_supported_fn(UnifiHub hub, str obj_id)
Definition: switch.py:131
None async_dpi_group_control_fn(UnifiHub hub, str obj_id, bool target)
Definition: switch.py:119
None async_port_forward_control_fn(UnifiHub hub, str obj_id, bool target)
Definition: switch.py:157
None async_block_client_control_fn(UnifiHub hub, str obj_id, bool target)
Definition: switch.py:114
None async_wlan_control_fn(UnifiHub hub, str obj_id, bool target)
Definition: switch.py:173
None async_poe_port_control_fn(UnifiHub hub, str obj_id, bool target)
Definition: switch.py:146
DeviceInfo async_unifi_network_device_info_fn(UnifiHub hub, str obj_id)
Definition: switch.py:99
None async_setup_entry(HomeAssistant hass, UnifiConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: switch.py:331
None async_update_unique_id(HomeAssistant hass, UnifiConfigEntry config_entry)
Definition: switch.py:298
bool async_dpi_group_is_on_fn(UnifiHub hub, DPIRestrictionGroup dpi_group)
Definition: switch.py:76
None async_outlet_control_fn(UnifiHub hub, str obj_id, bool target)
Definition: switch.py:137
bool async_block_client_allowed_fn(UnifiHub hub, str obj_id)
Definition: switch.py:68
DeviceInfo async_dpi_group_device_info_fn(UnifiHub hub, str obj_id)
Definition: switch.py:87
None async_traffic_rule_control_fn(UnifiHub hub, str obj_id, bool target)
Definition: switch.py:165