mirror of https://github.com/home-assistant/core
252 lines
7.9 KiB
Python
252 lines
7.9 KiB
Python
"""Support the Universal Devices ISY/IoX controllers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from urllib.parse import urlparse
|
|
|
|
from aiohttp import CookieJar
|
|
from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError
|
|
from pyisy.constants import CONFIG_NETWORKING, CONFIG_PORTAL
|
|
import voluptuous as vol
|
|
|
|
from homeassistant import config_entries
|
|
from homeassistant.const import (
|
|
CONF_HOST,
|
|
CONF_PASSWORD,
|
|
CONF_USERNAME,
|
|
CONF_VARIABLES,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
Platform,
|
|
)
|
|
from homeassistant.core import Event, HomeAssistant, callback
|
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
|
import homeassistant.helpers.device_registry as dr
|
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
|
|
from .const import (
|
|
_LOGGER,
|
|
CONF_IGNORE_STRING,
|
|
CONF_NETWORK,
|
|
CONF_SENSOR_STRING,
|
|
CONF_TLS_VER,
|
|
DEFAULT_IGNORE_STRING,
|
|
DEFAULT_SENSOR_STRING,
|
|
DOMAIN,
|
|
ISY_CONF_FIRMWARE,
|
|
ISY_CONF_MODEL,
|
|
ISY_CONF_NAME,
|
|
MANUFACTURER,
|
|
PLATFORMS,
|
|
SCHEME_HTTP,
|
|
SCHEME_HTTPS,
|
|
)
|
|
from .helpers import _categorize_nodes, _categorize_programs
|
|
from .models import IsyData
|
|
from .services import async_setup_services, async_unload_services
|
|
from .util import _async_cleanup_registry_entries
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
cv.deprecated(DOMAIN),
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
|
) -> bool:
|
|
"""Set up the ISY 994 integration."""
|
|
hass.data.setdefault(DOMAIN, {})
|
|
isy_data = hass.data[DOMAIN][entry.entry_id] = IsyData()
|
|
|
|
isy_config = entry.data
|
|
isy_options = entry.options
|
|
|
|
# Required
|
|
user = isy_config[CONF_USERNAME]
|
|
password = isy_config[CONF_PASSWORD]
|
|
host = urlparse(isy_config[CONF_HOST])
|
|
|
|
# Optional
|
|
tls_version = isy_config.get(CONF_TLS_VER)
|
|
ignore_identifier = isy_options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING)
|
|
sensor_identifier = isy_options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING)
|
|
|
|
if host.scheme == SCHEME_HTTP:
|
|
https = False
|
|
port = host.port or 80
|
|
session = aiohttp_client.async_create_clientsession(
|
|
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
|
|
)
|
|
elif host.scheme == SCHEME_HTTPS:
|
|
https = True
|
|
port = host.port or 443
|
|
session = aiohttp_client.async_get_clientsession(hass)
|
|
else:
|
|
_LOGGER.error("The ISY/IoX host value in configuration is invalid")
|
|
return False
|
|
|
|
# Connect to ISY controller.
|
|
isy = ISY(
|
|
host.hostname,
|
|
port,
|
|
username=user,
|
|
password=password,
|
|
use_https=https,
|
|
tls_ver=tls_version,
|
|
webroot=host.path,
|
|
websession=session,
|
|
use_websocket=True,
|
|
)
|
|
|
|
try:
|
|
async with asyncio.timeout(60):
|
|
await isy.initialize()
|
|
except TimeoutError as err:
|
|
raise ConfigEntryNotReady(
|
|
"Timed out initializing the ISY; device may be busy, trying again later:"
|
|
f" {err}"
|
|
) from err
|
|
except ISYInvalidAuthError as err:
|
|
raise ConfigEntryAuthFailed(f"Invalid credentials for the ISY: {err}") from err
|
|
except ISYConnectionError as err:
|
|
raise ConfigEntryNotReady(
|
|
f"Failed to connect to the ISY, please adjust settings and try again: {err}"
|
|
) from err
|
|
except ISYResponseParseError as err:
|
|
raise ConfigEntryNotReady(
|
|
"Invalid XML response from ISY; Ensure the ISY is running the latest"
|
|
f" firmware: {err}"
|
|
) from err
|
|
except TypeError as err:
|
|
raise ConfigEntryNotReady(
|
|
f"Invalid response ISY, device is likely still starting: {err}"
|
|
) from err
|
|
|
|
_categorize_nodes(isy_data, isy.nodes, ignore_identifier, sensor_identifier)
|
|
_categorize_programs(isy_data, isy.programs)
|
|
# Gather ISY Variables to be added.
|
|
if isy.variables.children:
|
|
isy_data.devices[CONF_VARIABLES] = _create_service_device_info(
|
|
isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES
|
|
)
|
|
numbers = isy_data.variables[Platform.NUMBER]
|
|
for vtype, _, vid in isy.variables.children:
|
|
numbers.append(isy.variables[vtype][vid])
|
|
if (
|
|
isy.conf[CONFIG_NETWORKING] or isy.conf[CONFIG_PORTAL]
|
|
) and isy.networking.nobjs:
|
|
isy_data.devices[CONF_NETWORK] = _create_service_device_info(
|
|
isy, name=CONFIG_NETWORKING, unique_id=CONF_NETWORK
|
|
)
|
|
for resource in isy.networking.nobjs:
|
|
isy_data.net_resources.append(resource)
|
|
|
|
# Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
|
|
_LOGGER.debug(repr(isy.clock))
|
|
|
|
isy_data.root = isy
|
|
_async_get_or_create_isy_device_in_registry(hass, entry, isy)
|
|
|
|
# Load platforms for the devices in the ISY controller that we support.
|
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
|
|
# Clean-up any old entities that we no longer provide.
|
|
_async_cleanup_registry_entries(hass, entry.entry_id)
|
|
|
|
@callback
|
|
def _async_stop_auto_update(event: Event) -> None:
|
|
"""Stop the isy auto update on Home Assistant Shutdown."""
|
|
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
|
|
isy.websocket.stop()
|
|
|
|
_LOGGER.debug("ISY Starting Event Stream and automatic updates")
|
|
isy.websocket.start()
|
|
|
|
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
|
entry.async_on_unload(
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update)
|
|
)
|
|
|
|
# Register Integration-wide Services:
|
|
async_setup_services(hass)
|
|
|
|
return True
|
|
|
|
|
|
async def _async_update_listener(
|
|
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
|
) -> None:
|
|
"""Handle options update."""
|
|
await hass.config_entries.async_reload(entry.entry_id)
|
|
|
|
|
|
@callback
|
|
def _async_get_or_create_isy_device_in_registry(
|
|
hass: HomeAssistant, entry: config_entries.ConfigEntry, isy: ISY
|
|
) -> None:
|
|
device_registry = dr.async_get(hass)
|
|
device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections={(dr.CONNECTION_NETWORK_MAC, isy.uuid)},
|
|
identifiers={(DOMAIN, isy.uuid)},
|
|
manufacturer=MANUFACTURER,
|
|
name=isy.conf[ISY_CONF_NAME],
|
|
model=isy.conf[ISY_CONF_MODEL],
|
|
sw_version=isy.conf[ISY_CONF_FIRMWARE],
|
|
configuration_url=isy.conn.url,
|
|
)
|
|
|
|
|
|
def _create_service_device_info(isy: ISY, name: str, unique_id: str) -> DeviceInfo:
|
|
"""Create device info for ISY service devices."""
|
|
return DeviceInfo(
|
|
identifiers={
|
|
(
|
|
DOMAIN,
|
|
f"{isy.uuid}_{unique_id}",
|
|
)
|
|
},
|
|
manufacturer=MANUFACTURER,
|
|
name=f"{isy.conf[ISY_CONF_NAME]} {name}",
|
|
model=isy.conf[ISY_CONF_MODEL],
|
|
sw_version=isy.conf[ISY_CONF_FIRMWARE],
|
|
configuration_url=isy.conn.url,
|
|
via_device=(DOMAIN, isy.uuid),
|
|
entry_type=DeviceEntryType.SERVICE,
|
|
)
|
|
|
|
|
|
async def async_unload_entry(
|
|
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
|
) -> bool:
|
|
"""Unload a config entry."""
|
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
|
|
|
isy: ISY = isy_data.root
|
|
|
|
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
|
|
isy.websocket.stop()
|
|
|
|
if unload_ok:
|
|
hass.data[DOMAIN].pop(entry.entry_id)
|
|
|
|
async_unload_services(hass)
|
|
|
|
return unload_ok
|
|
|
|
|
|
async def async_remove_config_entry_device(
|
|
hass: HomeAssistant,
|
|
config_entry: config_entries.ConfigEntry,
|
|
device_entry: dr.DeviceEntry,
|
|
) -> bool:
|
|
"""Remove ISY config entry from a device."""
|
|
isy_data = hass.data[DOMAIN][config_entry.entry_id]
|
|
return not device_entry.identifiers.intersection(
|
|
(DOMAIN, unique_id) for unique_id in isy_data.devices
|
|
)
|