* Make async_get_device connections Optional, default None * Remove unnecessary async_get_device connections arg usages Some of these were using an incorrect collection type, which didn't cause issues mostly just due to luck.
395 lines
14 KiB
Python
395 lines
14 KiB
Python
"""Utilities used by insteon component."""
|
|
import asyncio
|
|
import logging
|
|
|
|
from pyinsteon import devices
|
|
from pyinsteon.address import Address
|
|
from pyinsteon.constants import ALDBStatus
|
|
from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT
|
|
from pyinsteon.managers.link_manager import (
|
|
async_enter_linking_mode,
|
|
async_enter_unlinking_mode,
|
|
)
|
|
from pyinsteon.managers.scene_manager import (
|
|
async_trigger_scene_off,
|
|
async_trigger_scene_on,
|
|
)
|
|
from pyinsteon.managers.x10_manager import (
|
|
async_x10_all_lights_off,
|
|
async_x10_all_lights_on,
|
|
async_x10_all_units_off,
|
|
)
|
|
from pyinsteon.x10_address import create as create_x10_address
|
|
|
|
from homeassistant.const import (
|
|
CONF_ADDRESS,
|
|
CONF_ENTITY_ID,
|
|
CONF_PLATFORM,
|
|
ENTITY_MATCH_ALL,
|
|
)
|
|
from homeassistant.core import callback
|
|
from homeassistant.helpers.dispatcher import (
|
|
async_dispatcher_connect,
|
|
async_dispatcher_send,
|
|
dispatcher_send,
|
|
)
|
|
|
|
from .const import (
|
|
CONF_CAT,
|
|
CONF_DIM_STEPS,
|
|
CONF_HOUSECODE,
|
|
CONF_SUBCAT,
|
|
CONF_UNITCODE,
|
|
DOMAIN,
|
|
EVENT_CONF_BUTTON,
|
|
EVENT_GROUP_OFF,
|
|
EVENT_GROUP_OFF_FAST,
|
|
EVENT_GROUP_ON,
|
|
EVENT_GROUP_ON_FAST,
|
|
ON_OFF_EVENTS,
|
|
SIGNAL_ADD_DEFAULT_LINKS,
|
|
SIGNAL_ADD_DEVICE_OVERRIDE,
|
|
SIGNAL_ADD_ENTITIES,
|
|
SIGNAL_ADD_X10_DEVICE,
|
|
SIGNAL_LOAD_ALDB,
|
|
SIGNAL_PRINT_ALDB,
|
|
SIGNAL_REMOVE_DEVICE_OVERRIDE,
|
|
SIGNAL_REMOVE_ENTITY,
|
|
SIGNAL_REMOVE_X10_DEVICE,
|
|
SIGNAL_SAVE_DEVICES,
|
|
SRV_ADD_ALL_LINK,
|
|
SRV_ADD_DEFAULT_LINKS,
|
|
SRV_ALL_LINK_GROUP,
|
|
SRV_ALL_LINK_MODE,
|
|
SRV_CONTROLLER,
|
|
SRV_DEL_ALL_LINK,
|
|
SRV_HOUSECODE,
|
|
SRV_LOAD_ALDB,
|
|
SRV_LOAD_DB_RELOAD,
|
|
SRV_PRINT_ALDB,
|
|
SRV_PRINT_IM_ALDB,
|
|
SRV_SCENE_OFF,
|
|
SRV_SCENE_ON,
|
|
SRV_X10_ALL_LIGHTS_OFF,
|
|
SRV_X10_ALL_LIGHTS_ON,
|
|
SRV_X10_ALL_UNITS_OFF,
|
|
)
|
|
from .ipdb import get_device_platforms, get_platform_groups
|
|
from .schemas import (
|
|
ADD_ALL_LINK_SCHEMA,
|
|
ADD_DEFAULT_LINKS_SCHEMA,
|
|
DEL_ALL_LINK_SCHEMA,
|
|
LOAD_ALDB_SCHEMA,
|
|
PRINT_ALDB_SCHEMA,
|
|
TRIGGER_SCENE_SCHEMA,
|
|
X10_HOUSECODE_SCHEMA,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
def add_on_off_event_device(hass, device):
|
|
"""Register an Insteon device as an on/off event device."""
|
|
|
|
@callback
|
|
def async_fire_group_on_off_event(name, address, group, button):
|
|
# Firing an event when a button is pressed.
|
|
if button and button[-2] == "_":
|
|
button_id = button[-1].lower()
|
|
else:
|
|
button_id = None
|
|
|
|
schema = {CONF_ADDRESS: address}
|
|
if button_id:
|
|
schema[EVENT_CONF_BUTTON] = button_id
|
|
if name == ON_EVENT:
|
|
event = EVENT_GROUP_ON
|
|
if name == OFF_EVENT:
|
|
event = EVENT_GROUP_OFF
|
|
if name == ON_FAST_EVENT:
|
|
event = EVENT_GROUP_ON_FAST
|
|
if name == OFF_FAST_EVENT:
|
|
event = EVENT_GROUP_OFF_FAST
|
|
_LOGGER.debug("Firing event %s with %s", event, schema)
|
|
hass.bus.async_fire(event, schema)
|
|
|
|
for group in device.events:
|
|
if isinstance(group, int):
|
|
for event in device.events[group]:
|
|
if event in [
|
|
OFF_EVENT,
|
|
ON_EVENT,
|
|
OFF_FAST_EVENT,
|
|
ON_FAST_EVENT,
|
|
]:
|
|
_LOGGER.debug(
|
|
"Registering on/off event for %s %d %s",
|
|
str(device.address),
|
|
group,
|
|
event,
|
|
)
|
|
device.events[group][event].subscribe(
|
|
async_fire_group_on_off_event, force_strong_ref=True
|
|
)
|
|
|
|
|
|
def register_new_device_callback(hass):
|
|
"""Register callback for new Insteon device."""
|
|
|
|
@callback
|
|
def async_new_insteon_device(address=None):
|
|
"""Detect device from transport to be delegated to platform."""
|
|
hass.async_create_task(async_create_new_entities(address))
|
|
|
|
async def async_create_new_entities(address):
|
|
_LOGGER.debug(
|
|
"Adding new INSTEON device to Home Assistant with address %s", address
|
|
)
|
|
await devices.async_save(workdir=hass.config.config_dir)
|
|
device = devices[address]
|
|
await device.async_status()
|
|
platforms = get_device_platforms(device)
|
|
for platform in platforms:
|
|
if platform == ON_OFF_EVENTS:
|
|
add_on_off_event_device(hass, device)
|
|
|
|
else:
|
|
signal = f"{SIGNAL_ADD_ENTITIES}_{platform}"
|
|
dispatcher_send(hass, signal, {"address": device.address})
|
|
|
|
devices.subscribe(async_new_insteon_device, force_strong_ref=True)
|
|
|
|
|
|
@callback
|
|
def async_register_services(hass):
|
|
"""Register services used by insteon component."""
|
|
|
|
save_lock = asyncio.Lock()
|
|
|
|
async def async_srv_add_all_link(service):
|
|
"""Add an INSTEON All-Link between two devices."""
|
|
group = service.data.get(SRV_ALL_LINK_GROUP)
|
|
mode = service.data.get(SRV_ALL_LINK_MODE)
|
|
link_mode = mode.lower() == SRV_CONTROLLER
|
|
await async_enter_linking_mode(link_mode, group)
|
|
|
|
async def async_srv_del_all_link(service):
|
|
"""Delete an INSTEON All-Link between two devices."""
|
|
group = service.data.get(SRV_ALL_LINK_GROUP)
|
|
await async_enter_unlinking_mode(group)
|
|
|
|
async def async_srv_load_aldb(service):
|
|
"""Load the device All-Link database."""
|
|
entity_id = service.data[CONF_ENTITY_ID]
|
|
reload = service.data[SRV_LOAD_DB_RELOAD]
|
|
if entity_id.lower() == ENTITY_MATCH_ALL:
|
|
await async_srv_load_aldb_all(reload)
|
|
else:
|
|
signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
|
|
async_dispatcher_send(hass, signal, reload)
|
|
|
|
async def async_srv_load_aldb_all(reload):
|
|
"""Load the All-Link database for all devices."""
|
|
# Cannot be done concurrently due to issues with the underlying protocol.
|
|
for address in devices:
|
|
device = devices[address]
|
|
if device != devices.modem and device.cat != 0x03:
|
|
await device.aldb.async_load(
|
|
refresh=reload, callback=async_srv_save_devices
|
|
)
|
|
|
|
async def async_srv_save_devices():
|
|
"""Write the Insteon device configuration to file."""
|
|
async with save_lock:
|
|
_LOGGER.debug("Saving Insteon devices")
|
|
await devices.async_save(hass.config.config_dir)
|
|
|
|
def print_aldb(service):
|
|
"""Print the All-Link Database for a device."""
|
|
# For now this sends logs to the log file.
|
|
# Future direction is to create an INSTEON control panel.
|
|
entity_id = service.data[CONF_ENTITY_ID]
|
|
signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
|
|
dispatcher_send(hass, signal)
|
|
|
|
def print_im_aldb(service):
|
|
"""Print the All-Link Database for a device."""
|
|
# For now this sends logs to the log file.
|
|
# Future direction is to create an INSTEON control panel.
|
|
print_aldb_to_log(devices.modem.aldb)
|
|
|
|
async def async_srv_x10_all_units_off(service):
|
|
"""Send the X10 All Units Off command."""
|
|
housecode = service.data.get(SRV_HOUSECODE)
|
|
await async_x10_all_units_off(housecode)
|
|
|
|
async def async_srv_x10_all_lights_off(service):
|
|
"""Send the X10 All Lights Off command."""
|
|
housecode = service.data.get(SRV_HOUSECODE)
|
|
await async_x10_all_lights_off(housecode)
|
|
|
|
async def async_srv_x10_all_lights_on(service):
|
|
"""Send the X10 All Lights On command."""
|
|
housecode = service.data.get(SRV_HOUSECODE)
|
|
await async_x10_all_lights_on(housecode)
|
|
|
|
async def async_srv_scene_on(service):
|
|
"""Trigger an INSTEON scene ON."""
|
|
group = service.data.get(SRV_ALL_LINK_GROUP)
|
|
await async_trigger_scene_on(group)
|
|
|
|
async def async_srv_scene_off(service):
|
|
"""Trigger an INSTEON scene ON."""
|
|
group = service.data.get(SRV_ALL_LINK_GROUP)
|
|
await async_trigger_scene_off(group)
|
|
|
|
@callback
|
|
def async_add_default_links(service):
|
|
"""Add the default All-Link entries to a device."""
|
|
entity_id = service.data[CONF_ENTITY_ID]
|
|
signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}"
|
|
async_dispatcher_send(hass, signal)
|
|
|
|
async def async_add_device_override(override):
|
|
"""Remove an Insten device and associated entities."""
|
|
address = Address(override[CONF_ADDRESS])
|
|
await async_remove_device(address)
|
|
devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0)
|
|
await async_srv_save_devices()
|
|
|
|
async def async_remove_device_override(address):
|
|
"""Remove an Insten device and associated entities."""
|
|
address = Address(address)
|
|
await async_remove_device(address)
|
|
devices.set_id(address, None, None, None)
|
|
await devices.async_identify_device(address)
|
|
await async_srv_save_devices()
|
|
|
|
@callback
|
|
def async_add_x10_device(x10_config):
|
|
"""Add X10 device."""
|
|
housecode = x10_config[CONF_HOUSECODE]
|
|
unitcode = x10_config[CONF_UNITCODE]
|
|
platform = x10_config[CONF_PLATFORM]
|
|
steps = x10_config.get(CONF_DIM_STEPS, 22)
|
|
x10_type = "on_off"
|
|
if platform == "light":
|
|
x10_type = "dimmable"
|
|
elif platform == "binary_sensor":
|
|
x10_type = "sensor"
|
|
_LOGGER.debug(
|
|
"Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
|
|
)
|
|
# This must be run in the event loop
|
|
devices.add_x10_device(housecode, unitcode, x10_type, steps)
|
|
|
|
async def async_remove_x10_device(housecode, unitcode):
|
|
"""Remove an X10 device and associated entities."""
|
|
address = create_x10_address(housecode, unitcode)
|
|
devices.pop(address)
|
|
await async_remove_device(address)
|
|
|
|
async def async_remove_device(address):
|
|
"""Remove the device and all entities from hass."""
|
|
signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}"
|
|
async_dispatcher_send(hass, signal)
|
|
dev_registry = await hass.helpers.device_registry.async_get_registry()
|
|
device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))})
|
|
if device:
|
|
dev_registry.async_remove_device(device.id)
|
|
|
|
hass.services.async_register(
|
|
DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
|
|
)
|
|
hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SRV_X10_ALL_UNITS_OFF,
|
|
async_srv_x10_all_units_off,
|
|
schema=X10_HOUSECODE_SCHEMA,
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SRV_X10_ALL_LIGHTS_OFF,
|
|
async_srv_x10_all_lights_off,
|
|
schema=X10_HOUSECODE_SCHEMA,
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SRV_X10_ALL_LIGHTS_ON,
|
|
async_srv_x10_all_lights_on,
|
|
schema=X10_HOUSECODE_SCHEMA,
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA
|
|
)
|
|
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SRV_ADD_DEFAULT_LINKS,
|
|
async_add_default_links,
|
|
schema=ADD_DEFAULT_LINKS_SCHEMA,
|
|
)
|
|
async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices)
|
|
async_dispatcher_connect(
|
|
hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override
|
|
)
|
|
async_dispatcher_connect(
|
|
hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override
|
|
)
|
|
async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device)
|
|
async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device)
|
|
_LOGGER.debug("Insteon Services registered")
|
|
|
|
|
|
def print_aldb_to_log(aldb):
|
|
"""Print the All-Link Database to the log file."""
|
|
logger = logging.getLogger(f"{__name__}.links")
|
|
logger.info("%s ALDB load status is %s", aldb.address, aldb.status.name)
|
|
if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]:
|
|
_LOGGER.warning("All-Link database not loaded")
|
|
|
|
logger.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3")
|
|
logger.info("----- ------ ---- --- ----- -------- ------ ------ ------")
|
|
for mem_addr in aldb:
|
|
rec = aldb[mem_addr]
|
|
# For now we write this to the log
|
|
# Roadmap is to create a configuration panel
|
|
in_use = "Y" if rec.is_in_use else "N"
|
|
mode = "C" if rec.is_controller else "R"
|
|
hwm = "Y" if rec.is_high_water_mark else "N"
|
|
log_msg = (
|
|
f" {rec.mem_addr:04x} {in_use:s} {mode:s} {hwm:s} "
|
|
f"{rec.group:3d} {str(rec.target):s} {rec.data1:3d} "
|
|
f"{rec.data2:3d} {rec.data3:3d}"
|
|
)
|
|
logger.info(log_msg)
|
|
|
|
|
|
@callback
|
|
def async_add_insteon_entities(
|
|
hass, platform, entity_type, async_add_entities, discovery_info
|
|
):
|
|
"""Add Insteon devices to a platform."""
|
|
new_entities = []
|
|
device_list = [discovery_info.get("address")] if discovery_info else devices
|
|
|
|
for address in device_list:
|
|
device = devices[address]
|
|
groups = get_platform_groups(device, platform)
|
|
for group in groups:
|
|
new_entities.append(entity_type(device, group))
|
|
if new_entities:
|
|
async_add_entities(new_entities)
|