"""API calls to manage Insteon configuration changes.""" from __future__ import annotations from typing import Any, TypedDict from pyinsteon import async_close, async_connect, devices from pyinsteon.address import Address import voluptuous as vol import voluptuous_serialize from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DEVICE from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( CONF_HOUSECODE, CONF_OVERRIDE, CONF_UNITCODE, CONF_X10, DEVICE_ADDRESS, DOMAIN, ID, SIGNAL_ADD_DEVICE_OVERRIDE, SIGNAL_ADD_X10_DEVICE, SIGNAL_REMOVE_DEVICE_OVERRIDE, TYPE, ) from ..schemas import ( build_device_override_schema, build_hub_schema, build_plm_manual_schema, build_plm_schema, ) from ..utils import async_get_usb_ports HUB_V1_SCHEMA = build_hub_schema(hub_version=1) HUB_V2_SCHEMA = build_hub_schema(hub_version=2) PLM_SCHEMA = build_plm_manual_schema() DEVICE_OVERRIDE_SCHEMA = build_device_override_schema() OVERRIDE = "override" class X10DeviceConfig(TypedDict): """X10 Device Configuration Definition.""" housecode: str unitcode: int platform: str dim_steps: int class DeviceOverride(TypedDict): """X10 Device Configuration Definition.""" address: Address | str cat: int subcat: str def get_insteon_config_entry(hass: HomeAssistant) -> ConfigEntry: """Return the Insteon configuration entry.""" return hass.config_entries.async_entries(DOMAIN)[0] def add_x10_device(hass: HomeAssistant, x10_device: X10DeviceConfig): """Add an X10 device to the Insteon integration.""" config_entry = get_insteon_config_entry(hass) x10_config = config_entry.options.get(CONF_X10, []) if any( device[CONF_HOUSECODE] == x10_device["housecode"] and device[CONF_UNITCODE] == x10_device["unitcode"] for device in x10_config ): raise ValueError("Duplicate X10 device") hass.config_entries.async_update_entry( entry=config_entry, options=config_entry.options | {CONF_X10: [*x10_config, x10_device]}, ) async_dispatcher_send(hass, SIGNAL_ADD_X10_DEVICE, x10_device) def remove_x10_device(hass: HomeAssistant, housecode: str, unitcode: int): """Remove an X10 device from the config.""" config_entry = get_insteon_config_entry(hass) new_options = {**config_entry.options} new_x10 = [ existing_device for existing_device in config_entry.options.get(CONF_X10, []) if existing_device[CONF_HOUSECODE].lower() != housecode.lower() or existing_device[CONF_UNITCODE] != unitcode ] new_options[CONF_X10] = new_x10 hass.config_entries.async_update_entry(entry=config_entry, options=new_options) def add_device_overide(hass: HomeAssistant, override: DeviceOverride): """Add an Insteon device override.""" config_entry = get_insteon_config_entry(hass) override_config = config_entry.options.get(CONF_OVERRIDE, []) address = Address(override[CONF_ADDRESS]) if any( Address(existing_override[CONF_ADDRESS]) == address for existing_override in override_config ): raise ValueError("Duplicate override") hass.config_entries.async_update_entry( entry=config_entry, options=config_entry.options | {CONF_OVERRIDE: [*override_config, override]}, ) async_dispatcher_send(hass, SIGNAL_ADD_DEVICE_OVERRIDE, override) def remove_device_override(hass: HomeAssistant, address: Address): """Remove a device override from config.""" config_entry = get_insteon_config_entry(hass) new_options = {**config_entry.options} new_overrides = [ existing_override for existing_override in config_entry.options.get(CONF_OVERRIDE, []) if Address(existing_override[CONF_ADDRESS]) != address ] new_options[CONF_OVERRIDE] = new_overrides hass.config_entries.async_update_entry(entry=config_entry, options=new_options) async def _async_connect(**kwargs): """Connect to the Insteon modem.""" if devices.modem: await async_close() try: await async_connect(**kwargs) except ConnectionError: return False return True @websocket_api.websocket_command({vol.Required(TYPE): "insteon/config/get"}) @websocket_api.require_admin @websocket_api.async_response async def websocket_get_config( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Get Insteon configuration.""" config_entry = get_insteon_config_entry(hass) modem_config = config_entry.data options_config = config_entry.options x10_config = options_config.get(CONF_X10) override_config = options_config.get(CONF_OVERRIDE) connection.send_result( msg[ID], { "modem_config": {**modem_config}, "x10_config": x10_config, "override_config": override_config, }, ) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/config/get_modem_schema", } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_get_modem_schema( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Get the schema for the modem configuration.""" config_entry = get_insteon_config_entry(hass) config_data = config_entry.data if device := config_data.get(CONF_DEVICE): ports = await async_get_usb_ports(hass=hass) plm_schema = voluptuous_serialize.convert( build_plm_schema(ports=ports, device=device) ) connection.send_result(msg[ID], plm_schema) else: hub_schema = voluptuous_serialize.convert(build_hub_schema(**config_data)) connection.send_result(msg[ID], hub_schema) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/config/update_modem_config", vol.Required("config"): vol.Any(PLM_SCHEMA, HUB_V2_SCHEMA, HUB_V1_SCHEMA), } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_update_modem_config( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Get the schema for the modem configuration.""" config = msg["config"] config_entry = get_insteon_config_entry(hass) is_connected = devices.modem.connected if not await _async_connect(**config): connection.send_error( msg_id=msg[ID], code="connection_failed", message="Connection failed" ) # Try to reconnect using old info if is_connected: await _async_connect(**config_entry.data) return hass.config_entries.async_update_entry( entry=config_entry, data=config, ) connection.send_result(msg[ID], {"status": "success"}) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/config/device_override/add", vol.Required(OVERRIDE): DEVICE_OVERRIDE_SCHEMA, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_add_device_override( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Get the schema for the modem configuration.""" override = msg[OVERRIDE] try: add_device_overide(hass, override) except ValueError: connection.send_error(msg[ID], "duplicate", "Duplicate device address") connection.send_result(msg[ID]) @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/config/device_override/remove", vol.Required(DEVICE_ADDRESS): str, } ) @websocket_api.require_admin @websocket_api.async_response async def websocket_remove_device_override( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], ) -> None: """Get the schema for the modem configuration.""" address = Address(msg[DEVICE_ADDRESS]) remove_device_override(hass, address) async_dispatcher_send(hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, address) connection.send_result(msg[ID])