1 """Utilities used by insteon component."""
3 from __future__
import annotations
6 from collections.abc
import Callable
8 from typing
import TYPE_CHECKING, Any
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,
19 from pyinsteon.managers.scene_manager
import (
20 async_trigger_scene_off,
21 async_trigger_scene_on,
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,
28 from pyinsteon.x10_address
import create
as create_x10_address
29 from serial.tools
import list_ports
42 async_dispatcher_connect,
43 async_dispatcher_send,
60 SIGNAL_ADD_DEFAULT_LINKS,
61 SIGNAL_ADD_DEVICE_OVERRIDE,
63 SIGNAL_ADD_X10_DEVICE,
66 SIGNAL_REMOVE_DEVICE_OVERRIDE,
68 SIGNAL_REMOVE_HA_DEVICE,
69 SIGNAL_REMOVE_INSTEON_DEVICE,
70 SIGNAL_REMOVE_X10_DEVICE,
73 SRV_ADD_DEFAULT_LINKS,
85 SRV_X10_ALL_LIGHTS_OFF,
86 SRV_X10_ALL_LIGHTS_ON,
87 SRV_X10_ALL_UNITS_OFF,
89 from .ipdb
import get_device_platform_groups, get_device_platforms
90 from .schemas
import (
92 ADD_DEFAULT_LINKS_SCHEMA,
101 from .entity
import InsteonEntity
103 _LOGGER = logging.getLogger(__name__)
107 """Register the events raised by a device."""
109 "Registering on/off event for %s %d %s",
114 event.subscribe(listener, force_strong_ref=
True)
118 """Register Insteon device events."""
121 def async_fire_insteon_event(
122 name: str, address: Address, group: int, button: str |
None =
None
125 if button
and button[-2] ==
"_":
126 button_id = button[-1].lower()
130 schema = {CONF_ADDRESS: address,
"group": group}
132 schema[EVENT_CONF_BUTTON] = button_id
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
142 event = f
"insteon.{name}"
143 _LOGGER.debug(
"Firing event %s with %s", event, schema)
144 hass.bus.async_fire(event, schema)
146 if str(device.address).startswith(
"X10"):
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():
158 """Register callback for new Insteon device."""
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))
166 async
def async_create_new_entities(address):
168 "Adding new INSTEON device to Home Assistant with address %s", address
170 await devices.async_save(workdir=hass.config.config_dir)
171 device = devices[address]
172 await device.async_status()
174 for platform
in platforms:
176 signal = f
"{SIGNAL_ADD_ENTITIES}_{platform}"
177 dispatcher_send(hass, signal, {
"address": device.address,
"groups": groups})
180 devices.subscribe(async_new_insteon_device, force_strong_ref=
True)
185 """Register services used by insteon component."""
187 save_lock = asyncio.Lock()
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)
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)
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)
208 signal = f
"{entity_id}_{SIGNAL_LOAD_ALDB}"
211 async
def async_srv_load_aldb_all(reload):
212 """Load the All-Link database for all devices."""
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()
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)
226 def print_aldb(service: ServiceCall) ->
None:
227 """Print the All-Link Database for a device."""
230 entity_id = service.data[CONF_ENTITY_ID]
231 signal = f
"{entity_id}_{SIGNAL_PRINT_ALDB}"
234 def print_im_aldb(service: ServiceCall) ->
None:
235 """Print the All-Link Database for a device."""
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)
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)
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)
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)
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)
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}"
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()
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()
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)
295 if platform ==
"light":
296 x10_type =
"dimmable"
297 elif platform ==
"binary_sensor":
300 "Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
303 devices.add_x10_device(housecode, unitcode, x10_type, steps)
305 async
def async_remove_x10_device(housecode, unitcode):
306 """Remove an X10 device and associated entities."""
307 address = create_x10_address(housecode, unitcode)
309 await async_remove_ha_device(address)
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}"
315 dev_registry = dr.async_get(hass)
316 device = dev_registry.async_get_device(identifiers={(DOMAIN,
str(address))})
318 dev_registry.async_remove_device(device.id)
320 async
def async_remove_insteon_device(
321 address: Address, remove_all_refs: bool =
False
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
327 await async_srv_save_devices()
329 hass.services.async_register(
330 DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA
332 hass.services.async_register(
333 DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA
335 hass.services.async_register(
336 DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA
338 hass.services.async_register(
339 DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
341 hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=
None)
342 hass.services.async_register(
344 SRV_X10_ALL_UNITS_OFF,
345 async_srv_x10_all_units_off,
346 schema=X10_HOUSECODE_SCHEMA,
348 hass.services.async_register(
350 SRV_X10_ALL_LIGHTS_OFF,
351 async_srv_x10_all_lights_off,
352 schema=X10_HOUSECODE_SCHEMA,
354 hass.services.async_register(
356 SRV_X10_ALL_LIGHTS_ON,
357 async_srv_x10_all_lights_on,
358 schema=X10_HOUSECODE_SCHEMA,
360 hass.services.async_register(
361 DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA
363 hass.services.async_register(
364 DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA
367 hass.services.async_register(
369 SRV_ADD_DEFAULT_LINKS,
370 async_add_default_links,
371 schema=ADD_DEFAULT_LINKS_SCHEMA,
375 hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override
378 hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override
384 hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device
386 _LOGGER.debug(
"Insteon Services registered")
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")
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:
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"
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}"
417 entity_type: type[InsteonEntity],
418 async_add_entities: AddEntitiesCallback,
419 discovery_info: dict[str, Any],
421 """Add an Insteon group to a platform."""
422 address = discovery_info[
"address"]
423 device = devices[address]
425 entity_type(device=device, group=group)
for group
in discovery_info[
"groups"]
434 entity_type: type[InsteonEntity],
435 async_add_entities: AddEntitiesCallback,
437 """Add all entities to a platform."""
438 for address
in devices:
439 device = devices[address]
441 discovery_info = {
"address": address,
"groups": groups}
443 hass, platform, entity_type, async_add_entities, discovery_info
448 """Return a dict of USB ports and their friendly names."""
449 ports = list_ports.comports()
450 port_descriptions = {}
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)
458 dev_path = usb.get_serial_by_id(port.device)
459 human_name = usb.human_readable_device_name(
467 port_descriptions[dev_path] = human_name
468 return port_descriptions
472 """Return a dict of USB ports and their friendly names."""
473 return await hass.async_add_executor_job(get_usb_ports)
477 """Return the HA device name."""
478 return ha_device.name_by_user
if ha_device.name_by_user
else ha_device.name
482 """Get the Insteon device name from a device registry id."""
483 ha_device = dev_registry.async_get_device(identifiers={(DOMAIN,
str(address))})
485 if device := devices[address]:
486 return f
"{device.description} ({device.model})"
dict[Platform, Iterable[int]] get_device_platforms(device)
Iterable[int] get_device_platform_groups(Device device, Platform platform)
str compute_device_name(ha_device)
def print_aldb_to_log(aldb)
None async_add_insteon_devices(HomeAssistant hass, Platform platform, type[InsteonEntity] entity_type, AddEntitiesCallback async_add_entities)
def async_register_services(hass)
None _register_event(Event event, Callable listener)
def register_new_device_callback(hass)
None add_insteon_events(HomeAssistant hass, Device device)
str async_device_name(dr.DeviceRegistry dev_registry, Address address)
dict[str, str] async_get_usb_ports(HomeAssistant hass)
None async_add_insteon_entities(HomeAssistant hass, Platform platform, type[InsteonEntity] entity_type, AddEntitiesCallback async_add_entities, dict[str, Any] discovery_info)
dict[str, str] get_usb_ports()
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None dispatcher_send(HomeAssistant hass, str signal, *Any args)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)