"""Web socket API for Insteon devices.""" from typing import Any from pyinsteon import devices from pyinsteon.constants import ALDBStatus from pyinsteon.topics import ( ALDB_STATUS_CHANGED, DEVICE_LINK_CONTROLLER_CREATED, DEVICE_LINK_RESPONDER_CREATED, ) from pyinsteon.utils import subscribe_topic, unsubscribe_topic import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from ..const import DEVICE_ADDRESS, ID, INSTEON_DEVICE_NOT_FOUND, TYPE from .device import async_device_name, notify_device_not_found ALDB_RECORD = "record" ALDB_RECORD_SCHEMA = vol.Schema( { vol.Required("mem_addr"): int, vol.Required("in_use"): bool, vol.Required("group"): vol.Range(0, 255), vol.Required("is_controller"): bool, vol.Optional("highwater"): bool, vol.Required("target"): str, vol.Optional("target_name"): str, vol.Required("data1"): vol.Range(0, 255), vol.Required("data2"): vol.Range(0, 255), vol.Required("data3"): vol.Range(0, 255), vol.Optional("dirty"): bool, } ) async def async_aldb_record_to_dict(dev_registry, record, dirty=False): """Convert an ALDB record to a dict.""" return ALDB_RECORD_SCHEMA( { "mem_addr": record.mem_addr, "in_use": record.is_in_use, "is_controller": record.is_controller, "highwater": record.is_high_water_mark, "group": record.group, "target": str(record.target), "target_name": await async_device_name(dev_registry, record.target), "data1": record.data1, "data2": record.data2, "data3": record.data3, "dirty": dirty, } ) async def async_reload_and_save_aldb(hass, device): """Add default links to an Insteon device.""" if device == devices.modem: await device.aldb.async_load() else: await device.aldb.async_load(refresh=True) await devices.async_save(workdir=hass.config.config_dir) @websocket_api.websocket_command( {vol.Required(TYPE): "insteon/aldb/get", vol.Required(DEVICE_ADDRESS): str} ) @websocket_api.require_admin @websocket_api.async_response async def websocket_get_aldb( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Get the All-Link Database for an Insteon device.""" if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return # Convert the ALDB to a dict merge in pending changes aldb = {mem_addr: device.aldb[mem_addr] for mem_addr in device.aldb} aldb.update(device.aldb.pending_changes) changed_records = list(device.aldb.pending_changes.keys()) dev_registry = dr.async_get(hass) records = [ await async_aldb_record_to_dict( dev_registry, aldb[mem_addr], mem_addr in changed_records ) for mem_addr in aldb ] connection.send_result(msg[ID], records) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/aldb/change", vol.Required(DEVICE_ADDRESS): str, vol.Required(ALDB_RECORD): ALDB_RECORD_SCHEMA, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_change_aldb_record( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Change an All-Link Database record for an Insteon device.""" if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return record = msg[ALDB_RECORD] device.aldb.modify( mem_addr=record["mem_addr"], in_use=record["in_use"], group=record["group"], controller=record["is_controller"], target=record["target"], data1=record["data1"], data2=record["data2"], data3=record["data3"], ) connection.send_result(msg[ID]) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/aldb/create", vol.Required(DEVICE_ADDRESS): str, vol.Required(ALDB_RECORD): ALDB_RECORD_SCHEMA, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_create_aldb_record( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Create an All-Link Database record for an Insteon device.""" if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return record = msg[ALDB_RECORD] device.aldb.add( group=record["group"], controller=record["is_controller"], target=record["target"], data1=record["data1"], data2=record["data2"], data3=record["data3"], ) connection.send_result(msg[ID]) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/aldb/write", vol.Required(DEVICE_ADDRESS): str, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_write_aldb( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Create an All-Link Database record for an Insteon device.""" if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return await device.aldb.async_write() hass.async_create_task(async_reload_and_save_aldb(hass, device)) connection.send_result(msg[ID]) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/aldb/load", vol.Required(DEVICE_ADDRESS): str, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_load_aldb( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Create an All-Link Database record for an Insteon device.""" if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return hass.async_create_task(async_reload_and_save_aldb(hass, device)) connection.send_result(msg[ID]) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/aldb/reset", vol.Required(DEVICE_ADDRESS): str, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_reset_aldb( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Create an All-Link Database record for an Insteon device.""" if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return device.aldb.clear_pending() connection.send_result(msg[ID]) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/aldb/add_default_links", vol.Required(DEVICE_ADDRESS): str, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_add_default_links( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Add the default All-Link Database records for an Insteon device.""" if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return device.aldb.clear_pending() await device.async_add_default_links() hass.async_create_task(async_reload_and_save_aldb(hass, device)) connection.send_result(msg[ID]) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/aldb/notify", vol.Required(DEVICE_ADDRESS): str, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_notify_on_aldb_status( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Tell Insteon a new ALDB record was added.""" if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @callback def record_added(controller, responder, group): """Forward ALDB events to websocket.""" forward_data = {"type": "record_loaded"} connection.send_message(websocket_api.event_message(msg["id"], forward_data)) @callback def aldb_loaded(): """Forward ALDB loaded event to websocket.""" forward_data = { "type": "status_changed", "is_loading": device.aldb.status == ALDBStatus.LOADING, } connection.send_message(websocket_api.event_message(msg["id"], forward_data)) @callback def async_cleanup() -> None: """Remove signal listeners.""" unsubscribe_topic(record_added, f"{DEVICE_LINK_CONTROLLER_CREATED}.{device.id}") unsubscribe_topic(record_added, f"{DEVICE_LINK_RESPONDER_CREATED}.{device.id}") unsubscribe_topic(aldb_loaded, f"{device.id}.{ALDB_STATUS_CHANGED}") forward_data = {"type": "unsubscribed"} connection.send_message(websocket_api.event_message(msg["id"], forward_data)) connection.subscriptions[msg["id"]] = async_cleanup subscribe_topic(record_added, f"{DEVICE_LINK_CONTROLLER_CREATED}.{device.id}") subscribe_topic(record_added, f"{DEVICE_LINK_RESPONDER_CREATED}.{device.id}") subscribe_topic(aldb_loaded, f"{device.id}.{ALDB_STATUS_CHANGED}") connection.send_result(msg[ID])