Add honeywell config flow (#50731)
* Upgrade honeywell from platform to integration * Add codeowner and run code formatter * Add sensors for current indoor temp and humidity * Fix tests and away temp * Spring cleaning of honeywell tests * Add config flow to honeywell integration * Add config flow test * Tie in honeywell service update * Simplify config flow and add import * Remove unnecessary platform schema * Clean up based on PR comments * Use new helper method * Force single device and fix linter errors * Address PR feedback * Update translations * Change string key and remove logger message * Always add first device * Fix test assertion * Put PLATFORM_SCHEMA back * Skip code coverage check on honeywell init * add some tests for honeywell * Make retry async * Make device private * Use _attr_ instead of properties * Code cleanup from PR feedback * Fix test and cleanup code * Make description better Co-authored-by: Matt Zimmerman <mdz@alcor.net>
This commit is contained in:
parent
f5b3118d3c
commit
450fdc91e4
14 changed files with 442 additions and 581 deletions
|
@ -435,6 +435,7 @@ omit =
|
||||||
homeassistant/components/home_plus_control/api.py
|
homeassistant/components/home_plus_control/api.py
|
||||||
homeassistant/components/home_plus_control/switch.py
|
homeassistant/components/home_plus_control/switch.py
|
||||||
homeassistant/components/homeworks/*
|
homeassistant/components/homeworks/*
|
||||||
|
homeassistant/components/honeywell/__init__.py
|
||||||
homeassistant/components/honeywell/climate.py
|
homeassistant/components/honeywell/climate.py
|
||||||
homeassistant/components/horizon/media_player.py
|
homeassistant/components/horizon/media_player.py
|
||||||
homeassistant/components/hp_ilo/sensor.py
|
homeassistant/components/hp_ilo/sensor.py
|
||||||
|
|
|
@ -214,6 +214,7 @@ homeassistant/components/homeassistant/* @home-assistant/core
|
||||||
homeassistant/components/homekit/* @bdraco
|
homeassistant/components/homekit/* @bdraco
|
||||||
homeassistant/components/homekit_controller/* @Jc2k @bdraco
|
homeassistant/components/homekit_controller/* @Jc2k @bdraco
|
||||||
homeassistant/components/homematic/* @pvizeli @danielperna84
|
homeassistant/components/homematic/* @pvizeli @danielperna84
|
||||||
|
homeassistant/components/honeywell/* @rdfurman
|
||||||
homeassistant/components/http/* @home-assistant/core
|
homeassistant/components/http/* @home-assistant/core
|
||||||
homeassistant/components/huawei_lte/* @scop @fphammerle
|
homeassistant/components/huawei_lte/* @scop @fphammerle
|
||||||
homeassistant/components/huawei_router/* @abmantis
|
homeassistant/components/huawei_router/* @abmantis
|
||||||
|
|
|
@ -1 +1,132 @@
|
||||||
"""Support for Honeywell (US) Total Connect Comfort climate systems."""
|
"""Support for Honeywell (US) Total Connect Comfort climate systems."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import somecomfort
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
from .const import _LOGGER, CONF_DEV_ID, CONF_LOC_ID, DOMAIN
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
|
||||||
|
PLATFORMS = ["climate"]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config):
|
||||||
|
"""Set up the Honeywell thermostat."""
|
||||||
|
username = config.data[CONF_USERNAME]
|
||||||
|
password = config.data[CONF_PASSWORD]
|
||||||
|
|
||||||
|
client = await hass.async_add_executor_job(
|
||||||
|
get_somecomfort_client, username, password
|
||||||
|
)
|
||||||
|
|
||||||
|
if client is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
loc_id = config.data.get(CONF_LOC_ID)
|
||||||
|
dev_id = config.data.get(CONF_DEV_ID)
|
||||||
|
|
||||||
|
devices = []
|
||||||
|
|
||||||
|
for location in client.locations_by_id.values():
|
||||||
|
for device in location.devices_by_id.values():
|
||||||
|
if (not loc_id or location.locationid == loc_id) and (
|
||||||
|
not dev_id or device.deviceid == dev_id
|
||||||
|
):
|
||||||
|
devices.append(device)
|
||||||
|
|
||||||
|
if len(devices) == 0:
|
||||||
|
_LOGGER.debug("No devices found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
data = HoneywellService(hass, client, username, password, devices[0])
|
||||||
|
await data.update()
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][config.entry_id] = data
|
||||||
|
hass.config_entries.async_setup_platforms(config, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_somecomfort_client(username, password):
|
||||||
|
"""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 HoneywellService:
|
||||||
|
"""Get the latest data and update."""
|
||||||
|
|
||||||
|
def __init__(self, hass, client, username, password, device):
|
||||||
|
"""Initialize the data object."""
|
||||||
|
self._hass = hass
|
||||||
|
self._client = client
|
||||||
|
self._username = username
|
||||||
|
self._password = password
|
||||||
|
self.device = device
|
||||||
|
|
||||||
|
async def _retry(self) -> bool:
|
||||||
|
"""Recreate a new somecomfort client.
|
||||||
|
|
||||||
|
When we got an error, the best way to be sure that the next query
|
||||||
|
will succeed, is to recreate a new somecomfort client.
|
||||||
|
"""
|
||||||
|
self._client = await self._hass.async_add_executor_job(
|
||||||
|
get_somecomfort_client, self._username, self._password
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._client is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
devices = [
|
||||||
|
device
|
||||||
|
for location in self._client.locations_by_id.values()
|
||||||
|
for device in location.devices_by_id.values()
|
||||||
|
if device.name == self.device.name
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(devices) != 1:
|
||||||
|
_LOGGER.error("Failed to find device %s", self.device.name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.device = devices[0]
|
||||||
|
return True
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
async def update(self) -> None:
|
||||||
|
"""Update the state."""
|
||||||
|
retries = 3
|
||||||
|
while retries > 0:
|
||||||
|
try:
|
||||||
|
await self._hass.async_add_executor_job(self.device.refresh)
|
||||||
|
break
|
||||||
|
except (
|
||||||
|
somecomfort.client.APIRateLimited,
|
||||||
|
OSError,
|
||||||
|
somecomfort.client.ConnectionTimeout,
|
||||||
|
) as exp:
|
||||||
|
retries -= 1
|
||||||
|
if retries == 0:
|
||||||
|
raise exp
|
||||||
|
|
||||||
|
result = await self._hass.async_add_executor_job(self._retry())
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise exp
|
||||||
|
|
||||||
|
_LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"latestData = %s ", self.device._data # pylint: disable=protected-access
|
||||||
|
)
|
||||||
|
|
|
@ -2,10 +2,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
|
||||||
import somecomfort
|
import somecomfort
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -33,6 +31,7 @@ from homeassistant.components.climate.const import (
|
||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
|
@ -42,19 +41,21 @@ from homeassistant.const import (
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.helpers.device_registry as dr
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
from .const import (
|
||||||
|
_LOGGER,
|
||||||
|
CONF_COOL_AWAY_TEMPERATURE,
|
||||||
|
CONF_DEV_ID,
|
||||||
|
CONF_HEAT_AWAY_TEMPERATURE,
|
||||||
|
CONF_LOC_ID,
|
||||||
|
DEFAULT_COOL_AWAY_TEMPERATURE,
|
||||||
|
DEFAULT_HEAT_AWAY_TEMPERATURE,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
ATTR_FAN_ACTION = "fan_action"
|
ATTR_FAN_ACTION = "fan_action"
|
||||||
|
|
||||||
CONF_COOL_AWAY_TEMPERATURE = "away_cool_temperature"
|
|
||||||
CONF_HEAT_AWAY_TEMPERATURE = "away_heat_temperature"
|
|
||||||
CONF_DEV_ID = "thermostat"
|
|
||||||
CONF_LOC_ID = "location"
|
|
||||||
|
|
||||||
DEFAULT_COOL_AWAY_TEMPERATURE = 88
|
|
||||||
DEFAULT_HEAT_AWAY_TEMPERATURE = 61
|
|
||||||
|
|
||||||
ATTR_PERMANENT_HOLD = "permanent_hold"
|
ATTR_PERMANENT_HOLD = "permanent_hold"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.All(
|
PLATFORM_SCHEMA = vol.All(
|
||||||
|
@ -108,95 +109,88 @@ HW_FAN_MODE_TO_HA = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Honeywell thermostat."""
|
"""Set up the Honeywell thermostat."""
|
||||||
username = config.get(CONF_USERNAME)
|
cool_away_temp = config.data.get(CONF_COOL_AWAY_TEMPERATURE)
|
||||||
password = config.get(CONF_PASSWORD)
|
heat_away_temp = config.data.get(CONF_HEAT_AWAY_TEMPERATURE)
|
||||||
|
|
||||||
try:
|
data = hass.data[DOMAIN][config.entry_id]
|
||||||
client = somecomfort.SomeComfort(username, password)
|
|
||||||
except somecomfort.AuthError:
|
async_add_entities([HoneywellUSThermostat(data, cool_away_temp, heat_away_temp)])
|
||||||
_LOGGER.error("Failed to login to honeywell account %s", username)
|
|
||||||
return
|
|
||||||
except somecomfort.SomeComfortError:
|
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
_LOGGER.error(
|
"""Set up the Honeywell climate platform.
|
||||||
"Failed to initialize the Honeywell client: "
|
|
||||||
"Check your configuration (username, password), "
|
Honeywell uses config flow for configuration now. If an entry exists in
|
||||||
"or maybe you have exceeded the API rate limit?"
|
configuration.yaml, the import flow will attempt to import it and create
|
||||||
|
a config entry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if config["platform"] == "honeywell":
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Loading honeywell via platform config is deprecated; The configuration"
|
||||||
|
" has been migrated to a config entry and can be safely removed"
|
||||||
)
|
)
|
||||||
return
|
# No config entry exists and configuration.yaml config exists, trigger the import flow.
|
||||||
|
if not hass.config_entries.async_entries(DOMAIN):
|
||||||
dev_id = config.get(CONF_DEV_ID)
|
await hass.config_entries.flow.async_init(
|
||||||
loc_id = config.get(CONF_LOC_ID)
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||||
cool_away_temp = config.get(CONF_COOL_AWAY_TEMPERATURE)
|
|
||||||
heat_away_temp = config.get(CONF_HEAT_AWAY_TEMPERATURE)
|
|
||||||
|
|
||||||
add_entities(
|
|
||||||
[
|
|
||||||
HoneywellUSThermostat(
|
|
||||||
client,
|
|
||||||
device,
|
|
||||||
cool_away_temp,
|
|
||||||
heat_away_temp,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
)
|
)
|
||||||
for location in client.locations_by_id.values()
|
|
||||||
for device in location.devices_by_id.values()
|
|
||||||
if (
|
|
||||||
(not loc_id or location.locationid == loc_id)
|
|
||||||
and (not dev_id or device.deviceid == dev_id)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HoneywellUSThermostat(ClimateEntity):
|
class HoneywellUSThermostat(ClimateEntity):
|
||||||
"""Representation of a Honeywell US Thermostat."""
|
"""Representation of a Honeywell US Thermostat."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, data, cool_away_temp, heat_away_temp):
|
||||||
self, client, device, cool_away_temp, heat_away_temp, username, password
|
|
||||||
):
|
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
self._client = client
|
self._data = data
|
||||||
self._device = device
|
|
||||||
self._cool_away_temp = cool_away_temp
|
self._cool_away_temp = cool_away_temp
|
||||||
self._heat_away_temp = heat_away_temp
|
self._heat_away_temp = heat_away_temp
|
||||||
self._away = False
|
self._away = False
|
||||||
self._username = username
|
|
||||||
self._password = password
|
|
||||||
|
|
||||||
_LOGGER.debug("latestData = %s ", device._data)
|
self._attr_unique_id = dr.format_mac(data.device.mac_address)
|
||||||
|
self._attr_name = data.device.name
|
||||||
|
self._attr_temperature_unit = (
|
||||||
|
TEMP_CELSIUS if data.device.temperature_unit == "C" else TEMP_FAHRENHEIT
|
||||||
|
)
|
||||||
|
self._attr_preset_modes = [PRESET_NONE, PRESET_AWAY]
|
||||||
|
self._attr_is_aux_heat = data.device.system_mode == "emheat"
|
||||||
|
|
||||||
# not all honeywell HVACs support all modes
|
# not all honeywell HVACs support all modes
|
||||||
mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() if device.raw_ui_data[k]]
|
mappings = [
|
||||||
|
v for k, v in HVAC_MODE_TO_HW_MODE.items() if data.device.raw_ui_data[k]
|
||||||
|
]
|
||||||
self._hvac_mode_map = {k: v for d in mappings for k, v in d.items()}
|
self._hvac_mode_map = {k: v for d in mappings for k, v in d.items()}
|
||||||
|
self._attr_hvac_modes = list(self._hvac_mode_map)
|
||||||
|
|
||||||
self._supported_features = (
|
self._attr_supported_features = (
|
||||||
SUPPORT_PRESET_MODE
|
SUPPORT_PRESET_MODE
|
||||||
| SUPPORT_TARGET_TEMPERATURE
|
| SUPPORT_TARGET_TEMPERATURE
|
||||||
| SUPPORT_TARGET_TEMPERATURE_RANGE
|
| SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||||
)
|
)
|
||||||
|
|
||||||
if device._data["canControlHumidification"]:
|
if data.device._data["canControlHumidification"]:
|
||||||
self._supported_features |= SUPPORT_TARGET_HUMIDITY
|
self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY
|
||||||
|
|
||||||
if device.raw_ui_data["SwitchEmergencyHeatAllowed"]:
|
if data.device.raw_ui_data["SwitchEmergencyHeatAllowed"]:
|
||||||
self._supported_features |= SUPPORT_AUX_HEAT
|
self._attr_supported_features |= SUPPORT_AUX_HEAT
|
||||||
|
|
||||||
if not device._data["hasFan"]:
|
if not data.device._data["hasFan"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
# not all honeywell fans support all modes
|
# not all honeywell fans support all modes
|
||||||
mappings = [v for k, v in FAN_MODE_TO_HW.items() if device.raw_fan_data[k]]
|
mappings = [v for k, v in FAN_MODE_TO_HW.items() if data.device.raw_fan_data[k]]
|
||||||
self._fan_mode_map = {k: v for d in mappings for k, v in d.items()}
|
self._fan_mode_map = {k: v for d in mappings for k, v in d.items()}
|
||||||
|
|
||||||
self._supported_features |= SUPPORT_FAN_MODE
|
self._attr_fan_modes = list(self._fan_mode_map)
|
||||||
|
|
||||||
|
self._attr_supported_features |= SUPPORT_FAN_MODE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str | None:
|
def _device(self):
|
||||||
"""Return the name of the honeywell, if any."""
|
"""Shortcut to access the device."""
|
||||||
return self._device.name
|
return self._data.device
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
def extra_state_attributes(self) -> dict[str, Any]:
|
||||||
|
@ -208,11 +202,6 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||||
data["dr_phase"] = self._device.raw_dr_data.get("Phase")
|
data["dr_phase"] = self._device.raw_dr_data.get("Phase")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
|
||||||
def supported_features(self) -> int:
|
|
||||||
"""Return the list of supported features."""
|
|
||||||
return self._supported_features
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min_temp(self) -> float:
|
def min_temp(self) -> float:
|
||||||
"""Return the minimum temperature."""
|
"""Return the minimum temperature."""
|
||||||
|
@ -231,11 +220,6 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||||
return self._device.raw_ui_data["HeatUpperSetptLimit"]
|
return self._device.raw_ui_data["HeatUpperSetptLimit"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
|
||||||
def temperature_unit(self) -> str:
|
|
||||||
"""Return the unit of measurement."""
|
|
||||||
return TEMP_CELSIUS if self._device.temperature_unit == "C" else TEMP_FAHRENHEIT
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_humidity(self) -> int | None:
|
def current_humidity(self) -> int | None:
|
||||||
"""Return the current humidity."""
|
"""Return the current humidity."""
|
||||||
|
@ -246,11 +230,6 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||||
"""Return hvac operation ie. heat, cool mode."""
|
"""Return hvac operation ie. heat, cool mode."""
|
||||||
return HW_MODE_TO_HVAC_MODE[self._device.system_mode]
|
return HW_MODE_TO_HVAC_MODE[self._device.system_mode]
|
||||||
|
|
||||||
@property
|
|
||||||
def hvac_modes(self) -> list[str]:
|
|
||||||
"""Return the list of available hvac operation modes."""
|
|
||||||
return list(self._hvac_mode_map)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_action(self) -> str | None:
|
def hvac_action(self) -> str | None:
|
||||||
"""Return the current running hvac operation if supported."""
|
"""Return the current running hvac operation if supported."""
|
||||||
|
@ -291,26 +270,11 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||||
"""Return the current preset mode, e.g., home, away, temp."""
|
"""Return the current preset mode, e.g., home, away, temp."""
|
||||||
return PRESET_AWAY if self._away else None
|
return PRESET_AWAY if self._away else None
|
||||||
|
|
||||||
@property
|
|
||||||
def preset_modes(self) -> list[str] | None:
|
|
||||||
"""Return a list of available preset modes."""
|
|
||||||
return [PRESET_NONE, PRESET_AWAY]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_aux_heat(self) -> str | None:
|
|
||||||
"""Return true if aux heater."""
|
|
||||||
return self._device.system_mode == "emheat"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_mode(self) -> str | None:
|
def fan_mode(self) -> str | None:
|
||||||
"""Return the fan setting."""
|
"""Return the fan setting."""
|
||||||
return HW_FAN_MODE_TO_HA[self._device.fan_mode]
|
return HW_FAN_MODE_TO_HA[self._device.fan_mode]
|
||||||
|
|
||||||
@property
|
|
||||||
def fan_modes(self) -> list[str] | None:
|
|
||||||
"""Return the list of available fan modes."""
|
|
||||||
return list(self._fan_mode_map)
|
|
||||||
|
|
||||||
def _is_permanent_hold(self) -> bool:
|
def _is_permanent_hold(self) -> bool:
|
||||||
heat_status = self._device.raw_ui_data.get("StatusHeat", 0)
|
heat_status = self._device.raw_ui_data.get("StatusHeat", 0)
|
||||||
cool_status = self._device.raw_ui_data.get("StatusCool", 0)
|
cool_status = self._device.raw_ui_data.get("StatusCool", 0)
|
||||||
|
@ -383,7 +347,9 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||||
setattr(self._device, f"hold_{mode}", True)
|
setattr(self._device, f"hold_{mode}", True)
|
||||||
# Set temperature
|
# Set temperature
|
||||||
setattr(
|
setattr(
|
||||||
self._device, f"setpoint_{mode}", getattr(self, f"_{mode}_away_temp")
|
self._device,
|
||||||
|
f"setpoint_{mode}",
|
||||||
|
getattr(self, f"_{mode}_away_temp"),
|
||||||
)
|
)
|
||||||
except somecomfort.SomeComfortError:
|
except somecomfort.SomeComfortError:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@ -418,54 +384,6 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||||
else:
|
else:
|
||||||
self.set_hvac_mode(HVAC_MODE_OFF)
|
self.set_hvac_mode(HVAC_MODE_OFF)
|
||||||
|
|
||||||
def _retry(self) -> bool:
|
async def async_update(self):
|
||||||
"""Recreate a new somecomfort client.
|
"""Get the latest state from the service."""
|
||||||
|
await self._data.update()
|
||||||
When we got an error, the best way to be sure that the next query
|
|
||||||
will succeed, is to recreate a new somecomfort client.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._client = somecomfort.SomeComfort(self._username, self._password)
|
|
||||||
except somecomfort.AuthError:
|
|
||||||
_LOGGER.error("Failed to login to honeywell account %s", self._username)
|
|
||||||
return False
|
|
||||||
except somecomfort.SomeComfortError as ex:
|
|
||||||
_LOGGER.error("Failed to initialize honeywell client: %s", str(ex))
|
|
||||||
return False
|
|
||||||
|
|
||||||
devices = [
|
|
||||||
device
|
|
||||||
for location in self._client.locations_by_id.values()
|
|
||||||
for device in location.devices_by_id.values()
|
|
||||||
if device.name == self._device.name
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(devices) != 1:
|
|
||||||
_LOGGER.error("Failed to find device %s", self._device.name)
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._device = devices[0]
|
|
||||||
return True
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
"""Update the state."""
|
|
||||||
retries = 3
|
|
||||||
while retries > 0:
|
|
||||||
try:
|
|
||||||
self._device.refresh()
|
|
||||||
break
|
|
||||||
except (
|
|
||||||
somecomfort.client.APIRateLimited,
|
|
||||||
OSError,
|
|
||||||
requests.exceptions.ReadTimeout,
|
|
||||||
) as exp:
|
|
||||||
retries -= 1
|
|
||||||
if retries == 0:
|
|
||||||
raise exp
|
|
||||||
if not self._retry():
|
|
||||||
raise exp
|
|
||||||
_LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"latestData = %s ", self._device._data # pylint: disable=protected-access
|
|
||||||
)
|
|
||||||
|
|
55
homeassistant/components/honeywell/config_flow.py
Normal file
55
homeassistant/components/honeywell/config_flow.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
"""Config flow to configure the honeywell integration."""
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.honeywell import get_somecomfort_client
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
from .const import CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a honeywell config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Create config entry. Show the setup form to the user."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
valid = await self.is_valid(**user_input)
|
||||||
|
if valid:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=DOMAIN,
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
|
||||||
|
data_schema = {
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=vol.Schema(data_schema), errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def is_valid(self, **kwargs) -> bool:
|
||||||
|
"""Check if login credentials are valid."""
|
||||||
|
client = await self.hass.async_add_executor_job(
|
||||||
|
get_somecomfort_client, kwargs[CONF_USERNAME], kwargs[CONF_PASSWORD]
|
||||||
|
)
|
||||||
|
|
||||||
|
return client is not None
|
||||||
|
|
||||||
|
async def async_step_import(self, import_data):
|
||||||
|
"""Import entry from configuration.yaml."""
|
||||||
|
return await self.async_step_user(
|
||||||
|
{
|
||||||
|
CONF_USERNAME: import_data[CONF_USERNAME],
|
||||||
|
CONF_PASSWORD: import_data[CONF_PASSWORD],
|
||||||
|
CONF_COOL_AWAY_TEMPERATURE: import_data[CONF_COOL_AWAY_TEMPERATURE],
|
||||||
|
CONF_HEAT_AWAY_TEMPERATURE: import_data[CONF_HEAT_AWAY_TEMPERATURE],
|
||||||
|
}
|
||||||
|
)
|
13
homeassistant/components/honeywell/const.py
Normal file
13
homeassistant/components/honeywell/const.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
"""Support for Honeywell (US) Total Connect Comfort climate systems."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
DOMAIN = "honeywell"
|
||||||
|
|
||||||
|
DEFAULT_COOL_AWAY_TEMPERATURE = 88
|
||||||
|
DEFAULT_HEAT_AWAY_TEMPERATURE = 61
|
||||||
|
CONF_COOL_AWAY_TEMPERATURE = "away_cool_temperature"
|
||||||
|
CONF_HEAT_AWAY_TEMPERATURE = "away_heat_temperature"
|
||||||
|
CONF_DEV_ID = "thermostat"
|
||||||
|
CONF_LOC_ID = "location"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"domain": "honeywell",
|
"domain": "honeywell",
|
||||||
"name": "Honeywell Total Connect Comfort (US)",
|
"name": "Honeywell Total Connect Comfort (US)",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/honeywell",
|
"documentation": "https://www.home-assistant.io/integrations/honeywell",
|
||||||
"requirements": ["somecomfort==0.5.2"],
|
"requirements": ["somecomfort==0.5.2"],
|
||||||
"codeowners": [],
|
"codeowners": ["@rdfurman"],
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
}
|
}
|
||||||
|
|
17
homeassistant/components/honeywell/strings.json
Normal file
17
homeassistant/components/honeywell/strings.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Honeywell Total Connect Comfort (US)",
|
||||||
|
"description": "Please enter the credentials used to log into mytotalconnectcomfort.com.",
|
||||||
|
"data": {
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
homeassistant/components/honeywell/translations/en.json
Normal file
17
homeassistant/components/honeywell/translations/en.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"invalid_auth": "Invalid authentication"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "Password",
|
||||||
|
"username": "Username"
|
||||||
|
},
|
||||||
|
"description": "Please enter the credentials used to log into mytotalconnectcomfort.com.",
|
||||||
|
"title": "Honeywell Total Connect Comfort (US)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -113,6 +113,7 @@ FLOWS = [
|
||||||
"homekit",
|
"homekit",
|
||||||
"homekit_controller",
|
"homekit_controller",
|
||||||
"homematicip_cloud",
|
"homematicip_cloud",
|
||||||
|
"honeywell",
|
||||||
"huawei_lte",
|
"huawei_lte",
|
||||||
"hue",
|
"hue",
|
||||||
"huisbaasje",
|
"huisbaasje",
|
||||||
|
|
65
tests/components/honeywell/conftest.py
Normal file
65
tests/components/honeywell/conftest.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
"""Fixtures for honeywell tests."""
|
||||||
|
|
||||||
|
from unittest.mock import create_autospec, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import somecomfort
|
||||||
|
|
||||||
|
from homeassistant.components.honeywell.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_data():
|
||||||
|
"""Provide configuration data for tests."""
|
||||||
|
return {CONF_USERNAME: "fake", CONF_PASSWORD: "user"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_entry(config_data):
|
||||||
|
"""Create a mock config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=config_data,
|
||||||
|
options={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def device():
|
||||||
|
"""Mock a somecomfort.Device."""
|
||||||
|
mock_device = create_autospec(somecomfort.Device, instance=True)
|
||||||
|
mock_device.deviceid.return_value = "device1"
|
||||||
|
mock_device._data = {
|
||||||
|
"canControlHumidification": False,
|
||||||
|
"hasFan": False,
|
||||||
|
}
|
||||||
|
mock_device.system_mode = "off"
|
||||||
|
mock_device.name = "device1"
|
||||||
|
mock_device.current_temperature = 20
|
||||||
|
mock_device.mac_address = "macaddress1"
|
||||||
|
return mock_device
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def location(device):
|
||||||
|
"""Mock a somecomfort.Location."""
|
||||||
|
mock_location = create_autospec(somecomfort.Location, instance=True)
|
||||||
|
mock_location.locationid.return_value = "location1"
|
||||||
|
mock_location.devices_by_id = {device.deviceid: device}
|
||||||
|
return mock_location
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def client(location):
|
||||||
|
"""Mock a somecomfort.SomeComfort client."""
|
||||||
|
client_mock = create_autospec(somecomfort.SomeComfort, instance=True)
|
||||||
|
client_mock.locations_by_id = {location.locationid: location}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.honeywell.somecomfort.SomeComfort"
|
||||||
|
) as sc_class_mock:
|
||||||
|
sc_class_mock.return_value = client_mock
|
||||||
|
yield client_mock
|
|
@ -1,430 +0,0 @@
|
||||||
"""The test the Honeywell thermostat module."""
|
|
||||||
import unittest
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import requests.exceptions
|
|
||||||
import somecomfort
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.climate.const import (
|
|
||||||
ATTR_FAN_MODE,
|
|
||||||
ATTR_FAN_MODES,
|
|
||||||
ATTR_HVAC_MODES,
|
|
||||||
)
|
|
||||||
import homeassistant.components.honeywell.climate as honeywell
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_USERNAME,
|
|
||||||
TEMP_CELSIUS,
|
|
||||||
TEMP_FAHRENHEIT,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.skip("Need to be fixed!")
|
|
||||||
|
|
||||||
|
|
||||||
class TestHoneywell(unittest.TestCase):
|
|
||||||
"""A test class for Honeywell themostats."""
|
|
||||||
|
|
||||||
@mock.patch("somecomfort.SomeComfort")
|
|
||||||
@mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat")
|
|
||||||
def test_setup_us(self, mock_ht, mock_sc):
|
|
||||||
"""Test for the US setup."""
|
|
||||||
config = {
|
|
||||||
CONF_USERNAME: "user",
|
|
||||||
CONF_PASSWORD: "pass",
|
|
||||||
honeywell.CONF_REGION: "us",
|
|
||||||
}
|
|
||||||
bad_pass_config = {CONF_USERNAME: "user", honeywell.CONF_REGION: "us"}
|
|
||||||
bad_region_config = {
|
|
||||||
CONF_USERNAME: "user",
|
|
||||||
CONF_PASSWORD: "pass",
|
|
||||||
honeywell.CONF_REGION: "un",
|
|
||||||
}
|
|
||||||
|
|
||||||
with pytest.raises(vol.Invalid):
|
|
||||||
honeywell.PLATFORM_SCHEMA(None)
|
|
||||||
|
|
||||||
with pytest.raises(vol.Invalid):
|
|
||||||
honeywell.PLATFORM_SCHEMA({})
|
|
||||||
|
|
||||||
with pytest.raises(vol.Invalid):
|
|
||||||
honeywell.PLATFORM_SCHEMA(bad_pass_config)
|
|
||||||
|
|
||||||
with pytest.raises(vol.Invalid):
|
|
||||||
honeywell.PLATFORM_SCHEMA(bad_region_config)
|
|
||||||
|
|
||||||
hass = mock.MagicMock()
|
|
||||||
add_entities = mock.MagicMock()
|
|
||||||
|
|
||||||
locations = [mock.MagicMock(), mock.MagicMock()]
|
|
||||||
devices_1 = [mock.MagicMock()]
|
|
||||||
devices_2 = [mock.MagicMock(), mock.MagicMock]
|
|
||||||
mock_sc.return_value.locations_by_id.values.return_value = locations
|
|
||||||
locations[0].devices_by_id.values.return_value = devices_1
|
|
||||||
locations[1].devices_by_id.values.return_value = devices_2
|
|
||||||
|
|
||||||
result = honeywell.setup_platform(hass, config, add_entities)
|
|
||||||
assert result
|
|
||||||
assert mock_sc.call_count == 1
|
|
||||||
assert mock_sc.call_args == mock.call("user", "pass")
|
|
||||||
mock_ht.assert_has_calls(
|
|
||||||
[
|
|
||||||
mock.call(mock_sc.return_value, devices_1[0], 18, 28, "user", "pass"),
|
|
||||||
mock.call(mock_sc.return_value, devices_2[0], 18, 28, "user", "pass"),
|
|
||||||
mock.call(mock_sc.return_value, devices_2[1], 18, 28, "user", "pass"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch("somecomfort.SomeComfort")
|
|
||||||
def test_setup_us_failures(self, mock_sc):
|
|
||||||
"""Test the US setup."""
|
|
||||||
hass = mock.MagicMock()
|
|
||||||
add_entities = mock.MagicMock()
|
|
||||||
config = {
|
|
||||||
CONF_USERNAME: "user",
|
|
||||||
CONF_PASSWORD: "pass",
|
|
||||||
honeywell.CONF_REGION: "us",
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_sc.side_effect = somecomfort.AuthError
|
|
||||||
result = honeywell.setup_platform(hass, config, add_entities)
|
|
||||||
assert not result
|
|
||||||
assert not add_entities.called
|
|
||||||
|
|
||||||
mock_sc.side_effect = somecomfort.SomeComfortError
|
|
||||||
result = honeywell.setup_platform(hass, config, add_entities)
|
|
||||||
assert not result
|
|
||||||
assert not add_entities.called
|
|
||||||
|
|
||||||
@mock.patch("somecomfort.SomeComfort")
|
|
||||||
@mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat")
|
|
||||||
def _test_us_filtered_devices(self, mock_ht, mock_sc, loc=None, dev=None):
|
|
||||||
"""Test for US filtered thermostats."""
|
|
||||||
config = {
|
|
||||||
CONF_USERNAME: "user",
|
|
||||||
CONF_PASSWORD: "pass",
|
|
||||||
honeywell.CONF_REGION: "us",
|
|
||||||
"location": loc,
|
|
||||||
"thermostat": dev,
|
|
||||||
}
|
|
||||||
locations = {
|
|
||||||
1: mock.MagicMock(
|
|
||||||
locationid=mock.sentinel.loc1,
|
|
||||||
devices_by_id={
|
|
||||||
11: mock.MagicMock(deviceid=mock.sentinel.loc1dev1),
|
|
||||||
12: mock.MagicMock(deviceid=mock.sentinel.loc1dev2),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
2: mock.MagicMock(
|
|
||||||
locationid=mock.sentinel.loc2,
|
|
||||||
devices_by_id={21: mock.MagicMock(deviceid=mock.sentinel.loc2dev1)},
|
|
||||||
),
|
|
||||||
3: mock.MagicMock(
|
|
||||||
locationid=mock.sentinel.loc3,
|
|
||||||
devices_by_id={31: mock.MagicMock(deviceid=mock.sentinel.loc3dev1)},
|
|
||||||
),
|
|
||||||
}
|
|
||||||
mock_sc.return_value = mock.MagicMock(locations_by_id=locations)
|
|
||||||
hass = mock.MagicMock()
|
|
||||||
add_entities = mock.MagicMock()
|
|
||||||
assert honeywell.setup_platform(hass, config, add_entities) is True
|
|
||||||
|
|
||||||
return mock_ht.call_args_list, mock_sc
|
|
||||||
|
|
||||||
def test_us_filtered_thermostat_1(self):
|
|
||||||
"""Test for US filtered thermostats."""
|
|
||||||
result, client = self._test_us_filtered_devices(dev=mock.sentinel.loc1dev1)
|
|
||||||
devices = [x[0][1].deviceid for x in result]
|
|
||||||
assert [mock.sentinel.loc1dev1] == devices
|
|
||||||
|
|
||||||
def test_us_filtered_thermostat_2(self):
|
|
||||||
"""Test for US filtered location."""
|
|
||||||
result, client = self._test_us_filtered_devices(dev=mock.sentinel.loc2dev1)
|
|
||||||
devices = [x[0][1].deviceid for x in result]
|
|
||||||
assert [mock.sentinel.loc2dev1] == devices
|
|
||||||
|
|
||||||
def test_us_filtered_location_1(self):
|
|
||||||
"""Test for US filtered locations."""
|
|
||||||
result, client = self._test_us_filtered_devices(loc=mock.sentinel.loc1)
|
|
||||||
devices = [x[0][1].deviceid for x in result]
|
|
||||||
assert [mock.sentinel.loc1dev1, mock.sentinel.loc1dev2] == devices
|
|
||||||
|
|
||||||
def test_us_filtered_location_2(self):
|
|
||||||
"""Test for US filtered locations."""
|
|
||||||
result, client = self._test_us_filtered_devices(loc=mock.sentinel.loc2)
|
|
||||||
devices = [x[0][1].deviceid for x in result]
|
|
||||||
assert [mock.sentinel.loc2dev1] == devices
|
|
||||||
|
|
||||||
@mock.patch("evohomeclient.EvohomeClient")
|
|
||||||
@mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat")
|
|
||||||
def test_eu_setup_full_config(self, mock_round, mock_evo):
|
|
||||||
"""Test the EU setup with complete configuration."""
|
|
||||||
config = {
|
|
||||||
CONF_USERNAME: "user",
|
|
||||||
CONF_PASSWORD: "pass",
|
|
||||||
honeywell.CONF_REGION: "eu",
|
|
||||||
}
|
|
||||||
mock_evo.return_value.temperatures.return_value = [{"id": "foo"}, {"id": "bar"}]
|
|
||||||
hass = mock.MagicMock()
|
|
||||||
add_entities = mock.MagicMock()
|
|
||||||
assert honeywell.setup_platform(hass, config, add_entities)
|
|
||||||
assert mock_evo.call_count == 1
|
|
||||||
assert mock_evo.call_args == mock.call("user", "pass")
|
|
||||||
assert mock_evo.return_value.temperatures.call_count == 1
|
|
||||||
assert mock_evo.return_value.temperatures.call_args == mock.call(
|
|
||||||
force_refresh=True
|
|
||||||
)
|
|
||||||
mock_round.assert_has_calls(
|
|
||||||
[
|
|
||||||
mock.call(mock_evo.return_value, "foo", True, 20.0),
|
|
||||||
mock.call(mock_evo.return_value, "bar", False, 20.0),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
assert add_entities.call_count == 2
|
|
||||||
|
|
||||||
@mock.patch("evohomeclient.EvohomeClient")
|
|
||||||
@mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat")
|
|
||||||
def test_eu_setup_partial_config(self, mock_round, mock_evo):
|
|
||||||
"""Test the EU setup with partial configuration."""
|
|
||||||
config = {
|
|
||||||
CONF_USERNAME: "user",
|
|
||||||
CONF_PASSWORD: "pass",
|
|
||||||
honeywell.CONF_REGION: "eu",
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_evo.return_value.temperatures.return_value = [{"id": "foo"}, {"id": "bar"}]
|
|
||||||
|
|
||||||
hass = mock.MagicMock()
|
|
||||||
add_entities = mock.MagicMock()
|
|
||||||
assert honeywell.setup_platform(hass, config, add_entities)
|
|
||||||
mock_round.assert_has_calls(
|
|
||||||
[
|
|
||||||
mock.call(mock_evo.return_value, "foo", True, 16),
|
|
||||||
mock.call(mock_evo.return_value, "bar", False, 16),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch("evohomeclient.EvohomeClient")
|
|
||||||
@mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat")
|
|
||||||
def test_eu_setup_bad_temp(self, mock_round, mock_evo):
|
|
||||||
"""Test the EU setup with invalid temperature."""
|
|
||||||
config = {
|
|
||||||
CONF_USERNAME: "user",
|
|
||||||
CONF_PASSWORD: "pass",
|
|
||||||
honeywell.CONF_REGION: "eu",
|
|
||||||
}
|
|
||||||
|
|
||||||
with pytest.raises(vol.Invalid):
|
|
||||||
honeywell.PLATFORM_SCHEMA(config)
|
|
||||||
|
|
||||||
@mock.patch("evohomeclient.EvohomeClient")
|
|
||||||
@mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat")
|
|
||||||
def test_eu_setup_error(self, mock_round, mock_evo):
|
|
||||||
"""Test the EU setup with errors."""
|
|
||||||
config = {
|
|
||||||
CONF_USERNAME: "user",
|
|
||||||
CONF_PASSWORD: "pass",
|
|
||||||
honeywell.CONF_REGION: "eu",
|
|
||||||
}
|
|
||||||
mock_evo.return_value.temperatures.side_effect = (
|
|
||||||
requests.exceptions.RequestException
|
|
||||||
)
|
|
||||||
add_entities = mock.MagicMock()
|
|
||||||
hass = mock.MagicMock()
|
|
||||||
assert not honeywell.setup_platform(hass, config, add_entities)
|
|
||||||
|
|
||||||
|
|
||||||
class TestHoneywellRound(unittest.TestCase):
|
|
||||||
"""A test class for Honeywell Round thermostats."""
|
|
||||||
|
|
||||||
def setup_method(self, method):
|
|
||||||
"""Test the setup method."""
|
|
||||||
|
|
||||||
def fake_temperatures(force_refresh=None):
|
|
||||||
"""Create fake temperatures."""
|
|
||||||
temps = [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"temp": 20,
|
|
||||||
"setpoint": 21,
|
|
||||||
"thermostat": "main",
|
|
||||||
"name": "House",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"temp": 21,
|
|
||||||
"setpoint": 22,
|
|
||||||
"thermostat": "DOMESTIC_HOT_WATER",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
return temps
|
|
||||||
|
|
||||||
self.device = mock.MagicMock()
|
|
||||||
self.device.temperatures.side_effect = fake_temperatures
|
|
||||||
self.round1 = honeywell.RoundThermostat(self.device, "1", True, 16)
|
|
||||||
self.round1.update()
|
|
||||||
self.round2 = honeywell.RoundThermostat(self.device, "2", False, 17)
|
|
||||||
self.round2.update()
|
|
||||||
|
|
||||||
def test_attributes(self):
|
|
||||||
"""Test the attributes."""
|
|
||||||
assert self.round1.name == "House"
|
|
||||||
assert self.round1.temperature_unit == TEMP_CELSIUS
|
|
||||||
assert self.round1.current_temperature == 20
|
|
||||||
assert self.round1.target_temperature == 21
|
|
||||||
assert not self.round1.is_away_mode_on
|
|
||||||
|
|
||||||
assert self.round2.name == "Hot Water"
|
|
||||||
assert self.round2.temperature_unit == TEMP_CELSIUS
|
|
||||||
assert self.round2.current_temperature == 21
|
|
||||||
assert self.round2.target_temperature is None
|
|
||||||
assert not self.round2.is_away_mode_on
|
|
||||||
|
|
||||||
def test_away_mode(self):
|
|
||||||
"""Test setting the away mode."""
|
|
||||||
assert not self.round1.is_away_mode_on
|
|
||||||
self.round1.turn_away_mode_on()
|
|
||||||
assert self.round1.is_away_mode_on
|
|
||||||
assert self.device.set_temperature.call_count == 1
|
|
||||||
assert self.device.set_temperature.call_args == mock.call("House", 16)
|
|
||||||
|
|
||||||
self.device.set_temperature.reset_mock()
|
|
||||||
self.round1.turn_away_mode_off()
|
|
||||||
assert not self.round1.is_away_mode_on
|
|
||||||
assert self.device.cancel_temp_override.call_count == 1
|
|
||||||
assert self.device.cancel_temp_override.call_args == mock.call("House")
|
|
||||||
|
|
||||||
def test_set_temperature(self):
|
|
||||||
"""Test setting the temperature."""
|
|
||||||
self.round1.set_temperature(temperature=25)
|
|
||||||
assert self.device.set_temperature.call_count == 1
|
|
||||||
assert self.device.set_temperature.call_args == mock.call("House", 25)
|
|
||||||
|
|
||||||
def test_set_hvac_mode(self) -> None:
|
|
||||||
"""Test setting the system operation."""
|
|
||||||
self.round1.set_hvac_mode("cool")
|
|
||||||
assert self.round1.current_operation == "cool"
|
|
||||||
assert self.device.system_mode == "cool"
|
|
||||||
|
|
||||||
self.round1.set_hvac_mode("heat")
|
|
||||||
assert self.round1.current_operation == "heat"
|
|
||||||
assert self.device.system_mode == "heat"
|
|
||||||
|
|
||||||
|
|
||||||
class TestHoneywellUS(unittest.TestCase):
|
|
||||||
"""A test class for Honeywell US thermostats."""
|
|
||||||
|
|
||||||
def setup_method(self, method):
|
|
||||||
"""Test the setup method."""
|
|
||||||
self.client = mock.MagicMock()
|
|
||||||
self.device = mock.MagicMock()
|
|
||||||
self.cool_away_temp = 18
|
|
||||||
self.heat_away_temp = 28
|
|
||||||
self.honeywell = honeywell.HoneywellUSThermostat(
|
|
||||||
self.client,
|
|
||||||
self.device,
|
|
||||||
self.cool_away_temp,
|
|
||||||
self.heat_away_temp,
|
|
||||||
"user",
|
|
||||||
"password",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.device.fan_running = True
|
|
||||||
self.device.name = "test"
|
|
||||||
self.device.temperature_unit = "F"
|
|
||||||
self.device.current_temperature = 72
|
|
||||||
self.device.setpoint_cool = 78
|
|
||||||
self.device.setpoint_heat = 65
|
|
||||||
self.device.system_mode = "heat"
|
|
||||||
self.device.fan_mode = "auto"
|
|
||||||
|
|
||||||
def test_properties(self):
|
|
||||||
"""Test the properties."""
|
|
||||||
assert self.honeywell.is_fan_on
|
|
||||||
assert self.honeywell.name == "test"
|
|
||||||
assert self.honeywell.current_temperature == 72
|
|
||||||
|
|
||||||
def test_unit_of_measurement(self):
|
|
||||||
"""Test the unit of measurement."""
|
|
||||||
assert self.honeywell.temperature_unit == TEMP_FAHRENHEIT
|
|
||||||
self.device.temperature_unit = "C"
|
|
||||||
assert self.honeywell.temperature_unit == TEMP_CELSIUS
|
|
||||||
|
|
||||||
def test_target_temp(self):
|
|
||||||
"""Test the target temperature."""
|
|
||||||
assert self.honeywell.target_temperature == 65
|
|
||||||
self.device.system_mode = "cool"
|
|
||||||
assert self.honeywell.target_temperature == 78
|
|
||||||
|
|
||||||
def test_set_temp(self):
|
|
||||||
"""Test setting the temperature."""
|
|
||||||
self.honeywell.set_temperature(temperature=70)
|
|
||||||
assert self.device.setpoint_heat == 70
|
|
||||||
assert self.honeywell.target_temperature == 70
|
|
||||||
|
|
||||||
self.device.system_mode = "cool"
|
|
||||||
assert self.honeywell.target_temperature == 78
|
|
||||||
self.honeywell.set_temperature(temperature=74)
|
|
||||||
assert self.device.setpoint_cool == 74
|
|
||||||
assert self.honeywell.target_temperature == 74
|
|
||||||
|
|
||||||
def test_set_hvac_mode(self) -> None:
|
|
||||||
"""Test setting the operation mode."""
|
|
||||||
self.honeywell.set_hvac_mode("cool")
|
|
||||||
assert self.device.system_mode == "cool"
|
|
||||||
|
|
||||||
self.honeywell.set_hvac_mode("heat")
|
|
||||||
assert self.device.system_mode == "heat"
|
|
||||||
|
|
||||||
def test_set_temp_fail(self):
|
|
||||||
"""Test if setting the temperature fails."""
|
|
||||||
self.device.setpoint_heat = mock.MagicMock(
|
|
||||||
side_effect=somecomfort.SomeComfortError
|
|
||||||
)
|
|
||||||
self.honeywell.set_temperature(temperature=123)
|
|
||||||
|
|
||||||
def test_attributes(self):
|
|
||||||
"""Test the attributes."""
|
|
||||||
expected = {
|
|
||||||
honeywell.ATTR_FAN: "running",
|
|
||||||
ATTR_FAN_MODE: "auto",
|
|
||||||
ATTR_FAN_MODES: somecomfort.FAN_MODES,
|
|
||||||
ATTR_HVAC_MODES: somecomfort.SYSTEM_MODES,
|
|
||||||
}
|
|
||||||
assert expected == self.honeywell.extra_state_attributes
|
|
||||||
expected["fan"] = "idle"
|
|
||||||
self.device.fan_running = False
|
|
||||||
assert self.honeywell.extra_state_attributes == expected
|
|
||||||
|
|
||||||
def test_with_no_fan(self):
|
|
||||||
"""Test if there is on fan."""
|
|
||||||
self.device.fan_running = False
|
|
||||||
self.device.fan_mode = None
|
|
||||||
expected = {
|
|
||||||
honeywell.ATTR_FAN: "idle",
|
|
||||||
ATTR_FAN_MODE: None,
|
|
||||||
ATTR_FAN_MODES: somecomfort.FAN_MODES,
|
|
||||||
ATTR_HVAC_MODES: somecomfort.SYSTEM_MODES,
|
|
||||||
}
|
|
||||||
assert self.honeywell.extra_state_attributes == expected
|
|
||||||
|
|
||||||
def test_heat_away_mode(self):
|
|
||||||
"""Test setting the heat away mode."""
|
|
||||||
self.honeywell.set_hvac_mode("heat")
|
|
||||||
assert not self.honeywell.is_away_mode_on
|
|
||||||
self.honeywell.turn_away_mode_on()
|
|
||||||
assert self.honeywell.is_away_mode_on
|
|
||||||
assert self.device.setpoint_heat == self.heat_away_temp
|
|
||||||
assert self.device.hold_heat is True
|
|
||||||
|
|
||||||
self.honeywell.turn_away_mode_off()
|
|
||||||
assert not self.honeywell.is_away_mode_on
|
|
||||||
assert self.device.hold_heat is False
|
|
||||||
|
|
||||||
@mock.patch("somecomfort.SomeComfort")
|
|
||||||
def test_retry(self, test_somecomfort):
|
|
||||||
"""Test retry connection."""
|
|
||||||
old_device = self.honeywell._device
|
|
||||||
self.honeywell._retry()
|
|
||||||
assert self.honeywell._device == old_device
|
|
63
tests/components/honeywell/test_config_flow.py
Normal file
63
tests/components/honeywell/test_config_flow.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
"""Tests for honeywell config flow."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import somecomfort
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.honeywell.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
FAKE_CONFIG = {
|
||||||
|
"username": "fake",
|
||||||
|
"password": "user",
|
||||||
|
"away_cool_temperature": 88,
|
||||||
|
"away_heat_temperature": 61,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_authenticate_form(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that the config form is shown."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connection_error(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that an error message is shown on login fail."""
|
||||||
|
with patch(
|
||||||
|
"somecomfort.SomeComfort",
|
||||||
|
side_effect=somecomfort.AuthError,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG
|
||||||
|
)
|
||||||
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""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
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["data"] == FAKE_CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_step_import(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that the import step works."""
|
||||||
|
with patch(
|
||||||
|
"somecomfort.SomeComfort",
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=FAKE_CONFIG
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["data"] == FAKE_CONFIG
|
8
tests/components/honeywell/test_init.py
Normal file
8
tests/components/honeywell/test_init.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
"""Test honeywell setup process."""
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry(hass, config_entry):
|
||||||
|
"""Initialize the config entry."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
Loading…
Add table
Add a link
Reference in a new issue