Home Assistant Unofficial Reference 2024.12.1
config.py
Go to the documentation of this file.
1 """API calls to manage Insteon configuration changes."""
2 
3 from __future__ import annotations
4 
5 from typing import Any, TypedDict
6 
7 from pyinsteon import async_close, async_connect, devices
8 from pyinsteon.address import Address
9 from pyinsteon.aldb.aldb_record import ALDBRecord
10 from pyinsteon.constants import LinkStatus
11 from pyinsteon.managers.link_manager import get_broken_links
12 import voluptuous as vol
13 import voluptuous_serialize
14 
15 from homeassistant.components import websocket_api
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import CONF_ADDRESS, CONF_DEVICE
18 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers import device_registry as dr
20 from homeassistant.helpers.dispatcher import async_dispatcher_send
21 
22 from ..const import (
23  CONF_HOUSECODE,
24  CONF_OVERRIDE,
25  CONF_UNITCODE,
26  CONF_X10,
27  DEVICE_ADDRESS,
28  DOMAIN,
29  ID,
30  SIGNAL_ADD_DEVICE_OVERRIDE,
31  SIGNAL_ADD_X10_DEVICE,
32  SIGNAL_REMOVE_DEVICE_OVERRIDE,
33  TYPE,
34 )
35 from ..schemas import (
36  build_device_override_schema,
37  build_hub_schema,
38  build_plm_manual_schema,
39  build_plm_schema,
40 )
41 from ..utils import async_device_name, async_get_usb_ports
42 
43 HUB_V1_SCHEMA = build_hub_schema(hub_version=1)
44 HUB_V2_SCHEMA = build_hub_schema(hub_version=2)
46 DEVICE_OVERRIDE_SCHEMA = build_device_override_schema()
47 OVERRIDE = "override"
48 
49 
50 class X10DeviceConfig(TypedDict):
51  """X10 Device Configuration Definition."""
52 
53  housecode: str
54  unitcode: int
55  platform: str
56  dim_steps: int
57 
58 
59 class DeviceOverride(TypedDict):
60  """X10 Device Configuration Definition."""
61 
62  address: Address | str
63  cat: int
64  subcat: str
65 
66 
67 def get_insteon_config_entry(hass: HomeAssistant) -> ConfigEntry:
68  """Return the Insteon configuration entry."""
69  return hass.config_entries.async_entries(DOMAIN)[0]
70 
71 
72 def add_x10_device(hass: HomeAssistant, x10_device: X10DeviceConfig):
73  """Add an X10 device to the Insteon integration."""
74 
75  config_entry = get_insteon_config_entry(hass)
76  x10_config = config_entry.options.get(CONF_X10, [])
77  if any(
78  device[CONF_HOUSECODE] == x10_device["housecode"]
79  and device[CONF_UNITCODE] == x10_device["unitcode"]
80  for device in x10_config
81  ):
82  raise ValueError("Duplicate X10 device")
83 
84  hass.config_entries.async_update_entry(
85  entry=config_entry,
86  options=config_entry.options | {CONF_X10: [*x10_config, x10_device]},
87  )
88  async_dispatcher_send(hass, SIGNAL_ADD_X10_DEVICE, x10_device)
89 
90 
91 def remove_x10_device(hass: HomeAssistant, housecode: str, unitcode: int):
92  """Remove an X10 device from the config."""
93 
94  config_entry = get_insteon_config_entry(hass)
95  new_options = {**config_entry.options}
96  new_x10 = [
97  existing_device
98  for existing_device in config_entry.options.get(CONF_X10, [])
99  if existing_device[CONF_HOUSECODE].lower() != housecode.lower()
100  or existing_device[CONF_UNITCODE] != unitcode
101  ]
102 
103  new_options[CONF_X10] = new_x10
104  hass.config_entries.async_update_entry(entry=config_entry, options=new_options)
105 
106 
107 def add_device_overide(hass: HomeAssistant, override: DeviceOverride):
108  """Add an Insteon device override."""
109 
110  config_entry = get_insteon_config_entry(hass)
111  override_config = config_entry.options.get(CONF_OVERRIDE, [])
112  address = Address(override[CONF_ADDRESS])
113  if any(
114  Address(existing_override[CONF_ADDRESS]) == address
115  for existing_override in override_config
116  ):
117  raise ValueError("Duplicate override")
118 
119  hass.config_entries.async_update_entry(
120  entry=config_entry,
121  options=config_entry.options | {CONF_OVERRIDE: [*override_config, override]},
122  )
123  async_dispatcher_send(hass, SIGNAL_ADD_DEVICE_OVERRIDE, override)
124 
125 
126 def remove_device_override(hass: HomeAssistant, address: Address):
127  """Remove a device override from config."""
128 
129  config_entry = get_insteon_config_entry(hass)
130  new_options = {**config_entry.options}
131 
132  new_overrides = [
133  existing_override
134  for existing_override in config_entry.options.get(CONF_OVERRIDE, [])
135  if Address(existing_override[CONF_ADDRESS]) != address
136  ]
137  new_options[CONF_OVERRIDE] = new_overrides
138  hass.config_entries.async_update_entry(entry=config_entry, options=new_options)
139 
140 
142  address: Address, record: ALDBRecord, dev_registry: dr.DeviceRegistry, status=None
143 ) -> dict[str, str | int]:
144  """Convert a link to a dictionary."""
145  link_dict: dict[str, str | int] = {}
146  device_name = await async_device_name(dev_registry, address)
147  target_name = await async_device_name(dev_registry, record.target)
148  link_dict["address"] = str(address)
149  link_dict["device_name"] = device_name if device_name else str(address)
150  link_dict["mem_addr"] = record.mem_addr
151  link_dict["in_use"] = record.is_in_use
152  link_dict["group"] = record.group
153  link_dict["is_controller"] = record.is_controller
154  link_dict["highwater"] = record.is_high_water_mark
155  link_dict["target"] = str(record.target)
156  link_dict["target_name"] = target_name if target_name else str(record.target)
157  link_dict["data1"] = record.data1
158  link_dict["data2"] = record.data2
159  link_dict["data3"] = record.data3
160  if status:
161  link_dict["status"] = status.name.lower()
162  return link_dict
163 
164 
165 async def _async_connect(**kwargs):
166  """Connect to the Insteon modem."""
167  if devices.modem:
168  await async_close()
169  try:
170  await async_connect(**kwargs)
171  except ConnectionError:
172  return False
173  return True
174 
175 
176 @websocket_api.websocket_command({vol.Required(TYPE): "insteon/config/get"})
177 @websocket_api.require_admin
178 @websocket_api.async_response
180  hass: HomeAssistant,
182  msg: dict[str, Any],
183 ) -> None:
184  """Get Insteon configuration."""
185  config_entry = get_insteon_config_entry(hass)
186  modem_config = config_entry.data
187  options_config = config_entry.options
188  x10_config = options_config.get(CONF_X10)
189  override_config = options_config.get(CONF_OVERRIDE)
190  connection.send_result(
191  msg[ID],
192  {
193  "modem_config": {**modem_config},
194  "x10_config": x10_config,
195  "override_config": override_config,
196  },
197  )
198 
199 
200 @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/config/get_modem_schema",
201  }
202 )
203 @websocket_api.require_admin
204 @websocket_api.async_response
206  hass: HomeAssistant,
208  msg: dict[str, Any],
209 ) -> None:
210  """Get the schema for the modem configuration."""
211  config_entry = get_insteon_config_entry(hass)
212  config_data = config_entry.data
213  if device := config_data.get(CONF_DEVICE):
214  ports = await async_get_usb_ports(hass=hass)
215  plm_schema = voluptuous_serialize.convert(
216  build_plm_schema(ports=ports, device=device)
217  )
218  connection.send_result(msg[ID], plm_schema)
219  else:
220  hub_schema = voluptuous_serialize.convert(build_hub_schema(**config_data))
221  connection.send_result(msg[ID], hub_schema)
222 
223 
224 @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/config/update_modem_config",
225  vol.Required("config"): vol.Any(PLM_SCHEMA, HUB_V2_SCHEMA, HUB_V1_SCHEMA),
226  }
227 )
228 @websocket_api.require_admin
229 @websocket_api.async_response
231  hass: HomeAssistant,
233  msg: dict[str, Any],
234 ) -> None:
235  """Get the schema for the modem configuration."""
236  config = msg["config"]
237  config_entry = get_insteon_config_entry(hass)
238  is_connected = devices.modem is not None and devices.modem.connected
239 
240  if not await _async_connect(**config):
241  connection.send_error(
242  msg_id=msg[ID], code="connection_failed", message="Connection failed"
243  )
244  # Try to reconnect using old info
245  if is_connected:
246  await _async_connect(**config_entry.data)
247  return
248 
249  hass.config_entries.async_update_entry(
250  entry=config_entry,
251  data=config,
252  )
253  connection.send_result(msg[ID], {"status": "success"})
254 
255 
256 @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/config/device_override/add",
257  vol.Required(OVERRIDE): DEVICE_OVERRIDE_SCHEMA,
258  }
259 )
260 @websocket_api.require_admin
261 @websocket_api.async_response
263  hass: HomeAssistant,
265  msg: dict[str, Any],
266 ) -> None:
267  """Get the schema for the modem configuration."""
268  override = msg[OVERRIDE]
269  try:
270  add_device_overide(hass, override)
271  except ValueError:
272  connection.send_error(msg[ID], "duplicate", "Duplicate device address")
273 
274  connection.send_result(msg[ID])
275 
276 
277 @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/config/device_override/remove",
278  vol.Required(DEVICE_ADDRESS): str,
279  }
280 )
281 @websocket_api.require_admin
282 @websocket_api.async_response
284  hass: HomeAssistant,
286  msg: dict[str, Any],
287 ) -> None:
288  """Get the schema for the modem configuration."""
289  address = Address(msg[DEVICE_ADDRESS])
290  remove_device_override(hass, address)
291  async_dispatcher_send(hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, address)
292  connection.send_result(msg[ID])
293 
294 
295 @websocket_api.websocket_command( {vol.Required(TYPE): "insteon/config/get_broken_links"}
296 )
297 @websocket_api.require_admin
298 @websocket_api.async_response
300  hass: HomeAssistant,
302  msg: dict[str, Any],
303 ) -> None:
304  """Get any broken links between devices."""
305  broken_links = get_broken_links(devices=devices)
306  dev_registry = dr.async_get(hass)
307  broken_links_list = [
308  await async_link_to_dict(address, record, dev_registry, status)
309  for address, record, status in broken_links
310  if status != LinkStatus.MISSING_TARGET
311  ]
312  connection.send_result(msg[ID], broken_links_list)
313 
314 
315 @websocket_api.websocket_command( {vol.Required(TYPE): "insteon/config/get_unknown_devices"}
316 )
317 @websocket_api.require_admin
318 @websocket_api.async_response
320  hass: HomeAssistant,
322  msg: dict[str, Any],
323 ) -> None:
324  """Get any broken links between devices."""
325  broken_links = get_broken_links(devices=devices)
326  unknown_devices = {
327  str(record.target)
328  for _, record, status in broken_links
329  if status == LinkStatus.MISSING_TARGET
330  }
331  connection.send_result(msg[ID], unknown_devices)
332 
None websocket_get_modem_schema(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
Definition: config.py:211
dict[str, str|int] async_link_to_dict(Address address, ALDBRecord record, dr.DeviceRegistry dev_registry, status=None)
Definition: config.py:143
None websocket_update_modem_config(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
Definition: config.py:238
def remove_x10_device(HomeAssistant hass, str housecode, int unitcode)
Definition: config.py:91
def remove_device_override(HomeAssistant hass, Address address)
Definition: config.py:126
None websocket_remove_device_override(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
Definition: config.py:295
None websocket_get_broken_links(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
Definition: config.py:312
ConfigEntry get_insteon_config_entry(HomeAssistant hass)
Definition: config.py:67
def add_x10_device(HomeAssistant hass, X10DeviceConfig x10_device)
Definition: config.py:72
def add_device_overide(HomeAssistant hass, DeviceOverride override)
Definition: config.py:107
None websocket_get_config(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
Definition: config.py:183
None websocket_get_unknown_devices(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
Definition: config.py:333
None websocket_add_device_override(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
Definition: config.py:272
def build_hub_schema(hub_version, host=vol.UNDEFINED, port=vol.UNDEFINED, username=vol.UNDEFINED, password=vol.UNDEFINED)
Definition: schemas.py:140
def build_plm_manual_schema(device=vol.UNDEFINED)
Definition: schemas.py:129
def build_device_override_schema(address=vol.UNDEFINED, cat=vol.UNDEFINED, subcat=vol.UNDEFINED, firmware=vol.UNDEFINED)
Definition: schemas.py:80
def build_plm_schema(dict[str, str] ports, device=vol.UNDEFINED)
Definition: schemas.py:122
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_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193