diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 634f69b49d3..ab491c0a271 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -1,15 +1,16 @@ """Support for Nexia / Trane XL Thermostats.""" -from functools import partial +import asyncio import logging +import aiohttp from nexia.const import BRAND_NEXIA from nexia.home import NexiaHome -from requests.exceptions import ConnectTimeout, HTTPError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import CONF_BRAND, DOMAIN, PLATFORMS @@ -30,31 +31,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: brand = conf.get(CONF_BRAND, BRAND_NEXIA) state_file = hass.config.path(f"nexia_config_{username}.conf") + session = async_get_clientsession(hass) + nexia_home = NexiaHome( + session, + username=username, + password=password, + device_name=hass.config.location_name, + state_file=state_file, + brand=brand, + ) try: - nexia_home = await hass.async_add_executor_job( - partial( - NexiaHome, - username=username, - password=password, - device_name=hass.config.location_name, - state_file=state_file, - brand=brand, - ) - ) - except ConnectTimeout as ex: - _LOGGER.error("Unable to connect to Nexia service: %s", ex) - raise ConfigEntryNotReady from ex - except HTTPError as http_ex: - if is_invalid_auth_code(http_ex.response.status_code): + await nexia_home.login() + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + f"Timed out trying to connect to Nexia service: {ex}" + ) from ex + except aiohttp.ClientResponseError as http_ex: + if is_invalid_auth_code(http_ex.status): _LOGGER.error( "Access error from Nexia service, please check credentials: %s", http_ex ) return False - _LOGGER.error("HTTP error from Nexia service: %s", http_ex) - raise ConfigEntryNotReady from http_ex + raise ConfigEntryNotReady(f"Error from Nexia service: {http_ex}") from http_ex coordinator = NexiaDataUpdateCoordinator(hass, nexia_home) + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 7eeb04c5676..0c27b1497f5 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -123,13 +123,17 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_HUMIDIFY_SETPOINT, SET_HUMIDITY_SCHEMA, - SERVICE_SET_HUMIDIFY_SETPOINT, + f"async_{SERVICE_SET_HUMIDIFY_SETPOINT}", ) platform.async_register_entity_service( - SERVICE_SET_AIRCLEANER_MODE, SET_AIRCLEANER_SCHEMA, SERVICE_SET_AIRCLEANER_MODE + SERVICE_SET_AIRCLEANER_MODE, + SET_AIRCLEANER_SCHEMA, + f"async_{SERVICE_SET_AIRCLEANER_MODE}", ) platform.async_register_entity_service( - SERVICE_SET_HVAC_RUN_MODE, SET_HVAC_RUN_MODE_SCHEMA, SERVICE_SET_HVAC_RUN_MODE + SERVICE_SET_HVAC_RUN_MODE, + SET_HVAC_RUN_MODE_SCHEMA, + f"async_{SERVICE_SET_HVAC_RUN_MODE}", ) entities: list[NexiaZone] = [] @@ -192,20 +196,20 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Return the fan setting.""" return self._thermostat.get_fan_mode() - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" - self._thermostat.set_fan_mode(fan_mode) + await self._thermostat.set_fan_mode(fan_mode) self._signal_thermostat_update() - def set_hvac_run_mode(self, run_mode, hvac_mode): + async def async_set_hvac_run_mode(self, run_mode, hvac_mode): """Set the hvac run mode.""" if run_mode is not None: if run_mode == HOLD_PERMANENT: - self._zone.call_permanent_hold() + await self._zone.call_permanent_hold() else: - self._zone.call_return_to_schedule() + await self._zone.call_return_to_schedule() if hvac_mode is not None: - self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) + await self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self._signal_thermostat_update() @property @@ -213,12 +217,12 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Preset that is active.""" return self._zone.get_preset() - def set_humidity(self, humidity): + async def async_set_humidity(self, humidity): """Dehumidify target.""" if self._thermostat.has_dehumidify_support(): - self.set_dehumidify_setpoint(humidity) + await self.async_set_dehumidify_setpoint(humidity) else: - self.set_humidify_setpoint(humidity) + await self.async_set_humidify_setpoint(humidity) self._signal_thermostat_update() @property @@ -300,7 +304,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): return NEXIA_TO_HA_HVAC_MODE_MAP[mode] - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set target temperature.""" new_heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -332,7 +336,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): ): new_heat_temp = new_cool_temp - deadband - self._zone.set_heat_cool_temp( + await self._zone.set_heat_cool_temp( heat_temperature=new_heat_temp, cool_temperature=new_cool_temp, set_temperature=set_temp, @@ -366,63 +370,63 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): return data - def set_preset_mode(self, preset_mode: str): + async def async_set_preset_mode(self, preset_mode: str): """Set the preset mode.""" - self._zone.set_preset(preset_mode) + await self._zone.set_preset(preset_mode) self._signal_zone_update() - def turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self): """Turn. Aux Heat off.""" - self._thermostat.set_emergency_heat(False) + await self._thermostat.set_emergency_heat(False) self._signal_thermostat_update() - def turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self): """Turn. Aux Heat on.""" self._thermostat.set_emergency_heat(True) self._signal_thermostat_update() - def turn_off(self): + async def async_turn_off(self): """Turn. off the zone.""" - self.set_hvac_mode(OPERATION_MODE_OFF) + await self.set_hvac_mode(OPERATION_MODE_OFF) self._signal_zone_update() - def turn_on(self): + async def async_turn_on(self): """Turn. on the zone.""" - self.set_hvac_mode(OPERATION_MODE_AUTO) + await self.set_hvac_mode(OPERATION_MODE_AUTO) self._signal_zone_update() - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" if hvac_mode == HVACMode.AUTO: - self._zone.call_return_to_schedule() - self._zone.set_mode(mode=OPERATION_MODE_AUTO) + await self._zone.call_return_to_schedule() + await self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: - self._zone.call_permanent_hold() - self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) + await self._zone.call_permanent_hold() + await self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self._signal_zone_update() - def set_aircleaner_mode(self, aircleaner_mode): + async def async_set_aircleaner_mode(self, aircleaner_mode): """Set the aircleaner mode.""" - self._thermostat.set_air_cleaner(aircleaner_mode) + await self._thermostat.set_air_cleaner(aircleaner_mode) self._signal_thermostat_update() - def set_humidify_setpoint(self, humidity): + async def async_set_humidify_setpoint(self, humidity): """Set the humidify setpoint.""" target_humidity = find_humidity_setpoint(humidity / 100.0) if self._thermostat.get_humidify_setpoint() == target_humidity: # Trying to set the humidify setpoint to the # same value will cause the api to timeout return - self._thermostat.set_humidify_setpoint(target_humidity) + await self._thermostat.set_humidify_setpoint(target_humidity) self._signal_thermostat_update() - def set_dehumidify_setpoint(self, humidity): + async def async_set_dehumidify_setpoint(self, humidity): """Set the dehumidify setpoint.""" target_humidity = find_humidity_setpoint(humidity / 100.0) if self._thermostat.get_dehumidify_setpoint() == target_humidity: # Trying to set the dehumidify setpoint to the # same value will cause the api to timeout return - self._thermostat.set_dehumidify_setpoint(target_humidity) + await self._thermostat.set_dehumidify_setpoint(target_humidity) self._signal_thermostat_update() diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index 4e48123a5de..de5640beef7 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -1,13 +1,15 @@ """Config flow for Nexia integration.""" +import asyncio import logging +import aiohttp from nexia.const import BRAND_ASAIR, BRAND_NEXIA, BRAND_TRANE from nexia.home import NexiaHome -from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( BRAND_ASAIR_NAME, @@ -44,23 +46,23 @@ async def validate_input(hass: core.HomeAssistant, data): state_file = hass.config.path( f"{data[CONF_BRAND]}_config_{data[CONF_USERNAME]}.conf" ) + session = async_get_clientsession(hass) + nexia_home = NexiaHome( + session, + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + brand=data[CONF_BRAND], + device_name=hass.config.location_name, + state_file=state_file, + ) try: - nexia_home = NexiaHome( - username=data[CONF_USERNAME], - password=data[CONF_PASSWORD], - brand=data[CONF_BRAND], - auto_login=False, - auto_update=False, - device_name=hass.config.location_name, - state_file=state_file, - ) - await hass.async_add_executor_job(nexia_home.login) - except ConnectTimeout as ex: + await nexia_home.login() + except asyncio.TimeoutError as ex: _LOGGER.error("Unable to connect to Nexia service: %s", ex) raise CannotConnect from ex - except HTTPError as http_ex: + except aiohttp.ClientResponseError as http_ex: _LOGGER.error("HTTP error from Nexia service: %s", http_ex) - if is_invalid_auth_code(http_ex.response.status_code): + if is_invalid_auth_code(http_ex.status): raise InvalidAuth from http_ex raise CannotConnect from http_ex diff --git a/homeassistant/components/nexia/coordinator.py b/homeassistant/components/nexia/coordinator.py index 7f92ca9354b..ba61a3591f0 100644 --- a/homeassistant/components/nexia/coordinator.py +++ b/homeassistant/components/nexia/coordinator.py @@ -33,4 +33,4 @@ class NexiaDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch data from API endpoint.""" - return await self.hass.async_add_executor_job(self.nexia_home.update) + return await self.nexia_home.update() diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 0be5c05396d..dde0e5ae8f3 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -3,7 +3,10 @@ from nexia.thermostat import NexiaThermostat from nexia.zone import NexiaThermostatZone from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,7 +23,9 @@ from .coordinator import NexiaDataUpdateCoordinator class NexiaEntity(CoordinatorEntity): """Base class for nexia entities.""" - def __init__(self, coordinator, name, unique_id): + def __init__( + self, coordinator: NexiaDataUpdateCoordinator, name: str, unique_id: str + ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._unique_id = unique_id @@ -85,7 +90,7 @@ class NexiaThermostatEntity(NexiaEntity): Update all the zones on the thermostat. """ - dispatcher_send( + async_dispatcher_send( self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}" ) @@ -132,4 +137,4 @@ class NexiaThermostatZoneEntity(NexiaThermostatEntity): Update a single zone. """ - dispatcher_send(self.hass, f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}") + async_dispatcher_send(self.hass, f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}") diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 29b80fb00e9..bd1cd58c00b 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==0.9.13"], + "requirements": ["nexia==1.0.0"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 3dd90b07ca2..28c892fe8e5 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -1,6 +1,8 @@ """Support for Nexia Automations.""" from typing import Any +from nexia.automation import NexiaAutomation + from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -37,7 +39,9 @@ async def async_setup_entry( class NexiaAutomationScene(NexiaEntity, Scene): """Provides Nexia automation support.""" - def __init__(self, coordinator, automation): + def __init__( + self, coordinator: NexiaDataUpdateCoordinator, automation: NexiaAutomation + ) -> None: """Initialize the automation scene.""" super().__init__( coordinator, @@ -60,7 +64,7 @@ class NexiaAutomationScene(NexiaEntity, Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate an automation scene.""" - await self.hass.async_add_executor_job(self._automation.activate) + await self._automation.activate() async def refresh_callback(_): await self.coordinator.async_refresh() diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index 8137da8e5cc..3f280581ee7 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from nexia.const import UNIT_CELSIUS +from nexia.thermostat import NexiaThermostat from homeassistant.components.sensor import ( SensorDeviceClass, @@ -32,7 +33,7 @@ async def async_setup_entry( # Thermostat / System Sensors for thermostat_id in nexia_home.get_thermostat_ids(): - thermostat = nexia_home.get_thermostat_by_id(thermostat_id) + thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id) entities.append( NexiaThermostatSensor( diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index 09bc8a3852e..380fea8c4a0 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -56,12 +56,12 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): """Return the icon for the switch.""" return "mdi:timer-off" if self._zone.is_in_permanent_hold() else "mdi:timer" - def turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Enable permanent hold.""" - self._zone.call_permanent_hold() + await self._zone.call_permanent_hold() self._signal_zone_update() - def turn_off(self, **kwargs: Any) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Disable permanent hold.""" - self._zone.call_return_to_schedule() + await self._zone.call_return_to_schedule() self._signal_zone_update() diff --git a/requirements_all.txt b/requirements_all.txt index bddac66cb90..05403b5d3aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ nettigo-air-monitor==1.2.4 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.13 +nexia==1.0.0 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 522583f4ce4..d11047f4c95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.4 # homeassistant.components.nexia -nexia==0.9.13 +nexia==1.0.0 # homeassistant.components.discord nextcord==2.0.0a8 diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index bd3eb9180e7..5bfd7fb582c 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,9 +1,10 @@ """Test the nexia config flow.""" +import asyncio from unittest.mock import MagicMock, patch +import aiohttp from nexia.const import BRAND_ASAIR, BRAND_NEXIA import pytest -from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import config_entries from homeassistant.components.nexia.const import CONF_BRAND, DOMAIN @@ -52,7 +53,10 @@ async def test_form_invalid_auth(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.nexia.config_flow.NexiaHome.login"): + with patch("homeassistant.components.nexia.config_flow.NexiaHome.login",), patch( + "homeassistant.components.nexia.config_flow.NexiaHome.get_name", + return_value=None, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -74,7 +78,7 @@ async def test_form_cannot_connect(hass): with patch( "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=ConnectTimeout, + side_effect=asyncio.TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -95,11 +99,11 @@ async def test_form_invalid_auth_http_401(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - response_mock = MagicMock() - type(response_mock).status_code = 401 with patch( "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=HTTPError(response=response_mock), + side_effect=aiohttp.ClientResponseError( + status=401, request_info=MagicMock(), history=MagicMock() + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -120,11 +124,11 @@ async def test_form_cannot_connect_not_found(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - response_mock = MagicMock() - type(response_mock).status_code = 404 with patch( "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=HTTPError(response=response_mock), + side_effect=aiohttp.ClientResponseError( + status=404, request_info=MagicMock(), history=MagicMock() + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index b6d5c697a18..d564ccc351c 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -3,13 +3,13 @@ from unittest.mock import patch import uuid from nexia.home import NexiaHome -import requests_mock from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import mock_aiohttp_client async def async_init_integration( @@ -21,17 +21,18 @@ async def async_init_integration( house_fixture = "nexia/mobile_houses_123456.json" session_fixture = "nexia/session_123456.json" sign_in_fixture = "nexia/sign_in.json" - nexia = NexiaHome(auto_login=False) - - with requests_mock.mock() as m, patch( + with mock_aiohttp_client() as mock_session, patch( "nexia.home.load_or_create_uuid", return_value=uuid.uuid4() ): - m.post(nexia.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture)) - m.get( + nexia = NexiaHome(mock_session) + mock_session.post( + nexia.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture) + ) + mock_session.get( nexia.API_MOBILE_HOUSES_URL.format(house_id=123456), text=load_fixture(house_fixture), ) - m.post( + mock_session.post( nexia.API_MOBILE_ACCOUNTS_SIGN_IN_URL, text=load_fixture(sign_in_fixture), )