mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
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:
parent
f0ba7a3795
commit
5e6ba594aa
@ -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
|
||||||
|
@ -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)
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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": {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user