Update nexia to use asyncio (#72108)

This commit is contained in:
J. Nick Koston 2022-05-18 18:08:02 -05:00 committed by GitHub
parent bf63d381b2
commit d8a580a90f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 121 additions and 98 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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}")

View File

@ -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,

View File

@ -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()

View File

@ -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(

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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"],

View File

@ -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),
)