* Added Zones, and removed available() logic flesh out Zones tidy up init some more tidying up Nearly there - full functionality passed txo - ready to send PR Ready to PR, except to remove logging Add Zones and associated functionality to evohome component Add Zones to evohome (some more tidying up) Add Zones to evohome (Nearly there - full functionality) Add Zones to evohome (passed tox) Add Zones to evohome (except to remove logging) Add Zones and associated functionality to evohome component Revert _LOGGER.warn to .debug, as it should be Cleanup stupid REBASE * removed a duplicate/unwanted code block * tidy up comment * use async_added_to_hass instead of bus.listen * Pass evo_data instead of hass when instntiating * switch to async version of setup_platform/add_entities * Remove workaround for bug in client library - using github version for now, as awaiting new PyPi package * Avoid invalid-name lint - use 'zone_idx' instead of 'z' * Fix line too long error * remove commented-out line of code * fix a logic error, improve REDACTION of potentially-sensitive infomation * restore use of EVENT_HOMEASSISTANT_START to improve HA startup time * added a docstring to _flatten_json * Switch instantiation from component to platform * Use v0.2.8 of client api (resolves logging bug) * import rather than duplicate, and de-lint * We use evohomeclient v0.2.8 now * remove all the api logging * Changed scan_interal to Throttle * added a configurable scan_interval * small code tidy-up, removed sub-function * tidy up update() code * minimize use of self.hass.data[] * remove lint * remove unwanted logging * remove debug code * correct a small coding error * small tidyup of code * remove flatten_json * add @callback to _first_update() * switch back to load_platform * adhere to standards fro logging * use new format string formatting * minor change to comments * convert scan_interval to timedelta from int * restore rounding up of scan_interval * code tidy up * sync when in sync context * fix typo * remove raises not needed * tidy up typos, etc. * remove invalid-name lint * tidy up exception handling * de-lint/pretty-fy * move 'status' to a JSON node, so theirs room for 'config', 'schedule' in the future
163 lines
5.6 KiB
Python
163 lines
5.6 KiB
Python
"""Support for (EMEA/EU-based) Honeywell evohome systems.
|
|
|
|
Support for a temperature control system (TCS, controller) with 0+ heating
|
|
zones (e.g. TRVs, relays) and, optionally, a DHW controller.
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
https://home-assistant.io/components/evohome/
|
|
"""
|
|
|
|
# Glossary:
|
|
# TCS - temperature control system (a.k.a. Controller, Parent), which can
|
|
# have up to 13 Children:
|
|
# 0-12 Heating zones (a.k.a. Zone), and
|
|
# 0-1 DHW controller, (a.k.a. Boiler)
|
|
# The TCS & Zones are implemented as Climate devices, Boiler as a WaterHeater
|
|
|
|
from datetime import timedelta
|
|
import logging
|
|
|
|
from requests.exceptions import HTTPError
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import (
|
|
CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD,
|
|
EVENT_HOMEASSISTANT_START,
|
|
HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS
|
|
)
|
|
from homeassistant.core import callback
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.discovery import load_platform
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
|
|
REQUIREMENTS = ['evohomeclient==0.2.8']
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DOMAIN = 'evohome'
|
|
DATA_EVOHOME = 'data_' + DOMAIN
|
|
DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN
|
|
|
|
CONF_LOCATION_IDX = 'location_idx'
|
|
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
|
|
SCAN_INTERVAL_MINIMUM = timedelta(seconds=180)
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
DOMAIN: vol.Schema({
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
vol.Optional(CONF_LOCATION_IDX, default=0):
|
|
cv.positive_int,
|
|
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT):
|
|
vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)),
|
|
}),
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
# These are used to help prevent E501 (line too long) violations.
|
|
GWS = 'gateways'
|
|
TCS = 'temperatureControlSystems'
|
|
|
|
# bit masks for dispatcher packets
|
|
EVO_PARENT = 0x01
|
|
EVO_CHILD = 0x02
|
|
|
|
|
|
def setup(hass, hass_config):
|
|
"""Create a (EMEA/EU-based) Honeywell evohome system.
|
|
|
|
Currently, only the Controller and the Zones are implemented here.
|
|
"""
|
|
evo_data = hass.data[DATA_EVOHOME] = {}
|
|
evo_data['timers'] = {}
|
|
|
|
# use a copy, since scan_interval is rounded up to nearest 60s
|
|
evo_data['params'] = dict(hass_config[DOMAIN])
|
|
scan_interval = evo_data['params'][CONF_SCAN_INTERVAL]
|
|
scan_interval = timedelta(
|
|
minutes=(scan_interval.total_seconds() + 59) // 60)
|
|
|
|
from evohomeclient2 import EvohomeClient
|
|
|
|
try:
|
|
client = EvohomeClient(
|
|
evo_data['params'][CONF_USERNAME],
|
|
evo_data['params'][CONF_PASSWORD],
|
|
debug=False
|
|
)
|
|
|
|
except HTTPError as err:
|
|
if err.response.status_code == HTTP_BAD_REQUEST:
|
|
_LOGGER.error(
|
|
"setup(): Failed to connect with the vendor's web servers. "
|
|
"Check your username (%s), and password are correct."
|
|
"Unable to continue. Resolve any errors and restart HA.",
|
|
evo_data['params'][CONF_USERNAME]
|
|
)
|
|
|
|
elif err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
|
|
_LOGGER.error(
|
|
"setup(): Failed to connect with the vendor's web servers. "
|
|
"The server is not contactable. Unable to continue. "
|
|
"Resolve any errors and restart HA."
|
|
)
|
|
|
|
elif err.response.status_code == HTTP_TOO_MANY_REQUESTS:
|
|
_LOGGER.error(
|
|
"setup(): Failed to connect with the vendor's web servers. "
|
|
"You have exceeded the api rate limit. Unable to continue. "
|
|
"Wait a while (say 10 minutes) and restart HA."
|
|
)
|
|
|
|
else:
|
|
raise # we dont expect/handle any other HTTPErrors
|
|
|
|
return False # unable to continue
|
|
|
|
finally: # Redact username, password as no longer needed
|
|
evo_data['params'][CONF_USERNAME] = 'REDACTED'
|
|
evo_data['params'][CONF_PASSWORD] = 'REDACTED'
|
|
|
|
evo_data['client'] = client
|
|
evo_data['status'] = {}
|
|
|
|
# Redact any installation data we'll never need
|
|
for loc in client.installation_info:
|
|
loc['locationInfo']['locationId'] = 'REDACTED'
|
|
loc['locationInfo']['locationOwner'] = 'REDACTED'
|
|
loc['locationInfo']['streetAddress'] = 'REDACTED'
|
|
loc['locationInfo']['city'] = 'REDACTED'
|
|
loc[GWS][0]['gatewayInfo'] = 'REDACTED'
|
|
|
|
# Pull down the installation configuration
|
|
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
|
|
|
|
try:
|
|
evo_data['config'] = client.installation_info[loc_idx]
|
|
except IndexError:
|
|
_LOGGER.warning(
|
|
"setup(): Parameter '%s'=%s, is outside its range (0-%s)",
|
|
CONF_LOCATION_IDX,
|
|
loc_idx,
|
|
len(client.installation_info) - 1
|
|
)
|
|
return False # unable to continue
|
|
|
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
|
tmp_loc = dict(evo_data['config'])
|
|
tmp_loc['locationInfo']['postcode'] = 'REDACTED'
|
|
if 'dhw' in tmp_loc[GWS][0][TCS][0]: # if this location has DHW...
|
|
tmp_loc[GWS][0][TCS][0]['dhw'] = '...'
|
|
|
|
_LOGGER.debug("setup(): evo_data['config']=%s", tmp_loc)
|
|
|
|
load_platform(hass, 'climate', DOMAIN, {}, hass_config)
|
|
|
|
@callback
|
|
def _first_update(event):
|
|
# When HA has started, the hub knows to retreive it's first update
|
|
pkt = {'sender': 'setup()', 'signal': 'refresh', 'to': EVO_PARENT}
|
|
async_dispatcher_send(hass, DISPATCHER_EVOHOME, pkt)
|
|
|
|
hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update)
|
|
|
|
return True
|