From 775af9d2c5f9b9cfab56879dcc35525ae5d27121 Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 18 May 2021 14:15:47 -0500 Subject: [PATCH] Update PyISY to v3.0.0 and ISY994 to use Async IO (#50806) --- homeassistant/components/isy994/__init__.py | 82 ++++++++--- .../components/isy994/binary_sensor.py | 45 +++--- homeassistant/components/isy994/climate.py | 20 +-- .../components/isy994/config_flow.py | 50 +++---- homeassistant/components/isy994/const.py | 5 +- homeassistant/components/isy994/cover.py | 21 +-- homeassistant/components/isy994/entity.py | 40 ++--- homeassistant/components/isy994/fan.py | 24 ++- homeassistant/components/isy994/helpers.py | 5 +- homeassistant/components/isy994/light.py | 24 +-- homeassistant/components/isy994/lock.py | 17 ++- homeassistant/components/isy994/manifest.json | 6 +- homeassistant/components/isy994/sensor.py | 7 +- homeassistant/components/isy994/services.py | 28 ++-- homeassistant/components/isy994/services.yaml | 40 ++--- homeassistant/components/isy994/switch.py | 17 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/isy994/test_config_flow.py | 138 +++++++++++------- 19 files changed, 325 insertions(+), 248 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 90e114e7023..27d81a671c8 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -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,31 +146,50 @@ 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, - host.hostname, - port, - username=user, - password=password, - use_https=https, - tls_ver=tls_version, - webroot=host.path, - ) + isy = ISY( + host.hostname, + port, + username=user, + password=password, + 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) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index c58c37edb42..4a259dac6d8 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -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. diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 5895a060db2..578fbe2bf21 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -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() diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 248d2d9c520..bd1baa66045 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -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.""" diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 9fdef92c84f..ed40c7eb289 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -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, diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 8bac6f50eb7..ca5432f4456 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -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") diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 25a2dc428a6..6dab5b2ed65 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -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): diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 5d40eaef2a9..73b5bd683ba 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -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") diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 5322c8e0abf..b9b1a71901c 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -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 diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 73bd2f5934f..509fd259830 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -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) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 7e1296d2c86..c00a12d0096 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -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") diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index a2326cbae5f..84f13ae4fc4 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -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" } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 908bbdc72e8..79c5663f964 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -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 diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 6d93b53b912..03ecc3930bb 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -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" ) diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 94d5a3cd89d..c163d78a173 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -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. diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 53056e45c7e..99bf6566b1b 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 120926d5123..db8026faffd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2894eea7c2d..dcca9bcb33f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 51750d42718..e5458a3c96b 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -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 = """ + + 5.0.16C + ISY-C-994 + + ce:fb:72:31:b7:b9 + Name of the device + + + + 21040 + Networking Module + true + true + + + +""" + +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"\.*\n", "", MOCK_CONFIG_RESPONSE + ), # Test with 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,