core/homeassistant/components/honeywell/__init__.py

206 lines
6.5 KiB
Python

"""Support for Honeywell (US) Total Connect Comfort climate systems."""
import asyncio
from datetime import timedelta
import somecomfort
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.util import Throttle
from .const import (
_LOGGER,
CONF_COOL_AWAY_TEMPERATURE,
CONF_DEV_ID,
CONF_HEAT_AWAY_TEMPERATURE,
CONF_LOC_ID,
DOMAIN,
)
UPDATE_LOOP_SLEEP_TIME = 5
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE}
@callback
def _async_migrate_data_to_options(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
if not MIGRATE_OPTIONS_KEYS.intersection(config_entry.data):
return
hass.config_entries.async_update_entry(
config_entry,
data={
k: v for k, v in config_entry.data.items() if k not in MIGRATE_OPTIONS_KEYS
},
options={
**config_entry.options,
**{k: config_entry.data.get(k) for k in MIGRATE_OPTIONS_KEYS},
},
)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the Honeywell thermostat."""
_async_migrate_data_to_options(hass, config_entry)
username = config_entry.data[CONF_USERNAME]
password = config_entry.data[CONF_PASSWORD]
client = await hass.async_add_executor_job(
get_somecomfort_client, username, password
)
if client is None:
return False
loc_id = config_entry.data.get(CONF_LOC_ID)
dev_id = config_entry.data.get(CONF_DEV_ID)
devices = {}
for location in client.locations_by_id.values():
if not loc_id or location.locationid == loc_id:
for device in location.devices_by_id.values():
if not dev_id or device.deviceid == dev_id:
devices[device.deviceid] = device
if len(devices) == 0:
_LOGGER.debug("No devices found")
return False
data = HoneywellData(hass, config_entry, client, username, password, devices)
await data.async_update()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = data
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
return True
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Update listener."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload the config config and platforms."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
if unload_ok:
hass.data.pop(DOMAIN)
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:
"""Get the latest data and update."""
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
client: somecomfort.SomeComfort,
username: str,
password: str,
devices: dict[str, somecomfort.Device],
) -> None:
"""Initialize the data object."""
self._hass = hass
self._config = config_entry
self._client = client
self._username = username
self._password = password
self.devices = devices
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
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)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
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)