From 4347a0f6b7a24ee244d50462d85702fb89209b67 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 10 May 2019 17:34:28 +0100 Subject: [PATCH] Centralize geniushub updates (#23764) * add hub/parent/manager * add hub/parent/manager 2 * working now * delint * add back water heater * make water_heater a child * make water_heater a child - delint * make water_heater a child - delint 2 * improve turn_on logic, and small tidy-up * improve turn_on logic 2 * improve turn_on logic 3 - better docstring * improve turn_on logic 3 - better docstring * remove unnecessary DICT.get()s * remove unnecessary DICT.get()s 2 * code tidy-up * de-lint * refactor for GeniusData * refactor for GeniusData 2 * code tidy-up * add missing should_poll = False --- .../components/geniushub/__init__.py | 51 +++++++--- homeassistant/components/geniushub/climate.py | 98 ++++++++++--------- .../components/geniushub/water_heater.py | 52 +++++----- 3 files changed, 115 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 181e61a7e48..69c213c6aa5 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,18 +1,25 @@ -"""This module connects to a Genius hub and shares the data.""" +"""Support for a Genius Hub system.""" +from datetime import timedelta import logging import voluptuous as vol +from geniushubclient import GeniusHubClient + from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) DOMAIN = 'geniushub' +SCAN_INTERVAL = timedelta(seconds=60) + _V1_API_SCHEMA = vol.Schema({ vol.Required(CONF_TOKEN): cv.string, }) @@ -31,33 +38,45 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, hass_config): """Create a Genius Hub system.""" - from geniushubclient import GeniusHubClient # noqa; pylint: disable=no-name-in-module - - geniushub_data = hass.data[DOMAIN] = {} - kwargs = dict(hass_config[DOMAIN]) if CONF_HOST in kwargs: args = (kwargs.pop(CONF_HOST), ) else: args = (kwargs.pop(CONF_TOKEN), ) + hass.data[DOMAIN] = {} + data = hass.data[DOMAIN]['data'] = GeniusData(hass, args, kwargs) try: - client = geniushub_data['client'] = GeniusHubClient( - *args, **kwargs, session=async_get_clientsession(hass) - ) - - await client.hub.update() - + await data._client.hub.update() # pylint: disable=protected-access except AssertionError: # assert response.status == HTTP_OK _LOGGER.warning( - "setup(): Failed, check your configuration.", + "Setup failed, check your configuration.", exc_info=True) return False - hass.async_create_task(async_load_platform( - hass, 'climate', DOMAIN, {}, hass_config)) + async_track_time_interval(hass, data.async_update, SCAN_INTERVAL) - hass.async_create_task(async_load_platform( - hass, 'water_heater', DOMAIN, {}, hass_config)) + for platform in ['climate', 'water_heater']: + hass.async_create_task(async_load_platform( + hass, platform, DOMAIN, {}, hass_config)) return True + + +class GeniusData: + """Container for geniushub client and data.""" + + def __init__(self, hass, args, kwargs): + """Initialize the geniushub client.""" + self._hass = hass + self._client = hass.data[DOMAIN]['client'] = GeniusHubClient( + *args, **kwargs, session=async_get_clientsession(hass)) + + async def async_update(self, now, **kwargs): + """Update the geniushub client's data.""" + try: + await self._client.hub.update() + except AssertionError: # assert response.status == HTTP_OK + _LOGGER.warning("Update failed.", exc_info=True) + return + async_dispatcher_send(self._hass, DOMAIN) diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index b396f8d6dac..22761f6b184 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,5 +1,4 @@ """Support for Genius Hub climate devices.""" -import asyncio import logging from homeassistant.components.climate import ClimateDevice @@ -7,30 +6,33 @@ from homeassistant.components.climate.const import ( STATE_AUTO, STATE_ECO, STATE_HEAT, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_ON_OFF) from homeassistant.const import ( - ATTR_TEMPERATURE, TEMP_CELSIUS) + ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN _LOGGER = logging.getLogger(__name__) -GH_CLIMATE_DEVICES = ['radiator'] +GH_ZONES = ['radiator'] -GENIUSHUB_SUPPORT_FLAGS = \ +GH_SUPPORT_FLAGS = \ SUPPORT_TARGET_TEMPERATURE | \ SUPPORT_ON_OFF | \ SUPPORT_OPERATION_MODE -GENIUSHUB_MAX_TEMP = 28.0 -GENIUSHUB_MIN_TEMP = 4.0 +GH_MAX_TEMP = 28.0 +GH_MIN_TEMP = 4.0 # Genius Hub Zones support only Off, Override/Boost, Footprint & Timer modes HA_OPMODE_TO_GH = { + STATE_OFF: 'off', STATE_AUTO: 'timer', STATE_ECO: 'footprint', STATE_MANUAL: 'override', } -GH_OPMODE_OFF = 'off' GH_STATE_TO_HA = { + 'off': STATE_OFF, 'timer': STATE_AUTO, 'footprint': STATE_ECO, 'away': None, @@ -39,10 +41,9 @@ GH_STATE_TO_HA = { 'test': None, 'linked': None, 'other': None, -} # intentionally missing 'off': None - +} # temperature is repeated here, as it gives access to high-precision temps -GH_DEVICE_STATE_ATTRS = ['temperature', 'type', 'occupied', 'override'] +GH_STATE_ATTRS = ['temperature', 'type', 'occupied', 'override'] async def async_setup_platform(hass, hass_config, async_add_entities, @@ -50,60 +51,73 @@ async def async_setup_platform(hass, hass_config, async_add_entities, """Set up the Genius Hub climate entities.""" client = hass.data[DOMAIN]['client'] - entities = [GeniusClimate(client, z) - for z in client.hub.zone_objs if z.type in GH_CLIMATE_DEVICES] - - async_add_entities(entities) + async_add_entities([GeniusClimateZone(client, z) + for z in client.hub.zone_objs if z.type in GH_ZONES]) -class GeniusClimate(ClimateDevice): +class GeniusClimateZone(ClimateDevice): """Representation of a Genius Hub climate device.""" def __init__(self, client, zone): """Initialize the climate device.""" self._client = client - self._objref = zone - self._id = zone.id - self._name = zone.name + self._zone = zone # Only some zones have movement detectors, which allows footprint mode op_list = list(HA_OPMODE_TO_GH) - if not hasattr(self._objref, 'occupied'): + if not hasattr(self._zone, 'occupied'): op_list.remove(STATE_ECO) self._operation_list = op_list + self._supported_features = GH_SUPPORT_FLAGS + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self): + self.async_schedule_update_ha_state(force_refresh=True) @property def name(self): """Return the name of the climate device.""" - return self._objref.name + return self._zone.name @property def device_state_attributes(self): """Return the device state attributes.""" - tmp = self._objref.__dict__.items() - state = {k: v for k, v in tmp if k in GH_DEVICE_STATE_ATTRS} + tmp = self._zone.__dict__.items() + return {'status': {k: v for k, v in tmp if k in GH_STATE_ATTRS}} - return {'status': state} + @property + def should_poll(self) -> bool: + """Return False as the geniushub devices should not be polled.""" + return False + + @property + def icon(self): + """Return the icon to use in the frontend UI.""" + return "mdi:radiator" @property def current_temperature(self): """Return the current temperature.""" - return self._objref.temperature + return self._zone.temperature @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._objref.setpoint + return self._zone.setpoint @property def min_temp(self): """Return max valid temperature that can be set.""" - return GENIUSHUB_MIN_TEMP + return GH_MIN_TEMP @property def max_temp(self): """Return max valid temperature that can be set.""" - return GENIUSHUB_MAX_TEMP + return GH_MAX_TEMP @property def temperature_unit(self): @@ -113,7 +127,7 @@ class GeniusClimate(ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return GENIUSHUB_SUPPORT_FLAGS + return self._supported_features @property def operation_list(self): @@ -123,34 +137,30 @@ class GeniusClimate(ClimateDevice): @property def current_operation(self): """Return the current operation mode.""" - return GH_STATE_TO_HA.get(self._objref.mode) + return GH_STATE_TO_HA[self._zone.mode] @property def is_on(self): """Return True if the device is on.""" - return self._objref.mode in GH_STATE_TO_HA + return self._zone.mode != HA_OPMODE_TO_GH[STATE_OFF] async def async_set_operation_mode(self, operation_mode): """Set a new operation mode for this zone.""" - await self._objref.set_mode(HA_OPMODE_TO_GH.get(operation_mode)) + await self._zone.set_mode(HA_OPMODE_TO_GH[operation_mode]) async def async_set_temperature(self, **kwargs): """Set a new target temperature for this zone.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - await self._objref.set_override(temperature, 3600) # 1 hour + await self._zone.set_override(kwargs.get(ATTR_TEMPERATURE), 3600) async def async_turn_on(self): - """Turn on this heating zone.""" - await self._objref.set_mode(HA_OPMODE_TO_GH.get(STATE_AUTO)) + """Turn on this heating zone. + + Set a Zone to Footprint mode if they have a Room sensor, and to Timer + mode otherwise. + """ + mode = STATE_ECO if hasattr(self._zone, 'occupied') else STATE_AUTO + await self._zone.set_mode(HA_OPMODE_TO_GH[mode]) async def async_turn_off(self): """Turn off this heating zone (i.e. to frost protect).""" - await self._objref.set_mode(GH_OPMODE_OFF) - - async def async_update(self): - """Get the latest data from the hub.""" - try: - await self._objref.update() - except (AssertionError, asyncio.TimeoutError) as err: - _LOGGER.warning("Update for %s failed, message: %s", - self._id, err) + await self._zone.set_mode(HA_OPMODE_TO_GH[STATE_OFF]) diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index f5f09f9b1d5..6efbed514ee 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,5 +1,4 @@ """Support for Genius Hub water_heater devices.""" -import asyncio import logging from homeassistant.components.water_heater import ( @@ -7,6 +6,8 @@ from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.const import ( ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN @@ -15,15 +16,15 @@ STATE_MANUAL = 'manual' _LOGGER = logging.getLogger(__name__) -GH_WATER_HEATERS = ['hot water temperature'] +GH_HEATERS = ['hot water temperature'] -GENIUSHUB_SUPPORT_FLAGS = \ +GH_SUPPORT_FLAGS = \ SUPPORT_TARGET_TEMPERATURE | \ SUPPORT_OPERATION_MODE # HA does not have SUPPORT_ON_OFF for water_heater -GENIUSHUB_MAX_TEMP = 80.0 -GENIUSHUB_MIN_TEMP = 30.0 +GH_MAX_TEMP = 80.0 +GH_MIN_TEMP = 30.0 # Genius Hub HW supports only Off, Override/Boost & Timer modes HA_OPMODE_TO_GH = { @@ -31,7 +32,6 @@ HA_OPMODE_TO_GH = { STATE_AUTO: 'timer', STATE_MANUAL: 'override', } -GH_OPMODE_OFF = 'off' GH_STATE_TO_HA = { 'off': STATE_OFF, 'timer': STATE_AUTO, @@ -43,8 +43,7 @@ GH_STATE_TO_HA = { 'linked': None, 'other': None, } - -GH_DEVICE_STATE_ATTRS = ['type', 'override'] +GH_STATE_ATTRS = ['type', 'override'] async def async_setup_platform(hass, hass_config, async_add_entities, @@ -53,7 +52,7 @@ async def async_setup_platform(hass, hass_config, async_add_entities, client = hass.data[DOMAIN]['client'] entities = [GeniusWaterHeater(client, z) - for z in client.hub.zone_objs if z.type in GH_WATER_HEATERS] + for z in client.hub.zone_objs if z.type in GH_HEATERS] async_add_entities(entities) @@ -65,11 +64,17 @@ class GeniusWaterHeater(WaterHeaterDevice): """Initialize the water_heater device.""" self._client = client self._boiler = boiler - self._id = boiler.id - self._name = boiler.name self._operation_list = list(HA_OPMODE_TO_GH) + async def async_added_to_hass(self): + """Run when entity about to be added.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self): + self.async_schedule_update_ha_state(force_refresh=True) + @property def name(self): """Return the name of the water_heater device.""" @@ -79,9 +84,12 @@ class GeniusWaterHeater(WaterHeaterDevice): def device_state_attributes(self): """Return the device state attributes.""" tmp = self._boiler.__dict__.items() - state = {k: v for k, v in tmp if k in GH_DEVICE_STATE_ATTRS} + return {'status': {k: v for k, v in tmp if k in GH_STATE_ATTRS}} - return {'status': state} + @property + def should_poll(self) -> bool: + """Return False as the geniushub devices should not be polled.""" + return False @property def current_temperature(self): @@ -96,12 +104,12 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def min_temp(self): """Return max valid temperature that can be set.""" - return GENIUSHUB_MIN_TEMP + return GH_MIN_TEMP @property def max_temp(self): """Return max valid temperature that can be set.""" - return GENIUSHUB_MAX_TEMP + return GH_MAX_TEMP @property def temperature_unit(self): @@ -111,7 +119,7 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def supported_features(self): """Return the list of supported features.""" - return GENIUSHUB_SUPPORT_FLAGS + return GH_SUPPORT_FLAGS @property def operation_list(self): @@ -121,21 +129,13 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def current_operation(self): """Return the current operation mode.""" - return GH_STATE_TO_HA.get(self._boiler.mode) + return GH_STATE_TO_HA[self._boiler.mode] async def async_set_operation_mode(self, operation_mode): """Set a new operation mode for this boiler.""" - await self._boiler.set_mode(HA_OPMODE_TO_GH.get(operation_mode)) + await self._boiler.set_mode(HA_OPMODE_TO_GH[operation_mode]) async def async_set_temperature(self, **kwargs): """Set a new target temperature for this boiler.""" temperature = kwargs[ATTR_TEMPERATURE] await self._boiler.set_override(temperature, 3600) # 1 hour - - async def async_update(self): - """Get the latest data from the hub.""" - try: - await self._boiler.update() - except (AssertionError, asyncio.TimeoutError) as err: - _LOGGER.warning("Update for %s failed, message: %s", - self._id, err)