Change Honeywell somecomfort API to AIOSomecomfort API (#86102)

* Move to AIOSomecomfort

* Remove unused constant

* Improve test coverage to 100

* Update homeassistant/components/honeywell/__init__.py

remove "todo" from code

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Missing cannot_connect translation

* add asyncio errors
update devices per entity
rework retry login

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
mkmer 2023-01-18 10:03:13 -05:00 committed by GitHub
parent f0ba7a3795
commit 5e6ba594aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 184 additions and 200 deletions

View File

@ -509,8 +509,8 @@ build.json @home-assistant/supervisor
/tests/components/homematic/ @pvizeli @danielperna84 /tests/components/homematic/ @pvizeli @danielperna84
/homeassistant/components/homewizard/ @DCSBL /homeassistant/components/homewizard/ @DCSBL
/tests/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman /homeassistant/components/honeywell/ @rdfurman @mkmer
/tests/components/honeywell/ @rdfurman /tests/components/honeywell/ @rdfurman @mkmer
/homeassistant/components/http/ @home-assistant/core /homeassistant/components/http/ @home-assistant/core
/tests/components/http/ @home-assistant/core /tests/components/http/ @home-assistant/core
/homeassistant/components/huawei_lte/ @scop @fphammerle /homeassistant/components/huawei_lte/ @scop @fphammerle

View File

@ -1,14 +1,13 @@
"""Support for Honeywell (US) Total Connect Comfort climate systems.""" """Support for Honeywell (US) Total Connect Comfort climate systems."""
import asyncio import asyncio
from datetime import timedelta
import somecomfort import AIOSomecomfort
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.util import Throttle from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ( from .const import (
_LOGGER, _LOGGER,
@ -20,7 +19,6 @@ from .const import (
) )
UPDATE_LOOP_SLEEP_TIME = 5 UPDATE_LOOP_SLEEP_TIME = 5
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE}
@ -51,18 +49,33 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
username = config_entry.data[CONF_USERNAME] username = config_entry.data[CONF_USERNAME]
password = config_entry.data[CONF_PASSWORD] password = config_entry.data[CONF_PASSWORD]
client = await hass.async_add_executor_job( client = AIOSomecomfort.AIOSomeComfort(
get_somecomfort_client, username, password username, password, session=async_get_clientsession(hass)
) )
try:
await client.login()
await client.discover()
if client is None: except AIOSomecomfort.AuthError as ex:
return False raise ConfigEntryNotReady(
"Failed to initialize the Honeywell client: "
"Check your configuration (username, password), "
) from ex
except (
AIOSomecomfort.ConnectionError,
AIOSomecomfort.ConnectionTimeout,
asyncio.TimeoutError,
) as ex:
raise ConfigEntryNotReady(
"Failed to initialize the Honeywell client: "
"Connection error: maybe you have exceeded the API rate limit?"
) from ex
loc_id = config_entry.data.get(CONF_LOC_ID) loc_id = config_entry.data.get(CONF_LOC_ID)
dev_id = config_entry.data.get(CONF_DEV_ID) dev_id = config_entry.data.get(CONF_DEV_ID)
devices = {} devices = {}
for location in client.locations_by_id.values(): for location in client.locations_by_id.values():
if not loc_id or location.locationid == loc_id: if not loc_id or location.locationid == loc_id:
for device in location.devices_by_id.values(): for device in location.devices_by_id.values():
@ -74,7 +87,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return False return False
data = HoneywellData(hass, config_entry, client, username, password, devices) data = HoneywellData(hass, config_entry, client, username, password, devices)
await data.async_update()
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = data hass.data[DOMAIN][config_entry.entry_id] = data
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
@ -99,21 +111,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return unload_ok return unload_ok
def get_somecomfort_client(username: str, password: str) -> somecomfort.SomeComfort:
"""Initialize the somecomfort client."""
try:
return somecomfort.SomeComfort(username, password)
except somecomfort.AuthError:
_LOGGER.error("Failed to login to honeywell account %s", username)
return None
except somecomfort.SomeComfortError as ex:
raise ConfigEntryNotReady(
"Failed to initialize the Honeywell client: "
"Check your configuration (username, password), "
"or maybe you have exceeded the API rate limit?"
) from ex
class HoneywellData: class HoneywellData:
"""Get the latest data and update.""" """Get the latest data and update."""
@ -121,10 +118,10 @@ class HoneywellData:
self, self,
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
client: somecomfort.SomeComfort, client: AIOSomecomfort.AIOSomeComfort,
username: str, username: str,
password: str, password: str,
devices: dict[str, somecomfort.Device], devices: dict[str, AIOSomecomfort.device.Device],
) -> None: ) -> None:
"""Initialize the data object.""" """Initialize the data object."""
self._hass = hass self._hass = hass
@ -134,73 +131,13 @@ class HoneywellData:
self._password = password self._password = password
self.devices = devices self.devices = devices
async def _retry(self) -> bool: async def retry_login(self) -> bool:
"""Recreate a new somecomfort client. """Fire of a login retry."""
When we got an error, the best way to be sure that the next query try:
will succeed, is to recreate a new somecomfort client. await self._client.login()
""" except AIOSomecomfort.SomeComfortError:
self._client = await self._hass.async_add_executor_job(
get_somecomfort_client, self._username, self._password
)
if self._client is None:
return False
refreshed_devices = [
device
for location in self._client.locations_by_id.values()
for device in location.devices_by_id.values()
]
if len(refreshed_devices) == 0:
_LOGGER.error("Failed to find any devices after retry")
return False
for updated_device in refreshed_devices:
if updated_device.deviceid in self.devices:
self.devices[updated_device.deviceid] = updated_device
else:
_LOGGER.info(
"New device with ID %s detected, reload the honeywell integration"
" if you want to access it in Home Assistant"
)
await self._hass.config_entries.async_reload(self._config.entry_id)
return True
async def _refresh_devices(self):
"""Refresh each enabled device."""
for device in self.devices.values():
await self._hass.async_add_executor_job(device.refresh)
await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME) await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME)
return False
@Throttle(MIN_TIME_BETWEEN_UPDATES) return True
async def async_update(self) -> None:
"""Update the state."""
retries = 3
while retries > 0:
try:
await self._refresh_devices()
break
except (
somecomfort.client.APIRateLimited,
somecomfort.client.ConnectionError,
somecomfort.client.ConnectionTimeout,
OSError,
) as exp:
retries -= 1
if retries == 0:
_LOGGER.error(
"Ran out of retry attempts (3 attempts allocated). Error: %s",
exp,
)
raise exp
result = await self._retry()
if not result:
_LOGGER.error("Retry result was empty. Error: %s", exp)
raise exp
_LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp)

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import datetime import datetime
from typing import Any from typing import Any
import somecomfort import AIOSomecomfort
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_HIGH,
@ -70,7 +70,7 @@ HW_FAN_MODE_TO_HA = {
"follow schedule": FAN_AUTO, "follow schedule": FAN_AUTO,
} }
PARALLEL_UPDATES = 1 SCAN_INTERVAL = datetime.timedelta(seconds=10)
async def async_setup_entry( async def async_setup_entry(
@ -230,7 +230,7 @@ class HoneywellUSThermostat(ClimateEntity):
cool_status = self._device.raw_ui_data.get("StatusCool", 0) cool_status = self._device.raw_ui_data.get("StatusCool", 0)
return heat_status == 2 or cool_status == 2 return heat_status == 2 or cool_status == 2
def _set_temperature(self, **kwargs) -> None: async def _set_temperature(self, **kwargs) -> None:
"""Set new target temperature.""" """Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return return
@ -246,35 +246,43 @@ class HoneywellUSThermostat(ClimateEntity):
# Get next period time # Get next period time
hour, minute = divmod(next_period * 15, 60) hour, minute = divmod(next_period * 15, 60)
# Set hold time # Set hold time
setattr(self._device, f"hold_{mode}", datetime.time(hour, minute)) if mode == HVACMode.COOL:
await self._device.set_hold_cool(datetime.time(hour, minute))
elif mode == HVACMode.HEAT:
await self._device.set_hold_heat(datetime.time(hour, minute))
# Set temperature # Set temperature
setattr(self._device, f"setpoint_{mode}", temperature) if mode == HVACMode.COOL:
except somecomfort.SomeComfortError: await self._device.set_setpoint_cool(temperature)
elif mode == HVACMode.HEAT:
await self._device.set_setpoint_heat(temperature)
except AIOSomecomfort.SomeComfortError:
_LOGGER.error("Temperature %.1f out of range", temperature) _LOGGER.error("Temperature %.1f out of range", temperature)
def set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map): if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map):
self._set_temperature(**kwargs) await self._set_temperature(**kwargs)
try: try:
if HVACMode.HEAT_COOL in self._hvac_mode_map: if HVACMode.HEAT_COOL in self._hvac_mode_map:
if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH):
self._device.setpoint_cool = temperature await self._device.set_setpoint_cool(temperature)
if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW):
self._device.setpoint_heat = temperature await self._device.set_setpoint_heat(temperature)
except somecomfort.SomeComfortError as err: except AIOSomecomfort.SomeComfortError as err:
_LOGGER.error("Invalid temperature %s: %s", temperature, err) _LOGGER.error("Invalid temperature %s: %s", temperature, err)
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.""" """Set new target fan mode."""
self._device.fan_mode = self._fan_mode_map[fan_mode] await self._device.set_fan_mode(self._fan_mode_map[fan_mode])
def set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode.""" """Set new target hvac mode."""
self._device.system_mode = self._hvac_mode_map[hvac_mode] await self._device.set_system_mode(self._hvac_mode_map[hvac_mode])
def _turn_away_mode_on(self) -> None: async def _turn_away_mode_on(self) -> None:
"""Turn away on. """Turn away on.
Somecomfort does have a proprietary away mode, but it doesn't really Somecomfort does have a proprietary away mode, but it doesn't really
@ -285,73 +293,87 @@ class HoneywellUSThermostat(ClimateEntity):
try: try:
# Get current mode # Get current mode
mode = self._device.system_mode mode = self._device.system_mode
except somecomfort.SomeComfortError: except AIOSomecomfort.SomeComfortError:
_LOGGER.error("Can not get system mode") _LOGGER.error("Can not get system mode")
return return
try: try:
# Set permanent hold # Set permanent hold
setattr(self._device, f"hold_{mode}", True) # and Set temperature
# Set temperature away_temp = getattr(self, f"_{mode}_away_temp")
setattr( if mode == HVACMode.COOL:
self._device, self._device.set_hold_cool(True)
f"setpoint_{mode}", self._device.set_setpoint_cool(away_temp)
getattr(self, f"_{mode}_away_temp"), elif mode == HVACMode.HEAT:
) self._device.set_hold_heat(True)
except somecomfort.SomeComfortError: self._device.set_setpoint_heat(away_temp)
except AIOSomecomfort.SomeComfortError:
_LOGGER.error( _LOGGER.error(
"Temperature %.1f out of range", getattr(self, f"_{mode}_away_temp") "Temperature %.1f out of range", getattr(self, f"_{mode}_away_temp")
) )
def _turn_hold_mode_on(self) -> None: async def _turn_hold_mode_on(self) -> None:
"""Turn permanent hold on.""" """Turn permanent hold on."""
try: try:
# Get current mode # Get current mode
mode = self._device.system_mode mode = self._device.system_mode
except somecomfort.SomeComfortError: except AIOSomecomfort.SomeComfortError:
_LOGGER.error("Can not get system mode") _LOGGER.error("Can not get system mode")
return return
# Check that we got a valid mode back # Check that we got a valid mode back
if mode in HW_MODE_TO_HVAC_MODE: if mode in HW_MODE_TO_HVAC_MODE:
try: try:
# Set permanent hold # Set permanent hold
setattr(self._device, f"hold_{mode}", True) if mode == HVACMode.COOL:
except somecomfort.SomeComfortError: await self._device.set_hold_cool(True)
elif mode == HVACMode.HEAT:
await self._device.set_hold_heat(True)
except AIOSomecomfort.SomeComfortError:
_LOGGER.error("Couldn't set permanent hold") _LOGGER.error("Couldn't set permanent hold")
else: else:
_LOGGER.error("Invalid system mode returned: %s", mode) _LOGGER.error("Invalid system mode returned: %s", mode)
def _turn_away_mode_off(self) -> None: async def _turn_away_mode_off(self) -> None:
"""Turn away/hold off.""" """Turn away/hold off."""
self._away = False self._away = False
try: try:
# Disabling all hold modes # Disabling all hold modes
self._device.hold_cool = False await self._device.set_hold_cool(False)
self._device.hold_heat = False await self._device.set_hold_heat(False)
except somecomfort.SomeComfortError: except AIOSomecomfort.SomeComfortError:
_LOGGER.error("Can not stop hold mode") _LOGGER.error("Can not stop hold mode")
def set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
if preset_mode == PRESET_AWAY: if preset_mode == PRESET_AWAY:
self._turn_away_mode_on() await self._turn_away_mode_on()
elif preset_mode == PRESET_HOLD: elif preset_mode == PRESET_HOLD:
self._away = False self._away = False
self._turn_hold_mode_on() await self._turn_hold_mode_on()
else: else:
self._turn_away_mode_off() await self._turn_away_mode_off()
def turn_aux_heat_on(self) -> None: async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on.""" """Turn auxiliary heater on."""
self._device.system_mode = "emheat" await self._device.system_mode("emheat")
def turn_aux_heat_off(self) -> None: async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off.""" """Turn auxiliary heater off."""
if HVACMode.HEAT in self.hvac_modes: if HVACMode.HEAT in self.hvac_modes:
self.set_hvac_mode(HVACMode.HEAT) await self.async_set_hvac_mode(HVACMode.HEAT)
else: else:
self.set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)
async def async_update(self) -> None: async def async_update(self) -> None:
"""Get the latest state from the service.""" """Get the latest state from the service."""
await self._data.async_update() try:
await self._device.refresh()
except (
AIOSomecomfort.device.APIRateLimited,
AIOSomecomfort.device.ConnectionError,
AIOSomecomfort.device.ConnectionTimeout,
OSError,
):
await self._data.retry_login()

View File

@ -1,13 +1,17 @@
"""Config flow to configure the honeywell integration.""" """Config flow to configure the honeywell integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
import AIOSomecomfort
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import get_somecomfort_client
from .const import ( from .const import (
CONF_COOL_AWAY_TEMPERATURE, CONF_COOL_AWAY_TEMPERATURE,
CONF_HEAT_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE,
@ -22,20 +26,28 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None) -> FlowResult:
"""Create config entry. Show the setup form to the user.""" """Create config entry. Show the setup form to the user."""
errors = {} errors = {}
valid = False
if user_input is not None: if user_input is not None:
valid = await self.is_valid(**user_input) try:
valid = await self.is_valid(**user_input)
except AIOSomecomfort.AuthError:
errors["base"] = "invalid_auth"
except (
AIOSomecomfort.ConnectionError,
AIOSomecomfort.ConnectionTimeout,
asyncio.TimeoutError,
):
errors["base"] = "cannot_connect"
if valid: if valid:
return self.async_create_entry( return self.async_create_entry(
title=DOMAIN, title=DOMAIN,
data=user_input, data=user_input,
) )
errors["base"] = "invalid_auth"
data_schema = { data_schema = {
vol.Required(CONF_USERNAME): str, vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PASSWORD): str,
@ -46,11 +58,14 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def is_valid(self, **kwargs) -> bool: async def is_valid(self, **kwargs) -> bool:
"""Check if login credentials are valid.""" """Check if login credentials are valid."""
client = await self.hass.async_add_executor_job( client = AIOSomecomfort.AIOSomeComfort(
get_somecomfort_client, kwargs[CONF_USERNAME], kwargs[CONF_PASSWORD] kwargs[CONF_USERNAME],
kwargs[CONF_PASSWORD],
session=async_get_clientsession(self.hass),
) )
return client is not None await client.login()
return True
@staticmethod @staticmethod
@callback @callback
@ -68,7 +83,7 @@ class HoneywellOptionsFlowHandler(config_entries.OptionsFlow):
"""Initialize Honeywell options flow.""" """Initialize Honeywell options flow."""
self.config_entry = entry self.config_entry = entry
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None) -> FlowResult:
"""Manage the options.""" """Manage the options."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title=DOMAIN, data=user_input) return self.async_create_entry(title=DOMAIN, data=user_input)

View File

@ -3,8 +3,8 @@
"name": "Honeywell Total Connect Comfort (US)", "name": "Honeywell Total Connect Comfort (US)",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/honeywell", "documentation": "https://www.home-assistant.io/integrations/honeywell",
"requirements": ["somecomfort==0.8.0"], "requirements": ["aiosomecomfort==0.0.2"],
"codeowners": ["@rdfurman"], "codeowners": ["@rdfurman", "@mkmer"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["somecomfort"] "loggers": ["somecomfort"]
} }

View File

@ -5,7 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from somecomfort import Device from AIOSomecomfort.device import Device
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,

View File

@ -10,7 +10,8 @@
} }
}, },
"error": { "error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
} }
}, },
"options": { "options": {

View File

@ -278,6 +278,9 @@ aioskybell==22.7.0
# homeassistant.components.slimproto # homeassistant.components.slimproto
aioslimproto==2.1.1 aioslimproto==2.1.1
# homeassistant.components.honeywell
aiosomecomfort==0.0.2
# homeassistant.components.steamist # homeassistant.components.steamist
aiosteamist==0.3.2 aiosteamist==0.3.2
@ -2362,9 +2365,6 @@ solaredge==0.0.2
# homeassistant.components.solax # homeassistant.components.solax
solax==0.3.0 solax==0.3.0
# homeassistant.components.honeywell
somecomfort==0.8.0
# homeassistant.components.somfy_mylink # homeassistant.components.somfy_mylink
somfy-mylink-synergy==1.0.6 somfy-mylink-synergy==1.0.6

View File

@ -256,6 +256,9 @@ aioskybell==22.7.0
# homeassistant.components.slimproto # homeassistant.components.slimproto
aioslimproto==2.1.1 aioslimproto==2.1.1
# homeassistant.components.honeywell
aiosomecomfort==0.0.2
# homeassistant.components.steamist # homeassistant.components.steamist
aiosteamist==0.3.2 aiosteamist==0.3.2
@ -1659,9 +1662,6 @@ solaredge==0.0.2
# homeassistant.components.solax # homeassistant.components.solax
solax==0.3.0 solax==0.3.0
# homeassistant.components.honeywell
somecomfort==0.8.0
# homeassistant.components.somfy_mylink # homeassistant.components.somfy_mylink
somfy-mylink-synergy==1.0.6 somfy-mylink-synergy==1.0.6

View File

@ -1,9 +1,9 @@
"""Fixtures for honeywell tests.""" """Fixtures for honeywell tests."""
from unittest.mock import create_autospec, patch from unittest.mock import AsyncMock, create_autospec, patch
import AIOSomecomfort
import pytest import pytest
import somecomfort
from homeassistant.components.honeywell.const import DOMAIN from homeassistant.components.honeywell.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@ -30,7 +30,7 @@ def config_entry(config_data):
@pytest.fixture @pytest.fixture
def device(): def device():
"""Mock a somecomfort.Device.""" """Mock a somecomfort.Device."""
mock_device = create_autospec(somecomfort.Device, instance=True) mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True)
mock_device.deviceid = 1234567 mock_device.deviceid = 1234567
mock_device._data = { mock_device._data = {
"canControlHumidification": False, "canControlHumidification": False,
@ -48,7 +48,7 @@ def device():
@pytest.fixture @pytest.fixture
def device_with_outdoor_sensor(): def device_with_outdoor_sensor():
"""Mock a somecomfort.Device.""" """Mock a somecomfort.Device."""
mock_device = create_autospec(somecomfort.Device, instance=True) mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True)
mock_device.deviceid = 1234567 mock_device.deviceid = 1234567
mock_device._data = { mock_device._data = {
"canControlHumidification": False, "canControlHumidification": False,
@ -67,7 +67,7 @@ def device_with_outdoor_sensor():
@pytest.fixture @pytest.fixture
def another_device(): def another_device():
"""Mock a somecomfort.Device.""" """Mock a somecomfort.Device."""
mock_device = create_autospec(somecomfort.Device, instance=True) mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True)
mock_device.deviceid = 7654321 mock_device.deviceid = 7654321
mock_device._data = { mock_device._data = {
"canControlHumidification": False, "canControlHumidification": False,
@ -85,7 +85,7 @@ def another_device():
@pytest.fixture @pytest.fixture
def location(device): def location(device):
"""Mock a somecomfort.Location.""" """Mock a somecomfort.Location."""
mock_location = create_autospec(somecomfort.Location, instance=True) mock_location = create_autospec(AIOSomecomfort.location.Location, instance=True)
mock_location.locationid.return_value = "location1" mock_location.locationid.return_value = "location1"
mock_location.devices_by_id = {device.deviceid: device} mock_location.devices_by_id = {device.deviceid: device}
return mock_location return mock_location
@ -94,11 +94,13 @@ def location(device):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def client(location): def client(location):
"""Mock a somecomfort.SomeComfort client.""" """Mock a somecomfort.SomeComfort client."""
client_mock = create_autospec(somecomfort.SomeComfort, instance=True) client_mock = create_autospec(AIOSomecomfort.AIOSomeComfort, instance=True)
client_mock.locations_by_id = {location.locationid: location} client_mock.locations_by_id = {location.locationid: location}
client_mock.login = AsyncMock(return_value=True)
client_mock.discover = AsyncMock()
with patch( with patch(
"homeassistant.components.honeywell.somecomfort.SomeComfort" "homeassistant.components.honeywell.AIOSomecomfort.AIOSomeComfort"
) as sc_class_mock: ) as sc_class_mock:
sc_class_mock.return_value = client_mock sc_class_mock.return_value = client_mock
yield client_mock yield client_mock

View File

@ -1,7 +1,7 @@
"""Tests for honeywell config flow.""" """Tests for honeywell config flow."""
from unittest.mock import patch from unittest.mock import MagicMock, patch
import somecomfort import AIOSomecomfort
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.honeywell.const import ( from homeassistant.components.honeywell.const import (
@ -33,28 +33,32 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None:
assert result["step_id"] == "user" assert result["step_id"] == "user"
async def test_connection_error(hass: HomeAssistant) -> None: async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None:
"""Test that an error message is shown on connection fail."""
client.login.side_effect = AIOSomecomfort.ConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG
)
assert result["errors"] == {"base": "cannot_connect"}
async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None:
"""Test that an error message is shown on login fail.""" """Test that an error message is shown on login fail."""
with patch( client.login.side_effect = AIOSomecomfort.AuthError
"somecomfort.SomeComfort", result = await hass.config_entries.flow.async_init(
side_effect=somecomfort.AuthError, DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG
): )
result = await hass.config_entries.flow.async_init( assert result["errors"] == {"base": "invalid_auth"}
DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG
)
assert result["errors"] == {"base": "invalid_auth"}
async def test_create_entry(hass: HomeAssistant) -> None: async def test_create_entry(hass: HomeAssistant) -> None:
"""Test that the config entry is created.""" """Test that the config entry is created."""
with patch(
"somecomfort.SomeComfort", result = await hass.config_entries.flow.async_init(
): DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG
result = await hass.config_entries.flow.async_init( )
DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
) assert result["data"] == FAKE_CONFIG
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"] == FAKE_CONFIG
@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) @patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0)

View File

@ -2,7 +2,7 @@
from unittest.mock import create_autospec, patch from unittest.mock import create_autospec, patch
import somecomfort import AIOSomecomfort
from homeassistant.components.honeywell.const import ( from homeassistant.components.honeywell.const import (
CONF_COOL_AWAY_TEMPERATURE, CONF_COOL_AWAY_TEMPERATURE,
@ -46,7 +46,7 @@ async def test_setup_multiple_thermostats_with_same_deviceid(
hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client
) -> None: ) -> None:
"""Test Honeywell TCC API returning duplicate device IDs.""" """Test Honeywell TCC API returning duplicate device IDs."""
mock_location2 = create_autospec(somecomfort.Location, instance=True) mock_location2 = create_autospec(AIOSomecomfort.Location, instance=True)
mock_location2.locationid.return_value = "location2" mock_location2.locationid.return_value = "location2"
mock_location2.devices_by_id = {device.deviceid: device} mock_location2.devices_by_id = {device.deviceid: device}
client.locations_by_id["location2"] = mock_location2 client.locations_by_id["location2"] = mock_location2
@ -71,13 +71,10 @@ async def test_away_temps_migration(hass: HomeAssistant) -> None:
options={}, options={},
) )
with patch( legacy_config.add_to_hass(hass)
"homeassistant.components.honeywell.somecomfort.SomeComfort", await hass.config_entries.async_setup(legacy_config.entry_id)
): await hass.async_block_till_done()
legacy_config.add_to_hass(hass) assert legacy_config.options == {
await hass.config_entries.async_setup(legacy_config.entry_id) CONF_COOL_AWAY_TEMPERATURE: 1,
await hass.async_block_till_done() CONF_HEAT_AWAY_TEMPERATURE: 2,
assert legacy_config.options == { }
CONF_COOL_AWAY_TEMPERATURE: 1,
CONF_HEAT_AWAY_TEMPERATURE: 2,
}

View File

@ -1,18 +1,24 @@
"""Test honeywell sensor.""" """Test honeywell sensor."""
from somecomfort import Device, Location from AIOSomecomfort.device import Device
from AIOSomecomfort.location import Location
import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@pytest.mark.parametrize("unit,temp", [("C", "5"), ("F", "-15")])
async def test_outdoor_sensor( async def test_outdoor_sensor(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
location: Location, location: Location,
device_with_outdoor_sensor: Device, device_with_outdoor_sensor: Device,
unit,
temp,
): ):
"""Test outdoor temperature sensor.""" """Test outdoor temperature sensor."""
device_with_outdoor_sensor.temperature_unit = unit
location.devices_by_id[ location.devices_by_id[
device_with_outdoor_sensor.deviceid device_with_outdoor_sensor.deviceid
] = device_with_outdoor_sensor ] = device_with_outdoor_sensor
@ -25,5 +31,5 @@ async def test_outdoor_sensor(
assert temperature_state assert temperature_state
assert humidity_state assert humidity_state
assert temperature_state.state == "5" assert temperature_state.state == temp
assert humidity_state.state == "25" assert humidity_state.state == "25"