Home Assistant Unofficial Reference 2024.12.1
utils.py
Go to the documentation of this file.
1 """Utilities used by insteon component."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable
7 import logging
8 from typing import TYPE_CHECKING, Any
9 
10 from pyinsteon import devices
11 from pyinsteon.address import Address
12 from pyinsteon.constants import ALDBStatus, DeviceAction
13 from pyinsteon.device_types.device_base import Device
14 from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT, Event
15 from pyinsteon.managers.link_manager import (
16  async_enter_linking_mode,
17  async_enter_unlinking_mode,
18 )
19 from pyinsteon.managers.scene_manager import (
20  async_trigger_scene_off,
21  async_trigger_scene_on,
22 )
23 from pyinsteon.managers.x10_manager import (
24  async_x10_all_lights_off,
25  async_x10_all_lights_on,
26  async_x10_all_units_off,
27 )
28 from pyinsteon.x10_address import create as create_x10_address
29 from serial.tools import list_ports
30 
31 from homeassistant.components import usb
32 from homeassistant.const import (
33  CONF_ADDRESS,
34  CONF_ENTITY_ID,
35  CONF_PLATFORM,
36  ENTITY_MATCH_ALL,
37  Platform,
38 )
39 from homeassistant.core import HomeAssistant, ServiceCall, callback
40 from homeassistant.helpers import device_registry as dr
42  async_dispatcher_connect,
43  async_dispatcher_send,
44  dispatcher_send,
45 )
46 from homeassistant.helpers.entity_platform import AddEntitiesCallback
47 
48 from .const import (
49  CONF_CAT,
50  CONF_DIM_STEPS,
51  CONF_HOUSECODE,
52  CONF_SUBCAT,
53  CONF_UNITCODE,
54  DOMAIN,
55  EVENT_CONF_BUTTON,
56  EVENT_GROUP_OFF,
57  EVENT_GROUP_OFF_FAST,
58  EVENT_GROUP_ON,
59  EVENT_GROUP_ON_FAST,
60  SIGNAL_ADD_DEFAULT_LINKS,
61  SIGNAL_ADD_DEVICE_OVERRIDE,
62  SIGNAL_ADD_ENTITIES,
63  SIGNAL_ADD_X10_DEVICE,
64  SIGNAL_LOAD_ALDB,
65  SIGNAL_PRINT_ALDB,
66  SIGNAL_REMOVE_DEVICE_OVERRIDE,
67  SIGNAL_REMOVE_ENTITY,
68  SIGNAL_REMOVE_HA_DEVICE,
69  SIGNAL_REMOVE_INSTEON_DEVICE,
70  SIGNAL_REMOVE_X10_DEVICE,
71  SIGNAL_SAVE_DEVICES,
72  SRV_ADD_ALL_LINK,
73  SRV_ADD_DEFAULT_LINKS,
74  SRV_ALL_LINK_GROUP,
75  SRV_ALL_LINK_MODE,
76  SRV_CONTROLLER,
77  SRV_DEL_ALL_LINK,
78  SRV_HOUSECODE,
79  SRV_LOAD_ALDB,
80  SRV_LOAD_DB_RELOAD,
81  SRV_PRINT_ALDB,
82  SRV_PRINT_IM_ALDB,
83  SRV_SCENE_OFF,
84  SRV_SCENE_ON,
85  SRV_X10_ALL_LIGHTS_OFF,
86  SRV_X10_ALL_LIGHTS_ON,
87  SRV_X10_ALL_UNITS_OFF,
88 )
89 from .ipdb import get_device_platform_groups, get_device_platforms
90 from .schemas import (
91  ADD_ALL_LINK_SCHEMA,
92  ADD_DEFAULT_LINKS_SCHEMA,
93  DEL_ALL_LINK_SCHEMA,
94  LOAD_ALDB_SCHEMA,
95  PRINT_ALDB_SCHEMA,
96  TRIGGER_SCENE_SCHEMA,
97  X10_HOUSECODE_SCHEMA,
98 )
99 
100 if TYPE_CHECKING:
101  from .entity import InsteonEntity
102 
103 _LOGGER = logging.getLogger(__name__)
104 
105 
106 def _register_event(event: Event, listener: Callable) -> None:
107  """Register the events raised by a device."""
108  _LOGGER.debug(
109  "Registering on/off event for %s %d %s",
110  str(event.address),
111  event.group,
112  event.name,
113  )
114  event.subscribe(listener, force_strong_ref=True)
115 
116 
117 def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
118  """Register Insteon device events."""
119 
120  @callback
121  def async_fire_insteon_event(
122  name: str, address: Address, group: int, button: str | None = None
123  ):
124  # Firing an event when a button is pressed.
125  if button and button[-2] == "_":
126  button_id = button[-1].lower()
127  else:
128  button_id = None
129 
130  schema = {CONF_ADDRESS: address, "group": group}
131  if button_id:
132  schema[EVENT_CONF_BUTTON] = button_id
133  if name == ON_EVENT:
134  event = EVENT_GROUP_ON
135  elif name == OFF_EVENT:
136  event = EVENT_GROUP_OFF
137  elif name == ON_FAST_EVENT:
138  event = EVENT_GROUP_ON_FAST
139  elif name == OFF_FAST_EVENT:
140  event = EVENT_GROUP_OFF_FAST
141  else:
142  event = f"insteon.{name}"
143  _LOGGER.debug("Firing event %s with %s", event, schema)
144  hass.bus.async_fire(event, schema)
145 
146  if str(device.address).startswith("X10"):
147  return
148 
149  for name_or_group, event in device.events.items():
150  if isinstance(name_or_group, int):
151  for event in device.events[name_or_group].values():
152  _register_event(event, async_fire_insteon_event)
153  else:
154  _register_event(event, async_fire_insteon_event)
155 
156 
158  """Register callback for new Insteon device."""
159 
160  @callback
161  def async_new_insteon_device(address, action: DeviceAction):
162  """Detect device from transport to be delegated to platform."""
163  if action == DeviceAction.ADDED:
164  hass.async_create_task(async_create_new_entities(address))
165 
166  async def async_create_new_entities(address):
167  _LOGGER.debug(
168  "Adding new INSTEON device to Home Assistant with address %s", address
169  )
170  await devices.async_save(workdir=hass.config.config_dir)
171  device = devices[address]
172  await device.async_status()
173  platforms = get_device_platforms(device)
174  for platform in platforms:
175  groups = get_device_platform_groups(device, platform)
176  signal = f"{SIGNAL_ADD_ENTITIES}_{platform}"
177  dispatcher_send(hass, signal, {"address": device.address, "groups": groups})
178  add_insteon_events(hass, device)
179 
180  devices.subscribe(async_new_insteon_device, force_strong_ref=True)
181 
182 
183 @callback
184 def async_register_services(hass): # noqa: C901
185  """Register services used by insteon component."""
186 
187  save_lock = asyncio.Lock()
188 
189  async def async_srv_add_all_link(service: ServiceCall) -> None:
190  """Add an INSTEON All-Link between two devices."""
191  group = service.data[SRV_ALL_LINK_GROUP]
192  mode = service.data[SRV_ALL_LINK_MODE]
193  link_mode = mode.lower() == SRV_CONTROLLER
194  await async_enter_linking_mode(link_mode, group)
195 
196  async def async_srv_del_all_link(service: ServiceCall) -> None:
197  """Delete an INSTEON All-Link between two devices."""
198  group = service.data.get(SRV_ALL_LINK_GROUP)
199  await async_enter_unlinking_mode(group)
200 
201  async def async_srv_load_aldb(service: ServiceCall) -> None:
202  """Load the device All-Link database."""
203  entity_id = service.data[CONF_ENTITY_ID]
204  reload = service.data[SRV_LOAD_DB_RELOAD]
205  if entity_id.lower() == ENTITY_MATCH_ALL:
206  await async_srv_load_aldb_all(reload)
207  else:
208  signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
209  async_dispatcher_send(hass, signal, reload)
210 
211  async def async_srv_load_aldb_all(reload):
212  """Load the All-Link database for all devices."""
213  # Cannot be done concurrently due to issues with the underlying protocol.
214  for address in devices:
215  device = devices[address]
216  if device != devices.modem and device.cat != 0x03:
217  await device.aldb.async_load(refresh=reload)
218  await async_srv_save_devices()
219 
220  async def async_srv_save_devices():
221  """Write the Insteon device configuration to file."""
222  async with save_lock:
223  _LOGGER.debug("Saving Insteon devices")
224  await devices.async_save(hass.config.config_dir)
225 
226  def print_aldb(service: ServiceCall) -> None:
227  """Print the All-Link Database for a device."""
228  # For now this sends logs to the log file.
229  # Future direction is to create an INSTEON control panel.
230  entity_id = service.data[CONF_ENTITY_ID]
231  signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
232  dispatcher_send(hass, signal)
233 
234  def print_im_aldb(service: ServiceCall) -> None:
235  """Print the All-Link Database for a device."""
236  # For now this sends logs to the log file.
237  # Future direction is to create an INSTEON control panel.
238  print_aldb_to_log(devices.modem.aldb)
239 
240  async def async_srv_x10_all_units_off(service: ServiceCall) -> None:
241  """Send the X10 All Units Off command."""
242  housecode = service.data.get(SRV_HOUSECODE)
243  await async_x10_all_units_off(housecode)
244 
245  async def async_srv_x10_all_lights_off(service: ServiceCall) -> None:
246  """Send the X10 All Lights Off command."""
247  housecode = service.data.get(SRV_HOUSECODE)
248  await async_x10_all_lights_off(housecode)
249 
250  async def async_srv_x10_all_lights_on(service: ServiceCall) -> None:
251  """Send the X10 All Lights On command."""
252  housecode = service.data.get(SRV_HOUSECODE)
253  await async_x10_all_lights_on(housecode)
254 
255  async def async_srv_scene_on(service: ServiceCall) -> None:
256  """Trigger an INSTEON scene ON."""
257  group = service.data.get(SRV_ALL_LINK_GROUP)
258  await async_trigger_scene_on(group)
259 
260  async def async_srv_scene_off(service: ServiceCall) -> None:
261  """Trigger an INSTEON scene ON."""
262  group = service.data.get(SRV_ALL_LINK_GROUP)
263  await async_trigger_scene_off(group)
264 
265  @callback
266  def async_add_default_links(service: ServiceCall) -> None:
267  """Add the default All-Link entries to a device."""
268  entity_id = service.data[CONF_ENTITY_ID]
269  signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}"
270  async_dispatcher_send(hass, signal)
271 
272  async def async_add_device_override(override):
273  """Remove an Insten device and associated entities."""
274  address = Address(override[CONF_ADDRESS])
275  await async_remove_ha_device(address)
276  devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0)
277  await async_srv_save_devices()
278 
279  async def async_remove_device_override(address):
280  """Remove an Insten device and associated entities."""
281  address = Address(address)
282  await async_remove_ha_device(address)
283  devices.set_id(address, None, None, None)
284  await devices.async_identify_device(address)
285  await async_srv_save_devices()
286 
287  @callback
288  def async_add_x10_device(x10_config):
289  """Add X10 device."""
290  housecode = x10_config[CONF_HOUSECODE]
291  unitcode = x10_config[CONF_UNITCODE]
292  platform = x10_config[CONF_PLATFORM]
293  steps = x10_config.get(CONF_DIM_STEPS, 22)
294  x10_type = "on_off"
295  if platform == "light":
296  x10_type = "dimmable"
297  elif platform == "binary_sensor":
298  x10_type = "sensor"
299  _LOGGER.debug(
300  "Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
301  )
302  # This must be run in the event loop
303  devices.add_x10_device(housecode, unitcode, x10_type, steps)
304 
305  async def async_remove_x10_device(housecode, unitcode):
306  """Remove an X10 device and associated entities."""
307  address = create_x10_address(housecode, unitcode)
308  devices.pop(address)
309  await async_remove_ha_device(address)
310 
311  async def async_remove_ha_device(address: Address, remove_all_refs: bool = False):
312  """Remove the device and all entities from hass."""
313  signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}"
314  async_dispatcher_send(hass, signal)
315  dev_registry = dr.async_get(hass)
316  device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))})
317  if device:
318  dev_registry.async_remove_device(device.id)
319 
320  async def async_remove_insteon_device(
321  address: Address, remove_all_refs: bool = False
322  ):
323  """Remove the underlying Insteon device from the network."""
324  await devices.async_remove_device(
325  address=address, force=False, remove_all_refs=remove_all_refs
326  )
327  await async_srv_save_devices()
328 
329  hass.services.async_register(
330  DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA
331  )
332  hass.services.async_register(
333  DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA
334  )
335  hass.services.async_register(
336  DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA
337  )
338  hass.services.async_register(
339  DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
340  )
341  hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
342  hass.services.async_register(
343  DOMAIN,
344  SRV_X10_ALL_UNITS_OFF,
345  async_srv_x10_all_units_off,
346  schema=X10_HOUSECODE_SCHEMA,
347  )
348  hass.services.async_register(
349  DOMAIN,
350  SRV_X10_ALL_LIGHTS_OFF,
351  async_srv_x10_all_lights_off,
352  schema=X10_HOUSECODE_SCHEMA,
353  )
354  hass.services.async_register(
355  DOMAIN,
356  SRV_X10_ALL_LIGHTS_ON,
357  async_srv_x10_all_lights_on,
358  schema=X10_HOUSECODE_SCHEMA,
359  )
360  hass.services.async_register(
361  DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA
362  )
363  hass.services.async_register(
364  DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA
365  )
366 
367  hass.services.async_register(
368  DOMAIN,
369  SRV_ADD_DEFAULT_LINKS,
370  async_add_default_links,
371  schema=ADD_DEFAULT_LINKS_SCHEMA,
372  )
373  async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices)
375  hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override
376  )
378  hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override
379  )
380  async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device)
381  async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device)
382  async_dispatcher_connect(hass, SIGNAL_REMOVE_HA_DEVICE, async_remove_ha_device)
384  hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device
385  )
386  _LOGGER.debug("Insteon Services registered")
387 
388 
390  """Print the All-Link Database to the log file."""
391  logger = logging.getLogger(f"{__name__}.links")
392  logger.info("%s ALDB load status is %s", aldb.address, aldb.status.name)
393  if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]:
394  _LOGGER.warning("All-Link database not loaded")
395 
396  logger.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3")
397  logger.info("----- ------ ---- --- ----- -------- ------ ------ ------")
398  for mem_addr in aldb:
399  rec = aldb[mem_addr]
400  # For now we write this to the log
401  # Roadmap is to create a configuration panel
402  in_use = "Y" if rec.is_in_use else "N"
403  mode = "C" if rec.is_controller else "R"
404  hwm = "Y" if rec.is_high_water_mark else "N"
405  log_msg = (
406  f" {rec.mem_addr:04x} {in_use:s} {mode:s} {hwm:s} "
407  f"{rec.group:3d} {rec.target!s:s} {rec.data1:3d} "
408  f"{rec.data2:3d} {rec.data3:3d}"
409  )
410  logger.info(log_msg)
411 
412 
413 @callback
415  hass: HomeAssistant,
416  platform: Platform,
417  entity_type: type[InsteonEntity],
418  async_add_entities: AddEntitiesCallback,
419  discovery_info: dict[str, Any],
420 ) -> None:
421  """Add an Insteon group to a platform."""
422  address = discovery_info["address"]
423  device = devices[address]
424  new_entities = [
425  entity_type(device=device, group=group) for group in discovery_info["groups"]
426  ]
427  async_add_entities(new_entities)
428 
429 
430 @callback
432  hass: HomeAssistant,
433  platform: Platform,
434  entity_type: type[InsteonEntity],
435  async_add_entities: AddEntitiesCallback,
436 ) -> None:
437  """Add all entities to a platform."""
438  for address in devices:
439  device = devices[address]
440  groups = get_device_platform_groups(device, platform)
441  discovery_info = {"address": address, "groups": groups}
443  hass, platform, entity_type, async_add_entities, discovery_info
444  )
445 
446 
447 def get_usb_ports() -> dict[str, str]:
448  """Return a dict of USB ports and their friendly names."""
449  ports = list_ports.comports()
450  port_descriptions = {}
451  for port in ports:
452  vid: str | None = None
453  pid: str | None = None
454  if port.vid is not None and port.pid is not None:
455  usb_device = usb.usb_device_from_port(port)
456  vid = usb_device.vid
457  pid = usb_device.pid
458  dev_path = usb.get_serial_by_id(port.device)
459  human_name = usb.human_readable_device_name(
460  dev_path,
461  port.serial_number,
462  port.manufacturer,
463  port.description,
464  vid,
465  pid,
466  )
467  port_descriptions[dev_path] = human_name
468  return port_descriptions
469 
470 
471 async def async_get_usb_ports(hass: HomeAssistant) -> dict[str, str]:
472  """Return a dict of USB ports and their friendly names."""
473  return await hass.async_add_executor_job(get_usb_ports)
474 
475 
476 def compute_device_name(ha_device) -> str:
477  """Return the HA device name."""
478  return ha_device.name_by_user if ha_device.name_by_user else ha_device.name
479 
480 
481 async def async_device_name(dev_registry: dr.DeviceRegistry, address: Address) -> str:
482  """Get the Insteon device name from a device registry id."""
483  ha_device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))})
484  if not ha_device:
485  if device := devices[address]:
486  return f"{device.description} ({device.model})"
487  return ""
488  return compute_device_name(ha_device)
dict[Platform, Iterable[int]] get_device_platforms(device)
Definition: ipdb.py:112
Iterable[int] get_device_platform_groups(Device device, Platform platform)
Definition: ipdb.py:117
None async_add_insteon_devices(HomeAssistant hass, Platform platform, type[InsteonEntity] entity_type, AddEntitiesCallback async_add_entities)
Definition: utils.py:436
None _register_event(Event event, Callable listener)
Definition: utils.py:106
None add_insteon_events(HomeAssistant hass, Device device)
Definition: utils.py:117
str async_device_name(dr.DeviceRegistry dev_registry, Address address)
Definition: utils.py:481
dict[str, str] async_get_usb_ports(HomeAssistant hass)
Definition: utils.py:471
None async_add_insteon_entities(HomeAssistant hass, Platform platform, type[InsteonEntity] entity_type, AddEntitiesCallback async_add_entities, dict[str, Any] discovery_info)
Definition: utils.py:420
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:137
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193