mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
Add config flow, use async loading, and restore brightness option to ISY994 (#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>
This commit is contained in:
parent
d61bde6ae6
commit
d7f736ed6c
@ -1,31 +1,34 @@
|
|||||||
"""Support the ISY-994 controllers."""
|
"""Support the ISY-994 controllers."""
|
||||||
|
import asyncio
|
||||||
|
from functools import partial
|
||||||
|
from typing import Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from pyisy import ISY
|
from pyisy import ISY
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant import config_entries
|
||||||
CONF_HOST,
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
CONF_PASSWORD,
|
from homeassistant.core import HomeAssistant, callback
|
||||||
CONF_USERNAME,
|
from homeassistant.helpers import config_validation as cv
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers import config_validation as cv, discovery
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
CONF_IGNORE_STRING,
|
CONF_IGNORE_STRING,
|
||||||
|
CONF_RESTORE_LIGHT_STATE,
|
||||||
CONF_SENSOR_STRING,
|
CONF_SENSOR_STRING,
|
||||||
CONF_TLS_VER,
|
CONF_TLS_VER,
|
||||||
DEFAULT_IGNORE_STRING,
|
DEFAULT_IGNORE_STRING,
|
||||||
|
DEFAULT_RESTORE_LIGHT_STATE,
|
||||||
DEFAULT_SENSOR_STRING,
|
DEFAULT_SENSOR_STRING,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
ISY994_ISY,
|
||||||
ISY994_NODES,
|
ISY994_NODES,
|
||||||
ISY994_PROGRAMS,
|
ISY994_PROGRAMS,
|
||||||
SUPPORTED_PLATFORMS,
|
SUPPORTED_PLATFORMS,
|
||||||
SUPPORTED_PROGRAM_PLATFORMS,
|
SUPPORTED_PROGRAM_PLATFORMS,
|
||||||
|
UNDO_UPDATE_LISTENER,
|
||||||
)
|
)
|
||||||
from .helpers import _categorize_nodes, _categorize_programs
|
from .helpers import _categorize_nodes, _categorize_programs
|
||||||
|
|
||||||
@ -43,6 +46,9 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING
|
CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING
|
||||||
): cv.string,
|
): cv.string,
|
||||||
|
vol.Required(
|
||||||
|
CONF_RESTORE_LIGHT_STATE, default=DEFAULT_RESTORE_LIGHT_STATE
|
||||||
|
): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -50,24 +56,71 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the ISY 994 platform."""
|
"""Set up the isy994 integration from YAML."""
|
||||||
hass.data[ISY994_NODES] = {}
|
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:
|
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:
|
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)
|
# Required
|
||||||
password = isy_config.get(CONF_PASSWORD)
|
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)
|
tls_version = isy_config.get(CONF_TLS_VER)
|
||||||
host = urlparse(isy_config.get(CONF_HOST))
|
ignore_identifier = isy_options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING)
|
||||||
ignore_identifier = isy_config.get(CONF_IGNORE_STRING)
|
sensor_identifier = isy_options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING)
|
||||||
sensor_identifier = isy_config.get(CONF_SENSOR_STRING)
|
|
||||||
|
|
||||||
if host.scheme == "http":
|
if host.scheme == "http":
|
||||||
https = False
|
https = False
|
||||||
@ -80,7 +133,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Connect to ISY controller.
|
# Connect to ISY controller.
|
||||||
isy = ISY(
|
isy = await hass.async_add_executor_job(
|
||||||
|
partial(
|
||||||
|
ISY,
|
||||||
host.hostname,
|
host.hostname,
|
||||||
port,
|
port,
|
||||||
username=user,
|
username=user,
|
||||||
@ -88,23 +143,93 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
use_https=https,
|
use_https=https,
|
||||||
tls_ver=tls_version,
|
tls_ver=tls_version,
|
||||||
log=_LOGGER,
|
log=_LOGGER,
|
||||||
|
webroot=host.path,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if not isy.connected:
|
if not isy.connected:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier)
|
_categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier)
|
||||||
_categorize_programs(hass, isy.programs)
|
_categorize_programs(hass_isy_data, isy.programs)
|
||||||
|
|
||||||
def stop(event: object) -> None:
|
# Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
|
||||||
"""Stop ISY auto updates."""
|
_LOGGER.info(repr(isy.clock))
|
||||||
isy.auto_update = False
|
|
||||||
|
|
||||||
# Listen for HA stop to disconnect.
|
hass_isy_data[ISY994_ISY] = isy
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
|
|
||||||
|
|
||||||
# Load platforms for the devices in the ISY controller that we support.
|
# Load platforms for the devices in the ISY controller that we support.
|
||||||
for platform in SUPPORTED_PLATFORMS:
|
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
|
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
|
||||||
|
|
||||||
return 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
|
||||||
|
@ -23,20 +23,24 @@ from homeassistant.components.binary_sensor import (
|
|||||||
DOMAIN as BINARY_SENSOR,
|
DOMAIN as BINARY_SENSOR,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
|
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
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 homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import ISY994_NODES, ISY994_PROGRAMS
|
|
||||||
from .const import (
|
from .const import (
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
BINARY_SENSOR_DEVICE_TYPES_ISY,
|
BINARY_SENSOR_DEVICE_TYPES_ISY,
|
||||||
BINARY_SENSOR_DEVICE_TYPES_ZWAVE,
|
BINARY_SENSOR_DEVICE_TYPES_ZWAVE,
|
||||||
|
DOMAIN as ISY994_DOMAIN,
|
||||||
|
ISY994_NODES,
|
||||||
|
ISY994_PROGRAMS,
|
||||||
TYPE_CATEGORY_CLIMATE,
|
TYPE_CATEGORY_CLIMATE,
|
||||||
)
|
)
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
from .helpers import migrate_old_unique_ids
|
||||||
|
|
||||||
DEVICE_PARENT_REQUIRED = [
|
DEVICE_PARENT_REQUIRED = [
|
||||||
DEVICE_CLASS_OPENING,
|
DEVICE_CLASS_OPENING,
|
||||||
@ -56,15 +60,18 @@ SUBNODE_MOTION_DISABLED = (13, 19) # Int->13 or Hex->0xD depending on firmware
|
|||||||
TYPE_INSTEON_MOTION = ("16.1.", "16.22.")
|
TYPE_INSTEON_MOTION = ("16.1.", "16.22.")
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
|
hass: HomeAssistantType,
|
||||||
):
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[list], None],
|
||||||
|
) -> bool:
|
||||||
"""Set up the ISY994 binary sensor platform."""
|
"""Set up the ISY994 binary sensor platform."""
|
||||||
devices = []
|
devices = []
|
||||||
devices_by_address = {}
|
devices_by_address = {}
|
||||||
child_nodes = []
|
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)
|
device_class, device_type = _detect_device_type_and_class(node)
|
||||||
if node.protocol == PROTO_INSTEON:
|
if node.protocol == PROTO_INSTEON:
|
||||||
if node.parent_node is not None:
|
if node.parent_node is not None:
|
||||||
@ -162,10 +169,11 @@ def setup_platform(
|
|||||||
device = ISYBinarySensorEntity(node, device_class)
|
device = ISYBinarySensorEntity(node, device_class)
|
||||||
devices.append(device)
|
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))
|
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):
|
def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str):
|
||||||
@ -235,7 +243,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
|
|||||||
|
|
||||||
Often times, a single device is represented by multiple nodes in the ISY,
|
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
|
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.
|
Assistant entity and handles both ways that ISY binary sensors can work.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
180
homeassistant/components/isy994/config_flow.py
Normal file
180
homeassistant/components/isy994/config_flow.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
"""Config flow for Universal Devices ISY994 integration."""
|
||||||
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from pyisy.configuration import Configuration
|
||||||
|
from pyisy.connection import Connection
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core, exceptions
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_IGNORE_STRING,
|
||||||
|
CONF_RESTORE_LIGHT_STATE,
|
||||||
|
CONF_SENSOR_STRING,
|
||||||
|
CONF_TLS_VER,
|
||||||
|
DEFAULT_IGNORE_STRING,
|
||||||
|
DEFAULT_RESTORE_LIGHT_STATE,
|
||||||
|
DEFAULT_SENSOR_STRING,
|
||||||
|
DEFAULT_TLS_VERSION,
|
||||||
|
)
|
||||||
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
vol.Optional(CONF_TLS_VER, default=DEFAULT_TLS_VERSION): vol.In([1.1, 1.2]),
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(hass: core.HomeAssistant, data):
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
user = data[CONF_USERNAME]
|
||||||
|
password = data[CONF_PASSWORD]
|
||||||
|
host = urlparse(data[CONF_HOST])
|
||||||
|
tls_version = data.get(CONF_TLS_VER)
|
||||||
|
|
||||||
|
if host.scheme == "http":
|
||||||
|
https = False
|
||||||
|
port = host.port or 80
|
||||||
|
elif host.scheme == "https":
|
||||||
|
https = True
|
||||||
|
port = host.port or 443
|
||||||
|
else:
|
||||||
|
_LOGGER.error("isy994 host value in configuration is invalid")
|
||||||
|
raise InvalidHost
|
||||||
|
|
||||||
|
# Connect to ISY controller.
|
||||||
|
isy_conf = await hass.async_add_executor_job(
|
||||||
|
_fetch_isy_configuration,
|
||||||
|
host.hostname,
|
||||||
|
port,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
https,
|
||||||
|
tls_version,
|
||||||
|
host.path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return info that you want to store in the config entry.
|
||||||
|
return {"title": f"{isy_conf['name']} ({host.hostname})", "uuid": isy_conf["uuid"]}
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_isy_configuration(
|
||||||
|
address, port, username, password, use_https, tls_ver, webroot
|
||||||
|
):
|
||||||
|
"""Validate and fetch the configuration from the ISY."""
|
||||||
|
try:
|
||||||
|
isy_conn = Connection(
|
||||||
|
address,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
use_https,
|
||||||
|
tls_ver,
|
||||||
|
log=_LOGGER,
|
||||||
|
webroot=webroot,
|
||||||
|
)
|
||||||
|
except ValueError as err:
|
||||||
|
raise InvalidAuth(err.args[0])
|
||||||
|
|
||||||
|
return Configuration(log=_LOGGER, xml=isy_conn.get_config())
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Universal Devices ISY994."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
info = None
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
info = await validate_input(self.hass, user_input)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidHost:
|
||||||
|
errors["base"] = "invalid_host"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
if "base" not in errors:
|
||||||
|
await self.async_set_unique_id(info["uuid"])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(title=info["title"], data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, user_input):
|
||||||
|
"""Handle import."""
|
||||||
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle a option flow for isy994."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle options flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
options = self.config_entry.options
|
||||||
|
restore_light_state = options.get(
|
||||||
|
CONF_RESTORE_LIGHT_STATE, DEFAULT_RESTORE_LIGHT_STATE
|
||||||
|
)
|
||||||
|
ignore_string = options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING)
|
||||||
|
sensor_string = options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING)
|
||||||
|
|
||||||
|
options_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_IGNORE_STRING, default=ignore_string): str,
|
||||||
|
vol.Optional(CONF_SENSOR_STRING, default=sensor_string): str,
|
||||||
|
vol.Required(
|
||||||
|
CONF_RESTORE_LIGHT_STATE, default=restore_light_state
|
||||||
|
): bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="init", data_schema=options_schema)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidHost(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate the host value is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
@ -98,9 +98,11 @@ MANUFACTURER = "Universal Devices, Inc"
|
|||||||
CONF_IGNORE_STRING = "ignore_string"
|
CONF_IGNORE_STRING = "ignore_string"
|
||||||
CONF_SENSOR_STRING = "sensor_string"
|
CONF_SENSOR_STRING = "sensor_string"
|
||||||
CONF_TLS_VER = "tls"
|
CONF_TLS_VER = "tls"
|
||||||
|
CONF_RESTORE_LIGHT_STATE = "restore_light_state"
|
||||||
|
|
||||||
DEFAULT_IGNORE_STRING = "{IGNORE ME}"
|
DEFAULT_IGNORE_STRING = "{IGNORE ME}"
|
||||||
DEFAULT_SENSOR_STRING = "sensor"
|
DEFAULT_SENSOR_STRING = "sensor"
|
||||||
|
DEFAULT_RESTORE_LIGHT_STATE = False
|
||||||
DEFAULT_TLS_VERSION = 1.1
|
DEFAULT_TLS_VERSION = 1.1
|
||||||
DEFAULT_PROGRAM_STRING = "HA."
|
DEFAULT_PROGRAM_STRING = "HA."
|
||||||
|
|
||||||
@ -136,12 +138,16 @@ TYPE_CATEGORY_POOL_CTL = "6."
|
|||||||
TYPE_CATEGORY_SENSOR_ACTUATORS = "7."
|
TYPE_CATEGORY_SENSOR_ACTUATORS = "7."
|
||||||
TYPE_CATEGORY_ENERGY_MGMT = "9."
|
TYPE_CATEGORY_ENERGY_MGMT = "9."
|
||||||
TYPE_CATEGORY_COVER = "14."
|
TYPE_CATEGORY_COVER = "14."
|
||||||
TYPE_CATEOGRY_LOCK = "15."
|
TYPE_CATEGORY_LOCK = "15."
|
||||||
TYPE_CATEGORY_SAFETY = "16."
|
TYPE_CATEGORY_SAFETY = "16."
|
||||||
TYPE_CATEGORY_X10 = "113."
|
TYPE_CATEGORY_X10 = "113."
|
||||||
|
|
||||||
|
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||||
|
|
||||||
# Do not use the Home Assistant consts for the states here - we're matching exact API
|
# Do not use the Home Assistant consts for the states here - we're matching exact API
|
||||||
# responses, not using them for Home Assistant states
|
# responses, not using them for Home Assistant states
|
||||||
|
# Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml
|
||||||
|
# Z-Wave Categories: https://www.universal-devices.com/developers/wsdk/5.0.4/4_fam.xml
|
||||||
NODE_FILTERS = {
|
NODE_FILTERS = {
|
||||||
BINARY_SENSOR: {
|
BINARY_SENSOR: {
|
||||||
FILTER_UOM: [],
|
FILTER_UOM: [],
|
||||||
@ -191,7 +197,7 @@ NODE_FILTERS = {
|
|||||||
FILTER_UOM: ["11"],
|
FILTER_UOM: ["11"],
|
||||||
FILTER_STATES: ["locked", "unlocked"],
|
FILTER_STATES: ["locked", "unlocked"],
|
||||||
FILTER_NODE_DEF_ID: ["DoorLock"],
|
FILTER_NODE_DEF_ID: ["DoorLock"],
|
||||||
FILTER_INSTEON_TYPE: [TYPE_CATEOGRY_LOCK, "4.64."],
|
FILTER_INSTEON_TYPE: [TYPE_CATEGORY_LOCK, "4.64."],
|
||||||
FILTER_ZWAVE_CAT: ["111"],
|
FILTER_ZWAVE_CAT: ["111"],
|
||||||
},
|
},
|
||||||
FAN: {
|
FAN: {
|
||||||
|
@ -4,26 +4,37 @@ from typing import Callable
|
|||||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||||
|
|
||||||
from homeassistant.components.cover import DOMAIN as COVER, CoverEntity
|
from homeassistant.components.cover import DOMAIN as COVER, CoverEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN
|
from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import ISY994_NODES, ISY994_PROGRAMS
|
from .const import (
|
||||||
from .const import _LOGGER, UOM_TO_STATES
|
_LOGGER,
|
||||||
|
DOMAIN as ISY994_DOMAIN,
|
||||||
|
ISY994_NODES,
|
||||||
|
ISY994_PROGRAMS,
|
||||||
|
UOM_TO_STATES,
|
||||||
|
)
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
from .helpers import migrate_old_unique_ids
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
|
hass: HomeAssistantType,
|
||||||
):
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[list], None],
|
||||||
|
) -> bool:
|
||||||
"""Set up the ISY994 cover platform."""
|
"""Set up the ISY994 cover platform."""
|
||||||
|
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
|
||||||
devices = []
|
devices = []
|
||||||
for node in hass.data[ISY994_NODES][COVER]:
|
for node in hass_isy_data[ISY994_NODES][COVER]:
|
||||||
devices.append(ISYCoverEntity(node))
|
devices.append(ISYCoverEntity(node))
|
||||||
|
|
||||||
for name, status, actions in hass.data[ISY994_PROGRAMS][COVER]:
|
for name, status, actions in hass_isy_data[ISY994_PROGRAMS][COVER]:
|
||||||
devices.append(ISYCoverProgramEntity(name, status, actions))
|
devices.append(ISYCoverProgramEntity(name, status, actions))
|
||||||
|
|
||||||
add_entities(devices)
|
await migrate_old_unique_ids(hass, COVER, devices)
|
||||||
|
async_add_entities(devices)
|
||||||
|
|
||||||
|
|
||||||
class ISYCoverEntity(ISYNodeEntity, CoverEntity):
|
class ISYCoverEntity(ISYNodeEntity, CoverEntity):
|
||||||
|
@ -56,6 +56,13 @@ class ISYEntity(Entity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Get the unique identifier of the device."""
|
"""Get the unique identifier of the device."""
|
||||||
|
if hasattr(self._node, "address"):
|
||||||
|
return f"{self._node.isy.configuration['uuid']}_{self._node.address}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def old_unique_id(self) -> str:
|
||||||
|
"""Get the old unique identifier of the device."""
|
||||||
if hasattr(self._node, "address"):
|
if hasattr(self._node, "address"):
|
||||||
return self._node.address
|
return self._node.address
|
||||||
return None
|
return None
|
||||||
|
@ -10,11 +10,12 @@ from homeassistant.components.fan import (
|
|||||||
SUPPORT_SET_SPEED,
|
SUPPORT_SET_SPEED,
|
||||||
FanEntity,
|
FanEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import ISY994_NODES, ISY994_PROGRAMS
|
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
|
||||||
from .const import _LOGGER
|
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
from .helpers import migrate_old_unique_ids
|
||||||
|
|
||||||
VALUE_TO_STATE = {
|
VALUE_TO_STATE = {
|
||||||
0: SPEED_OFF,
|
0: SPEED_OFF,
|
||||||
@ -30,19 +31,23 @@ for key in VALUE_TO_STATE:
|
|||||||
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
|
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
|
hass: HomeAssistantType,
|
||||||
):
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[list], None],
|
||||||
|
) -> bool:
|
||||||
"""Set up the ISY994 fan platform."""
|
"""Set up the ISY994 fan platform."""
|
||||||
|
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
|
||||||
devices = []
|
devices = []
|
||||||
|
|
||||||
for node in hass.data[ISY994_NODES][FAN]:
|
for node in hass_isy_data[ISY994_NODES][FAN]:
|
||||||
devices.append(ISYFanEntity(node))
|
devices.append(ISYFanEntity(node))
|
||||||
|
|
||||||
for name, status, actions in hass.data[ISY994_PROGRAMS][FAN]:
|
for name, status, actions in hass_isy_data[ISY994_PROGRAMS][FAN]:
|
||||||
devices.append(ISYFanProgramEntity(name, status, actions))
|
devices.append(ISYFanProgramEntity(name, status, actions))
|
||||||
|
|
||||||
add_entities(devices)
|
await migrate_old_unique_ids(hass, FAN, devices)
|
||||||
|
async_add_entities(devices)
|
||||||
|
|
||||||
|
|
||||||
class ISYFanEntity(ISYNodeEntity, FanEntity):
|
class ISYFanEntity(ISYNodeEntity, FanEntity):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""Sorting helpers for ISY994 device classifications."""
|
"""Sorting helpers for ISY994 device classifications."""
|
||||||
from typing import Union
|
from typing import Any, List, Optional, Union
|
||||||
|
|
||||||
from pyisy.constants import (
|
from pyisy.constants import (
|
||||||
PROTO_GROUP,
|
PROTO_GROUP,
|
||||||
@ -16,11 +16,13 @@ from homeassistant.components.fan import DOMAIN as FAN
|
|||||||
from homeassistant.components.light import DOMAIN as LIGHT
|
from homeassistant.components.light import DOMAIN as LIGHT
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||||
|
from homeassistant.helpers.entity_registry import async_get_registry
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
DEFAULT_PROGRAM_STRING,
|
DEFAULT_PROGRAM_STRING,
|
||||||
|
DOMAIN,
|
||||||
FILTER_INSTEON_TYPE,
|
FILTER_INSTEON_TYPE,
|
||||||
FILTER_NODE_DEF_ID,
|
FILTER_NODE_DEF_ID,
|
||||||
FILTER_STATES,
|
FILTER_STATES,
|
||||||
@ -48,7 +50,7 @@ SUBNODE_IOLINC_RELAY = 2
|
|||||||
|
|
||||||
|
|
||||||
def _check_for_node_def(
|
def _check_for_node_def(
|
||||||
hass: HomeAssistantType, node: Union[Group, Node], single_platform: str = None
|
hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if the node matches the node_def_id for any platforms.
|
"""Check if the node matches the node_def_id for any platforms.
|
||||||
|
|
||||||
@ -64,14 +66,14 @@ def _check_for_node_def(
|
|||||||
platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
|
platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
|
||||||
for platform in platforms:
|
for platform in platforms:
|
||||||
if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]:
|
if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]:
|
||||||
hass.data[ISY994_NODES][platform].append(node)
|
hass_isy_data[ISY994_NODES][platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_for_insteon_type(
|
def _check_for_insteon_type(
|
||||||
hass: HomeAssistantType, node: Union[Group, Node], single_platform: str = None
|
hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if the node matches the Insteon type for any platforms.
|
"""Check if the node matches the Insteon type for any platforms.
|
||||||
|
|
||||||
@ -102,7 +104,7 @@ def _check_for_insteon_type(
|
|||||||
|
|
||||||
# FanLinc, which has a light module as one of its nodes.
|
# FanLinc, which has a light module as one of its nodes.
|
||||||
if platform == FAN and subnode_id == SUBNODE_FANLINC_LIGHT:
|
if platform == FAN and subnode_id == SUBNODE_FANLINC_LIGHT:
|
||||||
hass.data[ISY994_NODES][LIGHT].append(node)
|
hass_isy_data[ISY994_NODES][LIGHT].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# IOLincs which have a sensor and relay on 2 different nodes
|
# IOLincs which have a sensor and relay on 2 different nodes
|
||||||
@ -111,7 +113,7 @@ def _check_for_insteon_type(
|
|||||||
and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS)
|
and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS)
|
||||||
and subnode_id == SUBNODE_IOLINC_RELAY
|
and subnode_id == SUBNODE_IOLINC_RELAY
|
||||||
):
|
):
|
||||||
hass.data[ISY994_NODES][SWITCH].append(node)
|
hass_isy_data[ISY994_NODES][SWITCH].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Smartenit EZIO2X4
|
# Smartenit EZIO2X4
|
||||||
@ -120,17 +122,17 @@ def _check_for_insteon_type(
|
|||||||
and device_type.startswith(TYPE_EZIO2X4)
|
and device_type.startswith(TYPE_EZIO2X4)
|
||||||
and subnode_id in SUBNODE_EZIO2X4_SENSORS
|
and subnode_id in SUBNODE_EZIO2X4_SENSORS
|
||||||
):
|
):
|
||||||
hass.data[ISY994_NODES][BINARY_SENSOR].append(node)
|
hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
hass.data[ISY994_NODES][platform].append(node)
|
hass_isy_data[ISY994_NODES][platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_for_zwave_cat(
|
def _check_for_zwave_cat(
|
||||||
hass: HomeAssistantType, node: Union[Group, Node], single_platform: str = None
|
hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if the node matches the ISY Z-Wave Category for any platforms.
|
"""Check if the node matches the ISY Z-Wave Category for any platforms.
|
||||||
|
|
||||||
@ -154,14 +156,14 @@ def _check_for_zwave_cat(
|
|||||||
]
|
]
|
||||||
):
|
):
|
||||||
|
|
||||||
hass.data[ISY994_NODES][platform].append(node)
|
hass_isy_data[ISY994_NODES][platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_for_uom_id(
|
def _check_for_uom_id(
|
||||||
hass: HomeAssistantType,
|
hass_isy_data: dict,
|
||||||
node: Union[Group, Node],
|
node: Union[Group, Node],
|
||||||
single_platform: str = None,
|
single_platform: str = None,
|
||||||
uom_list: list = None,
|
uom_list: list = None,
|
||||||
@ -182,21 +184,21 @@ def _check_for_uom_id(
|
|||||||
|
|
||||||
if uom_list:
|
if uom_list:
|
||||||
if node_uom in uom_list:
|
if node_uom in uom_list:
|
||||||
hass.data[ISY994_NODES][single_platform].append(node)
|
hass_isy_data[ISY994_NODES][single_platform].append(node)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
|
platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
|
||||||
for platform in platforms:
|
for platform in platforms:
|
||||||
if node_uom in NODE_FILTERS[platform][FILTER_UOM]:
|
if node_uom in NODE_FILTERS[platform][FILTER_UOM]:
|
||||||
hass.data[ISY994_NODES][platform].append(node)
|
hass_isy_data[ISY994_NODES][platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_for_states_in_uom(
|
def _check_for_states_in_uom(
|
||||||
hass: HomeAssistantType,
|
hass_isy_data: dict,
|
||||||
node: Union[Group, Node],
|
node: Union[Group, Node],
|
||||||
single_platform: str = None,
|
single_platform: str = None,
|
||||||
states_list: list = None,
|
states_list: list = None,
|
||||||
@ -219,26 +221,24 @@ def _check_for_states_in_uom(
|
|||||||
|
|
||||||
if states_list:
|
if states_list:
|
||||||
if node_uom == set(states_list):
|
if node_uom == set(states_list):
|
||||||
hass.data[ISY994_NODES][single_platform].append(node)
|
hass_isy_data[ISY994_NODES][single_platform].append(node)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
|
platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
|
||||||
for platform in platforms:
|
for platform in platforms:
|
||||||
if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]):
|
if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]):
|
||||||
hass.data[ISY994_NODES][platform].append(node)
|
hass_isy_data[ISY994_NODES][platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _is_sensor_a_binary_sensor(
|
def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Union[Group, Node]) -> bool:
|
||||||
hass: HomeAssistantType, node: Union[Group, Node]
|
|
||||||
) -> bool:
|
|
||||||
"""Determine if the given sensor node should be a binary_sensor."""
|
"""Determine if the given sensor node should be a binary_sensor."""
|
||||||
if _check_for_node_def(hass, node, single_platform=BINARY_SENSOR):
|
if _check_for_node_def(hass_isy_data, node, single_platform=BINARY_SENSOR):
|
||||||
return True
|
return True
|
||||||
if _check_for_insteon_type(hass, node, single_platform=BINARY_SENSOR):
|
if _check_for_insteon_type(hass_isy_data, node, single_platform=BINARY_SENSOR):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# For the next two checks, we're providing our own set of uoms that
|
# For the next two checks, we're providing our own set of uoms that
|
||||||
@ -246,11 +246,14 @@ def _is_sensor_a_binary_sensor(
|
|||||||
# checks in the context of already knowing that this is definitely a
|
# checks in the context of already knowing that this is definitely a
|
||||||
# sensor device.
|
# sensor device.
|
||||||
if _check_for_uom_id(
|
if _check_for_uom_id(
|
||||||
hass, node, single_platform=BINARY_SENSOR, uom_list=BINARY_SENSOR_UOMS
|
hass_isy_data, node, single_platform=BINARY_SENSOR, uom_list=BINARY_SENSOR_UOMS
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
if _check_for_states_in_uom(
|
if _check_for_states_in_uom(
|
||||||
hass, node, single_platform=BINARY_SENSOR, states_list=BINARY_SENSOR_ISY_STATES
|
hass_isy_data,
|
||||||
|
node,
|
||||||
|
single_platform=BINARY_SENSOR,
|
||||||
|
states_list=BINARY_SENSOR_ISY_STATES,
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -258,10 +261,7 @@ def _is_sensor_a_binary_sensor(
|
|||||||
|
|
||||||
|
|
||||||
def _categorize_nodes(
|
def _categorize_nodes(
|
||||||
hass: HomeAssistantType,
|
hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str
|
||||||
nodes: Nodes,
|
|
||||||
ignore_identifier: str,
|
|
||||||
sensor_identifier: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Sort the nodes to their proper platforms."""
|
"""Sort the nodes to their proper platforms."""
|
||||||
for (path, node) in nodes:
|
for (path, node) in nodes:
|
||||||
@ -271,37 +271,36 @@ def _categorize_nodes(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if hasattr(node, "protocol") and node.protocol == PROTO_GROUP:
|
if hasattr(node, "protocol") and node.protocol == PROTO_GROUP:
|
||||||
hass.data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node)
|
hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if sensor_identifier in path or sensor_identifier in node.name:
|
if sensor_identifier in path or sensor_identifier in node.name:
|
||||||
# User has specified to treat this as a sensor. First we need to
|
# User has specified to treat this as a sensor. First we need to
|
||||||
# determine if it should be a binary_sensor.
|
# determine if it should be a binary_sensor.
|
||||||
if _is_sensor_a_binary_sensor(hass, node):
|
if _is_sensor_a_binary_sensor(hass_isy_data, node):
|
||||||
continue
|
continue
|
||||||
|
hass_isy_data[ISY994_NODES][SENSOR].append(node)
|
||||||
hass.data[ISY994_NODES][SENSOR].append(node)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# We have a bunch of different methods for determining the device type,
|
# We have a bunch of different methods for determining the device type,
|
||||||
# each of which works with different ISY firmware versions or device
|
# each of which works with different ISY firmware versions or device
|
||||||
# family. The order here is important, from most reliable to least.
|
# family. The order here is important, from most reliable to least.
|
||||||
if _check_for_node_def(hass, node):
|
if _check_for_node_def(hass_isy_data, node):
|
||||||
continue
|
continue
|
||||||
if _check_for_insteon_type(hass, node):
|
if _check_for_insteon_type(hass_isy_data, node):
|
||||||
continue
|
continue
|
||||||
if _check_for_zwave_cat(hass, node):
|
if _check_for_zwave_cat(hass_isy_data, node):
|
||||||
continue
|
continue
|
||||||
if _check_for_uom_id(hass, node):
|
if _check_for_uom_id(hass_isy_data, node):
|
||||||
continue
|
continue
|
||||||
if _check_for_states_in_uom(hass, node):
|
if _check_for_states_in_uom(hass_isy_data, node):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes.
|
# Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes.
|
||||||
hass.data[ISY994_NODES][SENSOR].append(node)
|
hass_isy_data[ISY994_NODES][SENSOR].append(node)
|
||||||
|
|
||||||
|
|
||||||
def _categorize_programs(hass: HomeAssistantType, programs: Programs) -> None:
|
def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
|
||||||
"""Categorize the ISY994 programs."""
|
"""Categorize the ISY994 programs."""
|
||||||
for platform in SUPPORTED_PROGRAM_PLATFORMS:
|
for platform in SUPPORTED_PROGRAM_PLATFORMS:
|
||||||
folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}")
|
folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}")
|
||||||
@ -334,4 +333,36 @@ def _categorize_programs(hass: HomeAssistantType, programs: Programs) -> None:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
entity = (entity_folder.name, status, actions)
|
entity = (entity_folder.name, status, actions)
|
||||||
hass.data[ISY994_PROGRAMS][platform].append(entity)
|
hass_isy_data[ISY994_PROGRAMS][platform].append(entity)
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate_old_unique_ids(
|
||||||
|
hass: HomeAssistantType, platform: str, devices: Optional[List[Any]]
|
||||||
|
) -> None:
|
||||||
|
"""Migrate to new controller-specific unique ids."""
|
||||||
|
registry = await async_get_registry(hass)
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
old_entity_id = registry.async_get_entity_id(
|
||||||
|
platform, DOMAIN, device.old_unique_id
|
||||||
|
)
|
||||||
|
if old_entity_id is not None:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migrating unique_id from [%s] to [%s]",
|
||||||
|
device.old_unique_id,
|
||||||
|
device.unique_id,
|
||||||
|
)
|
||||||
|
registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id)
|
||||||
|
|
||||||
|
old_entity_id_2 = registry.async_get_entity_id(
|
||||||
|
platform, DOMAIN, device.unique_id.replace(":", "")
|
||||||
|
)
|
||||||
|
if old_entity_id_2 is not None:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migrating unique_id from [%s] to [%s]",
|
||||||
|
device.unique_id.replace(":", ""),
|
||||||
|
device.unique_id,
|
||||||
|
)
|
||||||
|
registry.async_update_entity(
|
||||||
|
old_entity_id_2, new_unique_id=device.unique_id
|
||||||
|
)
|
||||||
|
@ -8,35 +8,49 @@ from homeassistant.components.light import (
|
|||||||
SUPPORT_BRIGHTNESS,
|
SUPPORT_BRIGHTNESS,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_UNKNOWN
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import ISY994_NODES
|
from .const import (
|
||||||
from .const import _LOGGER
|
_LOGGER,
|
||||||
|
CONF_RESTORE_LIGHT_STATE,
|
||||||
|
DOMAIN as ISY994_DOMAIN,
|
||||||
|
ISY994_NODES,
|
||||||
|
)
|
||||||
from .entity import ISYNodeEntity
|
from .entity import ISYNodeEntity
|
||||||
|
from .helpers import migrate_old_unique_ids
|
||||||
|
|
||||||
ATTR_LAST_BRIGHTNESS = "last_brightness"
|
ATTR_LAST_BRIGHTNESS = "last_brightness"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
|
hass: HomeAssistantType,
|
||||||
):
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[list], None],
|
||||||
|
) -> bool:
|
||||||
"""Set up the ISY994 light platform."""
|
"""Set up the ISY994 light platform."""
|
||||||
devices = []
|
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
|
||||||
for node in hass.data[ISY994_NODES][LIGHT]:
|
isy_options = entry.options
|
||||||
devices.append(ISYLightEntity(node))
|
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)
|
||||||
|
|
||||||
add_entities(devices)
|
devices = []
|
||||||
|
for node in hass_isy_data[ISY994_NODES][LIGHT]:
|
||||||
|
devices.append(ISYLightEntity(node, restore_light_state))
|
||||||
|
|
||||||
|
await migrate_old_unique_ids(hass, LIGHT, devices)
|
||||||
|
async_add_entities(devices)
|
||||||
|
|
||||||
|
|
||||||
class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
||||||
"""Representation of an ISY994 light device."""
|
"""Representation of an ISY994 light device."""
|
||||||
|
|
||||||
def __init__(self, node) -> None:
|
def __init__(self, node, restore_light_state) -> None:
|
||||||
"""Initialize the ISY994 light device."""
|
"""Initialize the ISY994 light device."""
|
||||||
super().__init__(node)
|
super().__init__(node)
|
||||||
self._last_brightness = None
|
self._last_brightness = None
|
||||||
|
self._restore_light_state = restore_light_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -65,16 +79,11 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
|||||||
# pylint: disable=arguments-differ
|
# pylint: disable=arguments-differ
|
||||||
def turn_on(self, brightness=None, **kwargs) -> None:
|
def turn_on(self, brightness=None, **kwargs) -> None:
|
||||||
"""Send the turn on command to the ISY994 light device."""
|
"""Send the turn on command to the ISY994 light device."""
|
||||||
if brightness is None and self._last_brightness:
|
if self._restore_light_state and brightness is None and self._last_brightness:
|
||||||
brightness = self._last_brightness
|
brightness = self._last_brightness
|
||||||
if not self._node.turn_on(val=brightness):
|
if not self._node.turn_on(val=brightness):
|
||||||
_LOGGER.debug("Unable to turn on light")
|
_LOGGER.debug("Unable to turn on light")
|
||||||
|
|
||||||
@property
|
|
||||||
def supported_features(self):
|
|
||||||
"""Flag supported features."""
|
|
||||||
return SUPPORT_BRIGHTNESS
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self) -> Dict:
|
def device_state_attributes(self) -> Dict:
|
||||||
"""Return the light attributes."""
|
"""Return the light attributes."""
|
||||||
@ -82,6 +91,11 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
|||||||
attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness
|
attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness
|
||||||
return attribs
|
return attribs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_BRIGHTNESS
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Restore last_brightness on restart."""
|
"""Restore last_brightness on restart."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
@ -4,28 +4,33 @@ from typing import Callable
|
|||||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||||
|
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK, LockEntity
|
from homeassistant.components.lock import DOMAIN as LOCK, LockEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED
|
from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import ISY994_NODES, ISY994_PROGRAMS
|
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
|
||||||
from .const import _LOGGER
|
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
from .helpers import migrate_old_unique_ids
|
||||||
|
|
||||||
VALUE_TO_STATE = {0: STATE_UNLOCKED, 100: STATE_LOCKED}
|
VALUE_TO_STATE = {0: STATE_UNLOCKED, 100: STATE_LOCKED}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
|
hass: HomeAssistantType,
|
||||||
):
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[list], None],
|
||||||
|
) -> bool:
|
||||||
"""Set up the ISY994 lock platform."""
|
"""Set up the ISY994 lock platform."""
|
||||||
|
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
|
||||||
devices = []
|
devices = []
|
||||||
for node in hass.data[ISY994_NODES][LOCK]:
|
for node in hass_isy_data[ISY994_NODES][LOCK]:
|
||||||
devices.append(ISYLockEntity(node))
|
devices.append(ISYLockEntity(node))
|
||||||
|
|
||||||
for name, status, actions in hass.data[ISY994_PROGRAMS][LOCK]:
|
for name, status, actions in hass_isy_data[ISY994_PROGRAMS][LOCK]:
|
||||||
devices.append(ISYLockProgramEntity(name, status, actions))
|
devices.append(ISYLockProgramEntity(name, status, actions))
|
||||||
|
|
||||||
add_entities(devices)
|
await migrate_old_unique_ids(hass, LOCK, devices)
|
||||||
|
async_add_entities(devices)
|
||||||
|
|
||||||
|
|
||||||
class ISYLockEntity(ISYNodeEntity, LockEntity):
|
class ISYLockEntity(ISYNodeEntity, LockEntity):
|
||||||
|
@ -3,5 +3,6 @@
|
|||||||
"name": "Universal Devices ISY994",
|
"name": "Universal Devices ISY994",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/isy994",
|
"documentation": "https://www.home-assistant.io/integrations/isy994",
|
||||||
"requirements": ["pyisy==2.0.2"],
|
"requirements": ["pyisy==2.0.2"],
|
||||||
"codeowners": ["@bdraco", "@shbatm"]
|
"codeowners": ["@bdraco", "@shbatm"],
|
||||||
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
@ -4,25 +4,36 @@ from typing import Callable
|
|||||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||||
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import ISY994_NODES
|
from .const import (
|
||||||
from .const import _LOGGER, UOM_FRIENDLY_NAME, UOM_TO_STATES
|
_LOGGER,
|
||||||
|
DOMAIN as ISY994_DOMAIN,
|
||||||
|
ISY994_NODES,
|
||||||
|
UOM_FRIENDLY_NAME,
|
||||||
|
UOM_TO_STATES,
|
||||||
|
)
|
||||||
from .entity import ISYNodeEntity
|
from .entity import ISYNodeEntity
|
||||||
|
from .helpers import migrate_old_unique_ids
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
|
hass: HomeAssistantType,
|
||||||
):
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[list], None],
|
||||||
|
) -> bool:
|
||||||
"""Set up the ISY994 sensor platform."""
|
"""Set up the ISY994 sensor platform."""
|
||||||
|
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
|
||||||
devices = []
|
devices = []
|
||||||
|
|
||||||
for node in hass.data[ISY994_NODES][SENSOR]:
|
for node in hass_isy_data[ISY994_NODES][SENSOR]:
|
||||||
_LOGGER.debug("Loading %s", node.name)
|
_LOGGER.debug("Loading %s", node.name)
|
||||||
devices.append(ISYSensorEntity(node))
|
devices.append(ISYSensorEntity(node))
|
||||||
|
|
||||||
add_entities(devices)
|
await migrate_old_unique_ids(hass, SENSOR, devices)
|
||||||
|
async_add_entities(devices)
|
||||||
|
|
||||||
|
|
||||||
class ISYSensorEntity(ISYNodeEntity):
|
class ISYSensorEntity(ISYNodeEntity):
|
||||||
|
39
homeassistant/components/isy994/strings.json
Normal file
39
homeassistant/components/isy994/strings.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"title": "Universal Devices ISY994",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"host": "URL",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"tls": "The TLS version of the ISY controller."
|
||||||
|
},
|
||||||
|
"description": "The host entry must be in full URL format, e.g., http://192.168.10.100:80",
|
||||||
|
"title": "Connect to your ISY994"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%",
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "ISY994 Options",
|
||||||
|
"description": "Set the options for the ISY Integration: \n • Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n • Ignore String: Any device with 'Ignore String' in the name will be ignored. \n • Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.",
|
||||||
|
"data": {
|
||||||
|
"sensor_string": "Node Sensor String",
|
||||||
|
"ignore_string": "Ignore String",
|
||||||
|
"restore_light_state": "Restore Light Brightness"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,26 +4,31 @@ from typing import Callable
|
|||||||
from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP
|
from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP
|
||||||
|
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity
|
from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_UNKNOWN
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import ISY994_NODES, ISY994_PROGRAMS
|
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
|
||||||
from .const import _LOGGER
|
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
from .helpers import migrate_old_unique_ids
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
|
hass: HomeAssistantType,
|
||||||
):
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[list], None],
|
||||||
|
) -> bool:
|
||||||
"""Set up the ISY994 switch platform."""
|
"""Set up the ISY994 switch platform."""
|
||||||
|
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
|
||||||
devices = []
|
devices = []
|
||||||
for node in hass.data[ISY994_NODES][SWITCH]:
|
for node in hass_isy_data[ISY994_NODES][SWITCH]:
|
||||||
devices.append(ISYSwitchEntity(node))
|
devices.append(ISYSwitchEntity(node))
|
||||||
|
|
||||||
for name, status, actions in hass.data[ISY994_PROGRAMS][SWITCH]:
|
for name, status, actions in hass_isy_data[ISY994_PROGRAMS][SWITCH]:
|
||||||
devices.append(ISYSwitchProgramEntity(name, status, actions))
|
devices.append(ISYSwitchProgramEntity(name, status, actions))
|
||||||
|
|
||||||
add_entities(devices)
|
await migrate_old_unique_ids(hass, SWITCH, devices)
|
||||||
|
async_add_entities(devices)
|
||||||
|
|
||||||
|
|
||||||
class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):
|
class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):
|
||||||
|
39
homeassistant/components/isy994/translations/en.json
Normal file
39
homeassistant/components/isy994/translations/en.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"title": "Universal Devices ISY994",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"username": "Username",
|
||||||
|
"host": "URL",
|
||||||
|
"password": "Password",
|
||||||
|
"tls": "The TLS version of the ISY controller."
|
||||||
|
},
|
||||||
|
"description": "The host entry must be in full URL format, e.g., http://192.168.10.100:80",
|
||||||
|
"title": "Connect to your ISY994"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"unknown": "Unexpected error",
|
||||||
|
"cannot_connect": "Failed to connect, please try again",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "ISY994 Options",
|
||||||
|
"description": "Set the options for the ISY Integration: \n • Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n • Ignore String: Any device with 'Ignore String' in the name will be ignored. \n • Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.",
|
||||||
|
"data": {
|
||||||
|
"sensor_string": "Node Sensor String",
|
||||||
|
"ignore_string": "Ignore String",
|
||||||
|
"restore_light_state": "Restore Light Brightness"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -68,6 +68,7 @@ FLOWS = [
|
|||||||
"ipp",
|
"ipp",
|
||||||
"iqvia",
|
"iqvia",
|
||||||
"islamic_prayer_times",
|
"islamic_prayer_times",
|
||||||
|
"isy994",
|
||||||
"izone",
|
"izone",
|
||||||
"juicenet",
|
"juicenet",
|
||||||
"konnected",
|
"konnected",
|
||||||
|
@ -572,6 +572,9 @@ pyipp==0.10.1
|
|||||||
# homeassistant.components.iqvia
|
# homeassistant.components.iqvia
|
||||||
pyiqvia==0.2.1
|
pyiqvia==0.2.1
|
||||||
|
|
||||||
|
# homeassistant.components.isy994
|
||||||
|
pyisy==2.0.2
|
||||||
|
|
||||||
# homeassistant.components.kira
|
# homeassistant.components.kira
|
||||||
pykira==0.1.1
|
pykira==0.1.1
|
||||||
|
|
||||||
|
1
tests/components/isy994/__init__.py
Normal file
1
tests/components/isy994/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Universal Devices ISY994 integration."""
|
206
tests/components/isy994/test_config_flow.py
Normal file
206
tests/components/isy994/test_config_flow.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
"""Test the Universal Devices ISY994 config flow."""
|
||||||
|
|
||||||
|
from homeassistant import config_entries, data_entry_flow, setup
|
||||||
|
from homeassistant.components.isy994.config_flow import CannotConnect
|
||||||
|
from homeassistant.components.isy994.const import (
|
||||||
|
CONF_IGNORE_STRING,
|
||||||
|
CONF_RESTORE_LIGHT_STATE,
|
||||||
|
CONF_SENSOR_STRING,
|
||||||
|
CONF_TLS_VER,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
MOCK_HOSTNAME = "1.1.1.1"
|
||||||
|
MOCK_USERNAME = "test-username"
|
||||||
|
MOCK_PASSWORD = "test-password"
|
||||||
|
|
||||||
|
# Don't use the integration defaults here to make sure they're being set correctly.
|
||||||
|
MOCK_TLS_VERSION = 1.2
|
||||||
|
MOCK_IGNORE_STRING = "{IGNOREME}"
|
||||||
|
MOCK_RESTORE_LIGHT_STATE = True
|
||||||
|
MOCK_SENSOR_STRING = "IMASENSOR"
|
||||||
|
|
||||||
|
MOCK_USER_INPUT = {
|
||||||
|
"host": f"http://{MOCK_HOSTNAME}",
|
||||||
|
"username": MOCK_USERNAME,
|
||||||
|
"password": MOCK_PASSWORD,
|
||||||
|
"tls": MOCK_TLS_VERSION,
|
||||||
|
}
|
||||||
|
MOCK_IMPORT_BASIC_CONFIG = {
|
||||||
|
CONF_HOST: f"http://{MOCK_HOSTNAME}",
|
||||||
|
CONF_USERNAME: MOCK_USERNAME,
|
||||||
|
CONF_PASSWORD: MOCK_PASSWORD,
|
||||||
|
}
|
||||||
|
MOCK_IMPORT_FULL_CONFIG = {
|
||||||
|
CONF_HOST: f"http://{MOCK_HOSTNAME}",
|
||||||
|
CONF_USERNAME: MOCK_USERNAME,
|
||||||
|
CONF_PASSWORD: MOCK_PASSWORD,
|
||||||
|
CONF_IGNORE_STRING: MOCK_IGNORE_STRING,
|
||||||
|
CONF_RESTORE_LIGHT_STATE: MOCK_RESTORE_LIGHT_STATE,
|
||||||
|
CONF_SENSOR_STRING: MOCK_SENSOR_STRING,
|
||||||
|
CONF_TLS_VER: MOCK_TLS_VERSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_DEVICE_NAME = "Name of the device"
|
||||||
|
MOCK_UUID = "CE:FB:72:31:B7:B9"
|
||||||
|
MOCK_VALIDATED_RESPONSE = {"name": MOCK_DEVICE_NAME, "uuid": MOCK_UUID}
|
||||||
|
|
||||||
|
PATCH_CONFIGURATION = "homeassistant.components.isy994.config_flow.Configuration"
|
||||||
|
PATCH_CONNECTION = "homeassistant.components.isy994.config_flow.Connection"
|
||||||
|
PATCH_ASYNC_SETUP = "homeassistant.components.isy994.async_setup"
|
||||||
|
PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.isy994.async_setup_entry"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass: HomeAssistantType):
|
||||||
|
"""Test we get the form."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(PATCH_CONFIGURATION) as mock_config_class, patch(
|
||||||
|
PATCH_CONNECTION
|
||||||
|
) as mock_connection_class, patch(
|
||||||
|
PATCH_ASYNC_SETUP, return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
PATCH_ASYNC_SETUP_ENTRY, return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
isy_conn = mock_connection_class.return_value
|
||||||
|
isy_conn.get_config.return_value = "<xml></xml>"
|
||||||
|
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})"
|
||||||
|
assert result2["result"].unique_id == MOCK_UUID
|
||||||
|
assert result2["data"] == MOCK_USER_INPUT
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_invalid_host(hass: HomeAssistantType):
|
||||||
|
"""Test we handle invalid host."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
"host": MOCK_HOSTNAME, # Test with missing protocol (http://)
|
||||||
|
"username": MOCK_USERNAME,
|
||||||
|
"password": MOCK_PASSWORD,
|
||||||
|
"tls": MOCK_TLS_VERSION,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["errors"] == {"base": "invalid_host"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_invalid_auth(hass: HomeAssistantType):
|
||||||
|
"""Test we handle invalid auth."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
with patch(PATCH_CONFIGURATION), patch(
|
||||||
|
PATCH_CONNECTION, side_effect=ValueError("PyISY could not connect to the ISY."),
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass: HomeAssistantType):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
with patch(PATCH_CONFIGURATION), patch(
|
||||||
|
PATCH_CONNECTION, side_effect=CannotConnect,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_existing_config_entry(hass: HomeAssistantType):
|
||||||
|
"""Test if config entry already exists."""
|
||||||
|
MockConfigEntry(domain=DOMAIN, unique_id=MOCK_UUID).add_to_hass(hass)
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(PATCH_CONFIGURATION) as mock_config_class, patch(
|
||||||
|
PATCH_CONNECTION
|
||||||
|
) as mock_connection_class:
|
||||||
|
isy_conn = mock_connection_class.return_value
|
||||||
|
isy_conn.get_config.return_value = "<xml></xml>"
|
||||||
|
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_flow_some_fields(hass: HomeAssistantType) -> None:
|
||||||
|
"""Test import config flow with just the basic fields."""
|
||||||
|
with patch(PATCH_CONFIGURATION) as mock_config_class, patch(
|
||||||
|
PATCH_CONNECTION
|
||||||
|
) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch(
|
||||||
|
PATCH_ASYNC_SETUP_ENTRY, return_value=True,
|
||||||
|
):
|
||||||
|
isy_conn = mock_connection_class.return_value
|
||||||
|
isy_conn.get_config.return_value = "<xml></xml>"
|
||||||
|
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_BASIC_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}"
|
||||||
|
assert result["data"][CONF_USERNAME] == MOCK_USERNAME
|
||||||
|
assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_flow_all_fields(hass: HomeAssistantType) -> None:
|
||||||
|
"""Test import config flow with all fields."""
|
||||||
|
with patch(PATCH_CONFIGURATION) as mock_config_class, patch(
|
||||||
|
PATCH_CONNECTION
|
||||||
|
) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch(
|
||||||
|
PATCH_ASYNC_SETUP_ENTRY, return_value=True,
|
||||||
|
):
|
||||||
|
isy_conn = mock_connection_class.return_value
|
||||||
|
isy_conn.get_config.return_value = "<xml></xml>"
|
||||||
|
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_FULL_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}"
|
||||||
|
assert result["data"][CONF_USERNAME] == MOCK_USERNAME
|
||||||
|
assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD
|
||||||
|
assert result["data"][CONF_IGNORE_STRING] == MOCK_IGNORE_STRING
|
||||||
|
assert result["data"][CONF_RESTORE_LIGHT_STATE] == MOCK_RESTORE_LIGHT_STATE
|
||||||
|
assert result["data"][CONF_SENSOR_STRING] == MOCK_SENSOR_STRING
|
||||||
|
assert result["data"][CONF_TLS_VER] == MOCK_TLS_VERSION
|
Loading…
x
Reference in New Issue
Block a user