Skip to content

Commit

Permalink
Add config flow, use async loading, and restore brightness option to …
Browse files Browse the repository at this point in the history
…ISY994 (home-assistant#35413)

* ISY994 Add Config and Options Flow


Add tests for config flow


Fix test


Update Tests

* Fix merge errors

* Update homeassistant/components/isy994/strings.json

Co-authored-by: J. Nick Koston <nick@koston.org>

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* Fix patching in tests to not actually start Home Assistant

Co-authored-by: J. Nick Koston <nick@koston.org>
  • Loading branch information
shbatm and bdraco authored May 9, 2020
1 parent d61bde6 commit d7f736e
Show file tree
Hide file tree
Showing 19 changed files with 847 additions and 149 deletions.
199 changes: 162 additions & 37 deletions homeassistant/components/isy994/__init__.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
"""Support the ISY-994 controllers."""
import asyncio
from functools import partial
from typing import Optional
from urllib.parse import urlparse

from pyisy import ISY
import voluptuous as vol

from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType

from .const import (
_LOGGER,
CONF_IGNORE_STRING,
CONF_RESTORE_LIGHT_STATE,
CONF_SENSOR_STRING,
CONF_TLS_VER,
DEFAULT_IGNORE_STRING,
DEFAULT_RESTORE_LIGHT_STATE,
DEFAULT_SENSOR_STRING,
DOMAIN,
ISY994_ISY,
ISY994_NODES,
ISY994_PROGRAMS,
SUPPORTED_PLATFORMS,
SUPPORTED_PROGRAM_PLATFORMS,
UNDO_UPDATE_LISTENER,
)
from .helpers import _categorize_nodes, _categorize_programs

Expand All @@ -43,31 +46,81 @@
vol.Optional(
CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING
): cv.string,
vol.Required(
CONF_RESTORE_LIGHT_STATE, default=DEFAULT_RESTORE_LIGHT_STATE
): bool,
}
)
},
extra=vol.ALLOW_EXTRA,
)


def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the ISY 994 platform."""
hass.data[ISY994_NODES] = {}
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the isy994 integration from YAML."""
isy_config: Optional[ConfigType] = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})

if not isy_config:
return True

# Only import if we haven't before.
config_entry = _async_find_matching_config_entry(hass)
if not config_entry:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=dict(isy_config),
)
)
return True

# Update the entry based on the YAML configuration, in case it changed.
hass.config_entries.async_update_entry(config_entry, data=dict(isy_config))
return True


@callback
def _async_find_matching_config_entry(hass):
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.source == config_entries.SOURCE_IMPORT:
return entry


async def async_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Set up the ISY 994 integration."""
# As there currently is no way to import options from yaml
# when setting up a config entry, we fallback to adding
# the options to the config entry and pull them out here if
# they are missing from the options
_async_import_options_from_data_if_missing(hass, entry)

hass.data[DOMAIN][entry.entry_id] = {}
hass_isy_data = hass.data[DOMAIN][entry.entry_id]

hass_isy_data[ISY994_NODES] = {}
for platform in SUPPORTED_PLATFORMS:
hass.data[ISY994_NODES][platform] = []
hass_isy_data[ISY994_NODES][platform] = []

hass.data[ISY994_PROGRAMS] = {}
hass_isy_data[ISY994_PROGRAMS] = {}
for platform in SUPPORTED_PROGRAM_PLATFORMS:
hass.data[ISY994_PROGRAMS][platform] = []
hass_isy_data[ISY994_PROGRAMS][platform] = []

isy_config = config.get(DOMAIN)
isy_config = entry.data
isy_options = entry.options

user = isy_config.get(CONF_USERNAME)
password = isy_config.get(CONF_PASSWORD)
# 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)
host = urlparse(isy_config.get(CONF_HOST))
ignore_identifier = isy_config.get(CONF_IGNORE_STRING)
sensor_identifier = isy_config.get(CONF_SENSOR_STRING)
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 == "http":
https = False
Expand All @@ -80,31 +133,103 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
return False

# Connect to ISY controller.
isy = ISY(
host.hostname,
port,
username=user,
password=password,
use_https=https,
tls_ver=tls_version,
log=_LOGGER,
isy = await hass.async_add_executor_job(
partial(
ISY,
host.hostname,
port,
username=user,
password=password,
use_https=https,
tls_ver=tls_version,
log=_LOGGER,
webroot=host.path,
)
)
if not isy.connected:
return False

_categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier)
_categorize_programs(hass, isy.programs)
_categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier)
_categorize_programs(hass_isy_data, isy.programs)

def stop(event: object) -> None:
"""Stop ISY auto updates."""
isy.auto_update = False
# Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
_LOGGER.info(repr(isy.clock))

# Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
hass_isy_data[ISY994_ISY] = isy

# Load platforms for the devices in the ISY controller that we support.
for platform in SUPPORTED_PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

def _start_auto_update() -> None:
"""Start isy auto update."""
_LOGGER.debug("ISY Starting Event Stream and automatic updates.")
isy.auto_update = True

await hass.async_add_executor_job(_start_auto_update)

undo_listener = entry.add_update_listener(_async_update_listener)

hass_isy_data[UNDO_UPDATE_LISTENER] = undo_listener

isy.auto_update = True
return True


async def _async_update_listener(
hass: HomeAssistant, entry: config_entries.ConfigEntry
):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)


@callback
def _async_import_options_from_data_if_missing(
hass: HomeAssistant, entry: config_entries.ConfigEntry
):
options = dict(entry.options)
modified = False
for importable_option in [
CONF_IGNORE_STRING,
CONF_SENSOR_STRING,
CONF_RESTORE_LIGHT_STATE,
]:
if importable_option not in entry.options and importable_option in entry.data:
options[importable_option] = entry.data[importable_option]
modified = True

if modified:
hass.config_entries.async_update_entry(entry, options=options)


async def async_unload_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in SUPPORTED_PLATFORMS
]
)
)

hass_isy_data = hass.data[DOMAIN][entry.entry_id]

isy = hass_isy_data[ISY994_ISY]

def _stop_auto_update() -> None:
"""Start isy auto update."""
_LOGGER.debug("ISY Stopping Event Stream and automatic updates.")
isy.auto_update = False

await hass.async_add_executor_job(_stop_auto_update)

hass_isy_data[UNDO_UPDATE_LISTENER]()

if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
26 changes: 17 additions & 9 deletions homeassistant/components/isy994/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,24 @@
DOMAIN as BINARY_SENSOR,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util

from . import ISY994_NODES, ISY994_PROGRAMS
from .const import (
_LOGGER,
BINARY_SENSOR_DEVICE_TYPES_ISY,
BINARY_SENSOR_DEVICE_TYPES_ZWAVE,
DOMAIN as ISY994_DOMAIN,
ISY994_NODES,
ISY994_PROGRAMS,
TYPE_CATEGORY_CLIMATE,
)
from .entity import ISYNodeEntity, ISYProgramEntity
from .helpers import migrate_old_unique_ids

DEVICE_PARENT_REQUIRED = [
DEVICE_CLASS_OPENING,
Expand All @@ -56,15 +60,18 @@
TYPE_INSTEON_MOTION = ("16.1.", "16.22.")


def setup_platform(
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
):
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
async_add_entities: Callable[[list], None],
) -> bool:
"""Set up the ISY994 binary sensor platform."""
devices = []
devices_by_address = {}
child_nodes = []

for node in hass.data[ISY994_NODES][BINARY_SENSOR]:
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
for node in hass_isy_data[ISY994_NODES][BINARY_SENSOR]:
device_class, device_type = _detect_device_type_and_class(node)
if node.protocol == PROTO_INSTEON:
if node.parent_node is not None:
Expand Down Expand Up @@ -162,10 +169,11 @@ def setup_platform(
device = ISYBinarySensorEntity(node, device_class)
devices.append(device)

for name, status, _ in hass.data[ISY994_PROGRAMS][BINARY_SENSOR]:
for name, status, _ in hass_isy_data[ISY994_PROGRAMS][BINARY_SENSOR]:
devices.append(ISYBinarySensorProgramEntity(name, status))

add_entities(devices)
await migrate_old_unique_ids(hass, BINARY_SENSOR, devices)
async_add_entities(devices)


def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str):
Expand Down Expand Up @@ -235,7 +243,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
Often times, a single device is represented by multiple nodes in the ISY,
allowing for different nuances in how those devices report their on and
off events. This class turns those multiple nodes in to a single Home
off events. This class turns those multiple nodes into a single Home
Assistant entity and handles both ways that ISY binary sensors can work.
"""

Expand Down
Loading

0 comments on commit d7f736e

Please sign in to comment.