core/homeassistant/components/isy994/__init__.py

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
)