Update PyISY to v3.0.0 and ISY994 to use Async IO (#50806)
This commit is contained in:
parent
1d174a1f6f
commit
775af9d2c5
19 changed files with 325 additions and 248 deletions
|
@ -1,16 +1,23 @@
|
|||
"""Support the ISY-994 controllers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pyisy import ISY
|
||||
from aiohttp import CookieJar
|
||||
import async_timeout
|
||||
from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
|
@ -32,7 +39,7 @@ from .const import (
|
|||
ISY994_VARIABLES,
|
||||
MANUFACTURER,
|
||||
PLATFORMS,
|
||||
SUPPORTED_PROGRAM_PLATFORMS,
|
||||
PROGRAM_PLATFORMS,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables
|
||||
|
@ -115,7 +122,7 @@ async def async_setup_entry(
|
|||
hass_isy_data[ISY994_NODES][platform] = []
|
||||
|
||||
hass_isy_data[ISY994_PROGRAMS] = {}
|
||||
for platform in SUPPORTED_PROGRAM_PLATFORMS:
|
||||
for platform in PROGRAM_PLATFORMS:
|
||||
hass_isy_data[ISY994_PROGRAMS][platform] = []
|
||||
|
||||
hass_isy_data[ISY994_VARIABLES] = []
|
||||
|
@ -139,17 +146,19 @@ async def async_setup_entry(
|
|||
if host.scheme == "http":
|
||||
https = False
|
||||
port = host.port or 80
|
||||
session = aiohttp_client.async_create_clientsession(
|
||||
hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True)
|
||||
)
|
||||
elif host.scheme == "https":
|
||||
https = True
|
||||
port = host.port or 443
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
else:
|
||||
_LOGGER.error("The isy994 host value in configuration is invalid")
|
||||
return False
|
||||
|
||||
# Connect to ISY controller.
|
||||
isy = await hass.async_add_executor_job(
|
||||
partial(
|
||||
ISY,
|
||||
isy = ISY(
|
||||
host.hostname,
|
||||
port,
|
||||
username=user,
|
||||
|
@ -157,13 +166,30 @@ async def async_setup_entry(
|
|||
use_https=https,
|
||||
tls_ver=tls_version,
|
||||
webroot=host.path,
|
||||
websession=session,
|
||||
use_websocket=True,
|
||||
)
|
||||
)
|
||||
if not isy.connected:
|
||||
return False
|
||||
|
||||
# Trigger a status update for all nodes, not done automatically in PyISY v2.x
|
||||
await hass.async_add_executor_job(isy.nodes.update)
|
||||
try:
|
||||
with async_timeout.timeout(30):
|
||||
await isy.initialize()
|
||||
except ISYInvalidAuthError as err:
|
||||
_LOGGER.error(
|
||||
"Invalid credentials for the ISY, please adjust settings and try again: %s",
|
||||
err,
|
||||
)
|
||||
return False
|
||||
except ISYConnectionError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to connect to the ISY, please adjust settings and try again: %s",
|
||||
err,
|
||||
)
|
||||
raise ConfigEntryNotReady from err
|
||||
except ISYResponseParseError as err:
|
||||
_LOGGER.warning(
|
||||
"Error processing responses from the ISY; device may be busy, trying again later"
|
||||
)
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
_categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier)
|
||||
_categorize_programs(hass_isy_data, isy.programs)
|
||||
|
@ -181,13 +207,21 @@ async def async_setup_entry(
|
|||
def _start_auto_update() -> None:
|
||||
"""Start isy auto update."""
|
||||
_LOGGER.debug("ISY Starting Event Stream and automatic updates")
|
||||
isy.auto_update = True
|
||||
isy.websocket.start()
|
||||
|
||||
def _stop_auto_update(event) -> None:
|
||||
"""Stop the isy auto update on Home Assistant Shutdown."""
|
||||
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
|
||||
isy.websocket.stop()
|
||||
|
||||
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
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_auto_update)
|
||||
)
|
||||
|
||||
# Register Integration-wide Services:
|
||||
async_setup_services(hass)
|
||||
|
@ -248,9 +282,9 @@ async def async_unload_entry(
|
|||
isy = hass_isy_data[ISY994_ISY]
|
||||
|
||||
def _stop_auto_update() -> None:
|
||||
"""Start isy auto update."""
|
||||
"""Stop the isy auto update."""
|
||||
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
|
||||
isy.auto_update = False
|
||||
isy.websocket.stop()
|
||||
|
||||
await hass.async_add_executor_job(_stop_auto_update)
|
||||
|
||||
|
|
|
@ -251,11 +251,11 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
|
|||
"""Subscribe to the node and subnode event emitters."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._node.control_events.subscribe(self._positive_node_control_handler)
|
||||
self._node.control_events.subscribe(self._async_positive_node_control_handler)
|
||||
|
||||
if self._negative_node is not None:
|
||||
self._negative_node.control_events.subscribe(
|
||||
self._negative_node_control_handler
|
||||
self._async_negative_node_control_handler
|
||||
)
|
||||
|
||||
def add_heartbeat_device(self, device) -> None:
|
||||
|
@ -267,10 +267,10 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
|
|||
"""
|
||||
self._heartbeat_device = device
|
||||
|
||||
def _heartbeat(self) -> None:
|
||||
def _async_heartbeat(self) -> None:
|
||||
"""Send a heartbeat to our heartbeat device, if we have one."""
|
||||
if self._heartbeat_device is not None:
|
||||
self._heartbeat_device.heartbeat()
|
||||
self._heartbeat_device.async_heartbeat()
|
||||
|
||||
def add_negative_node(self, child) -> None:
|
||||
"""Add a negative node to this binary sensor device.
|
||||
|
@ -292,7 +292,8 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
|
|||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
|
||||
def _negative_node_control_handler(self, event: object) -> None:
|
||||
@callback
|
||||
def _async_negative_node_control_handler(self, event: object) -> None:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
if event.control == CMD_ON:
|
||||
_LOGGER.debug(
|
||||
|
@ -300,10 +301,11 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
|
|||
self.name,
|
||||
)
|
||||
self._computed_state = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
self.async_write_ha_state()
|
||||
self._async_heartbeat()
|
||||
|
||||
def _positive_node_control_handler(self, event: object) -> None:
|
||||
@callback
|
||||
def _async_positive_node_control_handler(self, event: object) -> None:
|
||||
"""Handle On and Off control event coming from the primary node.
|
||||
|
||||
Depending on device configuration, sometimes only On events
|
||||
|
@ -316,18 +318,19 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
|
|||
self.name,
|
||||
)
|
||||
self._computed_state = True
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
self.async_write_ha_state()
|
||||
self._async_heartbeat()
|
||||
if event.control == CMD_OFF:
|
||||
_LOGGER.debug(
|
||||
"Sensor %s turning Off via the Primary node sending a DOF command",
|
||||
self.name,
|
||||
)
|
||||
self._computed_state = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
self.async_write_ha_state()
|
||||
self._async_heartbeat()
|
||||
|
||||
def on_update(self, event: object) -> None:
|
||||
@callback
|
||||
def async_on_update(self, event: object) -> None:
|
||||
"""Primary node status updates.
|
||||
|
||||
We MOSTLY ignore these updates, as we listen directly to the Control
|
||||
|
@ -340,8 +343,8 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
|
|||
if self._status_was_unknown and self._computed_state is None:
|
||||
self._computed_state = bool(self._node.status)
|
||||
self._status_was_unknown = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
self.async_write_ha_state()
|
||||
self._async_heartbeat()
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
@ -395,9 +398,10 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
|
|||
The ISY uses both DON and DOF commands (alternating) for a heartbeat.
|
||||
"""
|
||||
if event.control in [CMD_ON, CMD_OFF]:
|
||||
self.heartbeat()
|
||||
self.async_heartbeat()
|
||||
|
||||
def heartbeat(self):
|
||||
@callback
|
||||
def async_heartbeat(self):
|
||||
"""Mark the device as online, and restart the 25 hour timer.
|
||||
|
||||
This gets called when the heartbeat node beats, but also when the
|
||||
|
@ -407,7 +411,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
|
|||
"""
|
||||
self._computed_state = False
|
||||
self._restart_timer()
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _restart_timer(self):
|
||||
"""Restart the 25 hour timer."""
|
||||
|
@ -423,7 +427,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
|
|||
"""Heartbeat missed; set state to ON to indicate dead battery."""
|
||||
self._computed_state = True
|
||||
self._heartbeat_timer = None
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
point_in_time = dt_util.utcnow() + timedelta(hours=25)
|
||||
_LOGGER.debug(
|
||||
|
@ -436,7 +440,8 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
|
|||
self.hass, timer_elapsed, point_in_time
|
||||
)
|
||||
|
||||
def on_update(self, event: object) -> None:
|
||||
@callback
|
||||
def async_on_update(self, event: object) -> None:
|
||||
"""Ignore node status updates.
|
||||
|
||||
We listen directly to the Control events for this device.
|
||||
|
|
|
@ -203,7 +203,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
|
|||
return None
|
||||
return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value)
|
||||
|
||||
def set_temperature(self, **kwargs) -> None:
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
|
@ -214,27 +214,27 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
|
|||
if self.hvac_mode == HVAC_MODE_HEAT:
|
||||
target_temp_low = target_temp
|
||||
if target_temp_low is not None:
|
||||
self._node.set_climate_setpoint_heat(int(target_temp_low))
|
||||
await self._node.set_climate_setpoint_heat(int(target_temp_low))
|
||||
# Presumptive setting--event stream will correct if cmd fails:
|
||||
self._target_temp_low = target_temp_low
|
||||
if target_temp_high is not None:
|
||||
self._node.set_climate_setpoint_cool(int(target_temp_high))
|
||||
await self._node.set_climate_setpoint_cool(int(target_temp_high))
|
||||
# Presumptive setting--event stream will correct if cmd fails:
|
||||
self._target_temp_high = target_temp_high
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan_mode: str) -> None:
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
_LOGGER.debug("Requested fan mode %s", fan_mode)
|
||||
self._node.set_fan_mode(HA_FAN_TO_ISY.get(fan_mode))
|
||||
await self._node.set_fan_mode(HA_FAN_TO_ISY.get(fan_mode))
|
||||
# Presumptive setting--event stream will correct if cmd fails:
|
||||
self._fan_mode = fan_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
_LOGGER.debug("Requested operation mode %s", hvac_mode)
|
||||
self._node.set_climate_mode(HA_HVAC_TO_ISY.get(hvac_mode))
|
||||
await self._node.set_climate_mode(HA_HVAC_TO_ISY.get(hvac_mode))
|
||||
# Presumptive setting--event stream will correct if cmd fails:
|
||||
self._hvac_mode = hvac_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from aiohttp import CookieJar
|
||||
import async_timeout
|
||||
from pyisy import ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError
|
||||
from pyisy.configuration import Configuration
|
||||
from pyisy.connection import Connection
|
||||
import voluptuous as vol
|
||||
|
@ -11,6 +14,7 @@ from homeassistant.components import ssdp
|
|||
from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import (
|
||||
CONF_IGNORE_STRING,
|
||||
|
@ -57,25 +61,41 @@ async def validate_input(hass: core.HomeAssistant, data):
|
|||
if host.scheme == "http":
|
||||
https = False
|
||||
port = host.port or 80
|
||||
session = aiohttp_client.async_create_clientsession(
|
||||
hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True)
|
||||
)
|
||||
elif host.scheme == "https":
|
||||
https = True
|
||||
port = host.port or 443
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
else:
|
||||
_LOGGER.error("The isy994 host value in configuration is invalid")
|
||||
raise InvalidHost
|
||||
|
||||
# Connect to ISY controller.
|
||||
isy_conf = await hass.async_add_executor_job(
|
||||
_fetch_isy_configuration,
|
||||
isy_conn = Connection(
|
||||
host.hostname,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
https,
|
||||
tls_version,
|
||||
host.path,
|
||||
use_https=https,
|
||||
tls_ver=tls_version,
|
||||
webroot=host.path,
|
||||
websession=session,
|
||||
)
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(30):
|
||||
isy_conf_xml = await isy_conn.test_connection()
|
||||
except ISYInvalidAuthError as error:
|
||||
raise InvalidAuth from error
|
||||
except ISYConnectionError as error:
|
||||
raise CannotConnect from error
|
||||
|
||||
try:
|
||||
isy_conf = Configuration(xml=isy_conf_xml)
|
||||
except ISYResponseParseError as error:
|
||||
raise CannotConnect from error
|
||||
if not isy_conf or "name" not in isy_conf or not isy_conf["name"]:
|
||||
raise CannotConnect
|
||||
|
||||
|
@ -83,26 +103,6 @@ async def validate_input(hass: core.HomeAssistant, data):
|
|||
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,
|
||||
webroot=webroot,
|
||||
)
|
||||
except ValueError as err:
|
||||
raise InvalidAuth(err.args[0]) from err
|
||||
|
||||
return Configuration(xml=isy_conn.get_config())
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Universal Devices ISY994."""
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@ KEY_ACTIONS = "actions"
|
|||
KEY_STATUS = "status"
|
||||
|
||||
PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE]
|
||||
SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH]
|
||||
PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH]
|
||||
|
||||
SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"]
|
||||
|
||||
|
@ -184,6 +184,7 @@ UNDO_UPDATE_LISTENER = "undo_update_listener"
|
|||
# Used for discovery
|
||||
UDN_UUID_PREFIX = "uuid:"
|
||||
ISY_URL_POSTFIX = "/desc"
|
||||
EVENTS_SUFFIX = "_ISYSUB"
|
||||
|
||||
# Special Units of Measure
|
||||
UOM_ISYV4_DEGREES = "degrees"
|
||||
|
@ -352,7 +353,7 @@ UOM_FRIENDLY_NAME = {
|
|||
"22": "%RH",
|
||||
"23": PRESSURE_INHG,
|
||||
"24": SPEED_INCHES_PER_HOUR,
|
||||
UOM_INDEX: "index", # Index type. Use "node.formatted" for value
|
||||
UOM_INDEX: UOM_INDEX, # Index type. Use "node.formatted" for value
|
||||
"26": TEMP_KELVIN,
|
||||
"27": "keyword",
|
||||
"28": MASS_KILOGRAMS,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Support for ISY994 covers."""
|
||||
|
||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
|
@ -67,23 +68,23 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity):
|
|||
"""Flag supported features."""
|
||||
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||
|
||||
def open_cover(self, **kwargs) -> None:
|
||||
async def async_open_cover(self, **kwargs) -> None:
|
||||
"""Send the open cover command to the ISY994 cover device."""
|
||||
val = 100 if self._node.uom == UOM_BARRIER else None
|
||||
if not self._node.turn_on(val=val):
|
||||
if not await self._node.turn_on(val=val):
|
||||
_LOGGER.error("Unable to open the cover")
|
||||
|
||||
def close_cover(self, **kwargs) -> None:
|
||||
async def async_close_cover(self, **kwargs) -> None:
|
||||
"""Send the close cover command to the ISY994 cover device."""
|
||||
if not self._node.turn_off():
|
||||
if not await self._node.turn_off():
|
||||
_LOGGER.error("Unable to close the cover")
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
if self._node.uom == UOM_8_BIT_RANGE:
|
||||
position = round(position * 255.0 / 100.0)
|
||||
if not self._node.turn_on(val=position):
|
||||
if not await self._node.turn_on(val=position):
|
||||
_LOGGER.error("Unable to set cover position")
|
||||
|
||||
|
||||
|
@ -95,12 +96,12 @@ class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity):
|
|||
"""Get whether the ISY994 cover program is closed."""
|
||||
return bool(self._node.status)
|
||||
|
||||
def open_cover(self, **kwargs) -> None:
|
||||
async def async_open_cover(self, **kwargs) -> None:
|
||||
"""Send the open cover command to the ISY994 cover program."""
|
||||
if not self._actions.run_then():
|
||||
if not await self._actions.run_then():
|
||||
_LOGGER.error("Unable to open the cover")
|
||||
|
||||
def close_cover(self, **kwargs) -> None:
|
||||
async def async_close_cover(self, **kwargs) -> None:
|
||||
"""Send the close cover command to the ISY994 cover program."""
|
||||
if not self._actions.run_else():
|
||||
if not await self._actions.run_else():
|
||||
_LOGGER.error("Unable to close the cover")
|
||||
|
|
|
@ -11,9 +11,11 @@ from pyisy.constants import (
|
|||
from pyisy.helpers import NodeProperty
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class ISYEntity(Entity):
|
||||
|
@ -30,16 +32,20 @@ class ISYEntity(Entity):
|
|||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node change events."""
|
||||
self._change_handler = self._node.status_events.subscribe(self.on_update)
|
||||
self._change_handler = self._node.status_events.subscribe(self.async_on_update)
|
||||
|
||||
if hasattr(self._node, "control_events"):
|
||||
self._control_handler = self._node.control_events.subscribe(self.on_control)
|
||||
self._control_handler = self._node.control_events.subscribe(
|
||||
self.async_on_control
|
||||
)
|
||||
|
||||
def on_update(self, event: object) -> None:
|
||||
@callback
|
||||
def async_on_update(self, event: object) -> None:
|
||||
"""Handle the update event from the ISY994 Node."""
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def on_control(self, event: NodeProperty) -> None:
|
||||
@callback
|
||||
def async_on_control(self, event: NodeProperty) -> None:
|
||||
"""Handle a control event from the ISY994 Node."""
|
||||
event_data = {
|
||||
"entity_id": self.entity_id,
|
||||
|
@ -52,7 +58,7 @@ class ISYEntity(Entity):
|
|||
|
||||
if event.control not in EVENT_PROPS_IGNORED:
|
||||
# New state attributes may be available, update the state.
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.hass.bus.fire("isy994_control", event_data)
|
||||
|
||||
|
@ -99,9 +105,9 @@ class ISYEntity(Entity):
|
|||
f"ProductTypeID:{node.zwave_props.prod_type_id} "
|
||||
f"ProductID:{node.zwave_props.product_id}"
|
||||
)
|
||||
# Note: sw_version is not exposed by the ISY for the individual devices.
|
||||
if hasattr(node, "folder") and node.folder is not None:
|
||||
device_info["suggested_area"] = node.folder
|
||||
# Note: sw_version is not exposed by the ISY for the individual devices.
|
||||
|
||||
return device_info
|
||||
|
||||
|
@ -155,25 +161,23 @@ class ISYNodeEntity(ISYEntity):
|
|||
self._attrs.update(attr)
|
||||
return self._attrs
|
||||
|
||||
def send_node_command(self, command):
|
||||
async def async_send_node_command(self, command):
|
||||
"""Respond to an entity service command call."""
|
||||
if not hasattr(self._node, command):
|
||||
_LOGGER.error(
|
||||
"Invalid Service Call %s for device %s", command, self.entity_id
|
||||
raise HomeAssistantError(
|
||||
f"Invalid service call: {command} for device {self.entity_id}"
|
||||
)
|
||||
return
|
||||
getattr(self._node, command)()
|
||||
await getattr(self._node, command)()
|
||||
|
||||
def send_raw_node_command(
|
||||
async def async_send_raw_node_command(
|
||||
self, command, value=None, unit_of_measurement=None, parameters=None
|
||||
):
|
||||
"""Respond to an entity service raw command call."""
|
||||
if not hasattr(self._node, "send_cmd"):
|
||||
_LOGGER.error(
|
||||
"Invalid Service Call %s for device %s", command, self.entity_id
|
||||
raise HomeAssistantError(
|
||||
f"Invalid service call: {command} for device {self.entity_id}"
|
||||
)
|
||||
return
|
||||
self._node.send_cmd(command, value, unit_of_measurement, parameters)
|
||||
await self._node.send_cmd(command, value, unit_of_measurement, parameters)
|
||||
|
||||
|
||||
class ISYProgramEntity(ISYEntity):
|
||||
|
|
|
@ -65,17 +65,17 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
|
|||
return None
|
||||
return self._node.status != 0
|
||||
|
||||
def set_percentage(self, percentage: int) -> None:
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set node to speed percentage for the ISY994 fan device."""
|
||||
if percentage == 0:
|
||||
self._node.turn_off()
|
||||
await self._node.turn_off()
|
||||
return
|
||||
|
||||
isy_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
|
||||
|
||||
self._node.turn_on(val=isy_speed)
|
||||
await self._node.turn_on(val=isy_speed)
|
||||
|
||||
def turn_on(
|
||||
async def async_turn_on(
|
||||
self,
|
||||
speed: str = None,
|
||||
percentage: int = None,
|
||||
|
@ -83,11 +83,11 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
|
|||
**kwargs,
|
||||
) -> None:
|
||||
"""Send the turn on command to the ISY994 fan device."""
|
||||
self.set_percentage(percentage)
|
||||
await self.async_set_percentage(percentage)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 fan device."""
|
||||
self._node.turn_off()
|
||||
await self._node.turn_off()
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
|
@ -108,8 +108,6 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
|
|||
@property
|
||||
def speed_count(self) -> int:
|
||||
"""Return the number of speeds the fan supports."""
|
||||
if self._node.protocol == PROTO_INSTEON:
|
||||
return 3
|
||||
return int_states_in_range(SPEED_RANGE)
|
||||
|
||||
@property
|
||||
|
@ -117,12 +115,12 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
|
|||
"""Get if the fan is on."""
|
||||
return self._node.status != 0
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn on command to ISY994 fan program."""
|
||||
if not self._actions.run_then():
|
||||
if not await self._actions.run_then():
|
||||
_LOGGER.error("Unable to turn off the fan")
|
||||
|
||||
def turn_on(
|
||||
async def async_turn_on(
|
||||
self,
|
||||
speed: str = None,
|
||||
percentage: int = None,
|
||||
|
@ -130,5 +128,5 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
|
|||
**kwargs,
|
||||
) -> None:
|
||||
"""Send the turn off command to ISY994 fan program."""
|
||||
if not self._actions.run_else():
|
||||
if not await self._actions.run_else():
|
||||
_LOGGER.error("Unable to turn on the fan")
|
||||
|
|
|
@ -41,12 +41,12 @@ from .const import (
|
|||
KEY_STATUS,
|
||||
NODE_FILTERS,
|
||||
PLATFORMS,
|
||||
PROGRAM_PLATFORMS,
|
||||
SUBNODE_CLIMATE_COOL,
|
||||
SUBNODE_CLIMATE_HEAT,
|
||||
SUBNODE_EZIO2X4_SENSORS,
|
||||
SUBNODE_FANLINC_LIGHT,
|
||||
SUBNODE_IOLINC_RELAY,
|
||||
SUPPORTED_PROGRAM_PLATFORMS,
|
||||
TYPE_CATEGORY_SENSOR_ACTUATORS,
|
||||
TYPE_EZIO2X4,
|
||||
UOM_DOUBLE_TEMP,
|
||||
|
@ -167,7 +167,6 @@ def _check_for_zwave_cat(
|
|||
device_type.startswith(t)
|
||||
for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT])
|
||||
):
|
||||
|
||||
hass_isy_data[ISY994_NODES][platform].append(node)
|
||||
return True
|
||||
|
||||
|
@ -314,7 +313,7 @@ def _categorize_nodes(
|
|||
|
||||
def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
|
||||
"""Categorize the ISY994 programs."""
|
||||
for platform in SUPPORTED_PROGRAM_PLATFORMS:
|
||||
for platform in PROGRAM_PLATFORMS:
|
||||
folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}")
|
||||
if not folder:
|
||||
continue
|
||||
|
|
|
@ -9,7 +9,7 @@ from homeassistant.components.light import (
|
|||
LightEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
|
@ -72,30 +72,32 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
|||
return round(self._node.status * 255.0 / 100.0)
|
||||
return int(self._node.status)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 light device."""
|
||||
self._last_brightness = self.brightness
|
||||
if not self._node.turn_off():
|
||||
if not await self._node.turn_off():
|
||||
_LOGGER.debug("Unable to turn off light")
|
||||
|
||||
def on_update(self, event: object) -> None:
|
||||
@callback
|
||||
def async_on_update(self, event: object) -> None:
|
||||
"""Save brightness in the update event from the ISY994 Node."""
|
||||
if self._node.status not in (0, ISY_VALUE_UNKNOWN):
|
||||
self._last_brightness = self._node.status
|
||||
if self._node.uom == UOM_PERCENTAGE:
|
||||
self._last_brightness = round(self._node.status * 255.0 / 100.0)
|
||||
else:
|
||||
self._last_brightness = self._node.status
|
||||
super().on_update(event)
|
||||
super().async_on_update(event)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def turn_on(self, brightness=None, **kwargs) -> None:
|
||||
async def async_turn_on(self, brightness=None, **kwargs) -> None:
|
||||
"""Send the turn on command to the ISY994 light device."""
|
||||
if self._restore_light_state and brightness is None and self._last_brightness:
|
||||
brightness = self._last_brightness
|
||||
# Special Case for ISY Z-Wave Devices using % instead of 0-255:
|
||||
if brightness is not None and self._node.uom == UOM_PERCENTAGE:
|
||||
brightness = round(brightness * 100.0 / 255.0)
|
||||
if not self._node.turn_on(val=brightness):
|
||||
if not await self._node.turn_on(val=brightness):
|
||||
_LOGGER.debug("Unable to turn on light")
|
||||
|
||||
@property
|
||||
|
@ -125,10 +127,10 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
|||
):
|
||||
self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS]
|
||||
|
||||
def set_on_level(self, value):
|
||||
async def async_set_on_level(self, value):
|
||||
"""Set the ON Level for a device."""
|
||||
self._node.set_on_level(value)
|
||||
await self._node.set_on_level(value)
|
||||
|
||||
def set_ramp_rate(self, value):
|
||||
async def async_set_ramp_rate(self, value):
|
||||
"""Set the Ramp Rate for a device."""
|
||||
self._node.set_ramp_rate(value)
|
||||
await self._node.set_ramp_rate(value)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Support for ISY994 locks."""
|
||||
|
||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||
|
||||
from homeassistant.components.lock import DOMAIN as LOCK, LockEntity
|
||||
|
@ -41,14 +42,14 @@ class ISYLockEntity(ISYNodeEntity, LockEntity):
|
|||
return None
|
||||
return VALUE_TO_STATE.get(self._node.status)
|
||||
|
||||
def lock(self, **kwargs) -> None:
|
||||
async def async_lock(self, **kwargs) -> None:
|
||||
"""Send the lock command to the ISY994 device."""
|
||||
if not self._node.secure_lock():
|
||||
if not await self._node.secure_lock():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
|
||||
def unlock(self, **kwargs) -> None:
|
||||
async def async_unlock(self, **kwargs) -> None:
|
||||
"""Send the unlock command to the ISY994 device."""
|
||||
if not self._node.secure_unlock():
|
||||
if not await self._node.secure_unlock():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
|
||||
|
||||
|
@ -60,12 +61,12 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
|
|||
"""Return true if the device is locked."""
|
||||
return bool(self._node.status)
|
||||
|
||||
def lock(self, **kwargs) -> None:
|
||||
async def async_lock(self, **kwargs) -> None:
|
||||
"""Lock the device."""
|
||||
if not self._actions.run_then():
|
||||
if not await self._actions.run_then():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
|
||||
def unlock(self, **kwargs) -> None:
|
||||
async def async_unlock(self, **kwargs) -> None:
|
||||
"""Unlock the device."""
|
||||
if not self._actions.run_else():
|
||||
if not await self._actions.run_else():
|
||||
_LOGGER.error("Unable to unlock device")
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "isy994",
|
||||
"name": "Universal Devices ISY994",
|
||||
"documentation": "https://www.home-assistant.io/integrations/isy994",
|
||||
"requirements": ["pyisy==2.1.1"],
|
||||
"requirements": ["pyisy==3.0.0"],
|
||||
"codeowners": ["@bdraco", "@shbatm"],
|
||||
"config_flow": true,
|
||||
"ssdp": [
|
||||
|
@ -11,8 +11,6 @@
|
|||
"deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1"
|
||||
}
|
||||
],
|
||||
"dhcp": [
|
||||
{"hostname":"isy*", "macaddress":"0021B9*"}
|
||||
],
|
||||
"dhcp": [{ "hostname": "isy*", "macaddress": "0021B9*" }],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
|
|
@ -83,6 +83,10 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
|
|||
if uom in [UOM_INDEX, UOM_ON_OFF]:
|
||||
return self._node.formatted
|
||||
|
||||
# Check if this is an index type and get formatted value
|
||||
if uom == UOM_INDEX and hasattr(self._node, "formatted"):
|
||||
return self._node.formatted
|
||||
|
||||
# Handle ISY precision and rounding
|
||||
value = convert_isy_value_to_hass(value, uom, self._node.prec)
|
||||
|
||||
|
@ -123,7 +127,8 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity):
|
|||
return {
|
||||
"init_value": convert_isy_value_to_hass(
|
||||
self._node.init, "", self._node.prec
|
||||
)
|
||||
),
|
||||
"last_edited": self._node.last_edited,
|
||||
}
|
||||
|
||||
@property
|
||||
|
|
|
@ -27,7 +27,7 @@ from .const import (
|
|||
ISY994_PROGRAMS,
|
||||
ISY994_VARIABLES,
|
||||
PLATFORMS,
|
||||
SUPPORTED_PROGRAM_PLATFORMS,
|
||||
PROGRAM_PLATFORMS,
|
||||
)
|
||||
|
||||
# Common Services for All Platforms:
|
||||
|
@ -183,12 +183,12 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
|
|||
address,
|
||||
isy.configuration["uuid"],
|
||||
)
|
||||
await hass.async_add_executor_job(isy.query, address)
|
||||
await isy.query(address)
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"Requesting system query of ISY %s", isy.configuration["uuid"]
|
||||
)
|
||||
await hass.async_add_executor_job(isy.query)
|
||||
await isy.query()
|
||||
|
||||
async def async_run_network_resource_service_handler(service):
|
||||
"""Handle a network resource service call."""
|
||||
|
@ -208,10 +208,10 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
|
|||
if name:
|
||||
command = isy.networking.get_by_name(name)
|
||||
if command is not None:
|
||||
await hass.async_add_executor_job(command.run)
|
||||
await command.run()
|
||||
return
|
||||
_LOGGER.error(
|
||||
"Could not run network resource command. Not found or enabled on the ISY"
|
||||
"Could not run network resource command; not found or enabled on the ISY"
|
||||
)
|
||||
|
||||
async def async_send_program_command_service_handler(service):
|
||||
|
@ -231,9 +231,9 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
|
|||
if name:
|
||||
program = isy.programs.get_by_name(name)
|
||||
if program is not None:
|
||||
await hass.async_add_executor_job(getattr(program, command))
|
||||
await getattr(program, command)()
|
||||
return
|
||||
_LOGGER.error("Could not send program command. Not found or enabled on the ISY")
|
||||
_LOGGER.error("Could not send program command; not found or enabled on the ISY")
|
||||
|
||||
async def async_set_variable_service_handler(service):
|
||||
"""Handle a set variable service call."""
|
||||
|
@ -254,9 +254,9 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
|
|||
if address and vtype:
|
||||
variable = isy.variables.vobjs[vtype].get(address)
|
||||
if variable is not None:
|
||||
await hass.async_add_executor_job(variable.set_value, value, init)
|
||||
await variable.set_value(value, init)
|
||||
return
|
||||
_LOGGER.error("Could not set variable value. Not found or enabled on the ISY")
|
||||
_LOGGER.error("Could not set variable value; not found or enabled on the ISY")
|
||||
|
||||
async def async_cleanup_registry_entries(service) -> None:
|
||||
"""Remove extra entities that are no longer part of the integration."""
|
||||
|
@ -283,7 +283,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
|
|||
if hasattr(node, "address"):
|
||||
current_unique_ids.append(f"{uuid}_{node.address}")
|
||||
|
||||
for platform in SUPPORTED_PROGRAM_PLATFORMS:
|
||||
for platform in PROGRAM_PLATFORMS:
|
||||
for _, node, _ in hass_isy_data[ISY994_PROGRAMS][platform]:
|
||||
if hasattr(node, "address"):
|
||||
current_unique_ids.append(f"{uuid}_{node.address}")
|
||||
|
@ -355,7 +355,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
|
|||
|
||||
async def _async_send_raw_node_command(call: ServiceCall):
|
||||
await hass.helpers.service.entity_service_call(
|
||||
async_get_platforms(hass, DOMAIN), SERVICE_SEND_RAW_NODE_COMMAND, call
|
||||
async_get_platforms(hass, DOMAIN), "async_send_raw_node_command", call
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
|
@ -367,7 +367,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
|
|||
|
||||
async def _async_send_node_command(call: ServiceCall):
|
||||
await hass.helpers.service.entity_service_call(
|
||||
async_get_platforms(hass, DOMAIN), SERVICE_SEND_NODE_COMMAND, call
|
||||
async_get_platforms(hass, DOMAIN), "async_send_node_command", call
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
|
@ -408,8 +408,8 @@ def async_setup_light_services(hass: HomeAssistant):
|
|||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_ON_LEVEL, SERVICE_SET_VALUE_SCHEMA, SERVICE_SET_ON_LEVEL
|
||||
SERVICE_SET_ON_LEVEL, SERVICE_SET_VALUE_SCHEMA, "async_set_on_level"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, SERVICE_SET_RAMP_RATE
|
||||
SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, "async_set_ramp_rate"
|
||||
)
|
||||
|
|
|
@ -57,19 +57,19 @@ send_node_command:
|
|||
selector:
|
||||
select:
|
||||
options:
|
||||
- 'beep'
|
||||
- 'brighten'
|
||||
- 'dim'
|
||||
- 'disable'
|
||||
- 'enable'
|
||||
- 'fade_down'
|
||||
- 'fade_stop'
|
||||
- 'fade_up'
|
||||
- 'fast_off'
|
||||
- 'fast_on'
|
||||
- 'query'
|
||||
- "beep"
|
||||
- "brighten"
|
||||
- "dim"
|
||||
- "disable"
|
||||
- "enable"
|
||||
- "fade_down"
|
||||
- "fade_stop"
|
||||
- "fade_up"
|
||||
- "fast_off"
|
||||
- "fast_on"
|
||||
- "query"
|
||||
set_on_level:
|
||||
name: Set on level
|
||||
name: Set On Level
|
||||
description: Send a ISY set_on_level command to a Node.
|
||||
target:
|
||||
entity:
|
||||
|
@ -188,14 +188,14 @@ send_program_command:
|
|||
selector:
|
||||
select:
|
||||
options:
|
||||
- 'disable'
|
||||
- 'disable_run_at_startup'
|
||||
- 'enable'
|
||||
- 'enable_run_at_startup'
|
||||
- 'run'
|
||||
- 'run_else'
|
||||
- 'run_then'
|
||||
- 'stop'
|
||||
- "disable"
|
||||
- "disable_run_at_startup"
|
||||
- "enable"
|
||||
- "enable_run_at_startup"
|
||||
- "run"
|
||||
- "run_else"
|
||||
- "run_then"
|
||||
- "stop"
|
||||
isy:
|
||||
name: ISY
|
||||
description: If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same program name or address on multiple ISYs, omitting this will run the command on them all.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Support for ISY994 switches."""
|
||||
|
||||
from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity
|
||||
|
@ -39,14 +40,14 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):
|
|||
return None
|
||||
return bool(self._node.status)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 switch."""
|
||||
if not self._node.turn_off():
|
||||
if not await self._node.turn_off():
|
||||
_LOGGER.debug("Unable to turn off switch")
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Send the turn on command to the ISY994 switch."""
|
||||
if not self._node.turn_on():
|
||||
if not await self._node.turn_on():
|
||||
_LOGGER.debug("Unable to turn on switch")
|
||||
|
||||
@property
|
||||
|
@ -65,14 +66,14 @@ class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity):
|
|||
"""Get whether the ISY994 switch program is on."""
|
||||
return bool(self._node.status)
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Send the turn on command to the ISY994 switch program."""
|
||||
if not self._actions.run_then():
|
||||
if not await self._actions.run_then():
|
||||
_LOGGER.error("Unable to turn on switch")
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 switch program."""
|
||||
if not self._actions.run_else():
|
||||
if not await self._actions.run_else():
|
||||
_LOGGER.error("Unable to turn off switch")
|
||||
|
||||
@property
|
||||
|
|
|
@ -1491,7 +1491,7 @@ pyirishrail==0.0.2
|
|||
pyiss==1.0.1
|
||||
|
||||
# homeassistant.components.isy994
|
||||
pyisy==2.1.1
|
||||
pyisy==3.0.0
|
||||
|
||||
# homeassistant.components.itach
|
||||
pyitachip2ir==0.0.7
|
||||
|
|
|
@ -826,7 +826,7 @@ pyipp==0.11.0
|
|||
pyiqvia==0.3.1
|
||||
|
||||
# homeassistant.components.isy994
|
||||
pyisy==2.1.1
|
||||
pyisy==3.0.0
|
||||
|
||||
# homeassistant.components.kira
|
||||
pykira==0.1.1
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
"""Test the Universal Devices ISY994 config flow."""
|
||||
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyisy import ISYConnectionError, ISYInvalidAuthError
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow, setup
|
||||
from homeassistant.components import dhcp, ssdp
|
||||
from homeassistant.components.isy994.config_flow import CannotConnect
|
||||
from homeassistant.components.isy994.const import (
|
||||
CONF_IGNORE_STRING,
|
||||
CONF_RESTORE_LIGHT_STATE,
|
||||
|
@ -63,12 +64,30 @@ MOCK_IMPORT_FULL_CONFIG = {
|
|||
MOCK_DEVICE_NAME = "Name of the device"
|
||||
MOCK_UUID = "ce:fb:72:31:b7:b9"
|
||||
MOCK_MAC = "cefb7231b7b9"
|
||||
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"
|
||||
MOCK_CONFIG_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<app_full_version>5.0.16C</app_full_version>
|
||||
<platform>ISY-C-994</platform>
|
||||
<root>
|
||||
<id>ce:fb:72:31:b7:b9</id>
|
||||
<name>Name of the device</name>
|
||||
</root>
|
||||
<features>
|
||||
<feature>
|
||||
<id>21040</id>
|
||||
<desc>Networking Module</desc>
|
||||
<isInstalled>true</isInstalled>
|
||||
<isAvailable>true</isAvailable>
|
||||
</feature>
|
||||
</features>
|
||||
</configuration>
|
||||
"""
|
||||
|
||||
INTEGRATION = "homeassistant.components.isy994"
|
||||
PATCH_CONNECTION = f"{INTEGRATION}.config_flow.Connection.test_connection"
|
||||
PATCH_ASYNC_SETUP = f"{INTEGRATION}.async_setup"
|
||||
PATCH_ASYNC_SETUP_ENTRY = f"{INTEGRATION}.async_setup_entry"
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant):
|
||||
|
@ -80,17 +99,12 @@ async def test_form(hass: HomeAssistant):
|
|||
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(
|
||||
with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), 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 = None
|
||||
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_INPUT,
|
||||
|
@ -129,9 +143,9 @@ async def test_form_invalid_auth(hass: HomeAssistant):
|
|||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
with patch(PATCH_CONFIGURATION), patch(
|
||||
with patch(
|
||||
PATCH_CONNECTION,
|
||||
side_effect=ValueError("PyISY could not connect to the ISY."),
|
||||
side_effect=ISYInvalidAuthError(),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
|
@ -142,14 +156,52 @@ async def test_form_invalid_auth(hass: HomeAssistant):
|
|||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant):
|
||||
"""Test we handle cannot connect error."""
|
||||
async def test_form_isy_connection_error(hass: HomeAssistant):
|
||||
"""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(
|
||||
with patch(
|
||||
PATCH_CONNECTION,
|
||||
side_effect=CannotConnect,
|
||||
side_effect=ISYConnectionError(),
|
||||
):
|
||||
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_isy_parse_response_error(hass: HomeAssistant, caplog):
|
||||
"""Test we handle poorly formatted XML response from ISY."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
with patch(
|
||||
PATCH_CONNECTION,
|
||||
return_value=MOCK_CONFIG_RESPONSE.rsplit("\n", 3)[0], # Test with invalid XML
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_INPUT,
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert "ISY Could not parse response, poorly formatted XML." in caplog.text
|
||||
|
||||
|
||||
async def test_form_no_name_in_response(hass: HomeAssistant):
|
||||
"""Test we handle invalid response from ISY with name not set."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
with patch(
|
||||
PATCH_CONNECTION,
|
||||
return_value=re.sub(
|
||||
r"\<name\>.*\n", "", MOCK_CONFIG_RESPONSE
|
||||
), # Test with <name> line removed.
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
|
@ -170,12 +222,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant):
|
|||
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 = None
|
||||
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||
with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_INPUT,
|
||||
|
@ -185,15 +232,12 @@ async def test_form_existing_config_entry(hass: HomeAssistant):
|
|||
|
||||
async def test_import_flow_some_fields(hass: HomeAssistant) -> 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(
|
||||
with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), 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 = None
|
||||
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
|
@ -209,15 +253,12 @@ async def test_import_flow_some_fields(hass: HomeAssistant) -> None:
|
|||
async def test_import_flow_with_https(hass: HomeAssistant) -> None:
|
||||
"""Test import config with https."""
|
||||
|
||||
with patch(PATCH_CONFIGURATION) as mock_config_class, patch(
|
||||
PATCH_CONNECTION
|
||||
) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch(
|
||||
with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), 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 = None
|
||||
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
|
@ -232,15 +273,12 @@ async def test_import_flow_with_https(hass: HomeAssistant) -> None:
|
|||
|
||||
async def test_import_flow_all_fields(hass: HomeAssistant) -> 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(
|
||||
with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), 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 = None
|
||||
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
|
@ -297,17 +335,12 @@ async def test_form_ssdp(hass: HomeAssistant):
|
|||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(PATCH_CONFIGURATION) as mock_config_class, patch(
|
||||
PATCH_CONNECTION
|
||||
) as mock_connection_class, patch(
|
||||
with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), 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 = None
|
||||
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_INPUT,
|
||||
|
@ -339,17 +372,12 @@ async def test_form_dhcp(hass: HomeAssistant):
|
|||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(PATCH_CONFIGURATION) as mock_config_class, patch(
|
||||
PATCH_CONNECTION
|
||||
) as mock_connection_class, patch(
|
||||
with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), 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 = None
|
||||
mock_config_class.return_value = MOCK_VALIDATED_RESPONSE
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_INPUT,
|
||||
|
|
Loading…
Add table
Reference in a new issue