From e02a5f0b313b19a3ad021653164c4e431281ddb5 Mon Sep 17 00:00:00 2001 From: GeoffAtHome Date: Tue, 16 Apr 2019 23:54:46 +0200 Subject: [PATCH] Genius hub (#21598) * Adding Genius Hub * Added Genius hub * Correct hound errors * Correct hound errors. * Correct tox errors. * Fix travis errors * Correct sensor names * Correct travis errors * Correct hound errors * Follow up from code review by Martin Hjelmare * More changes from code review. * Attempt to resolve conflicts in requirements_all * de-lint for the houndci-bot * better logging message, and small tidy-up * minor refactor and de-lint * domain name should be the same as the component name * use self where appropriate * minor de-lint * add entities as a single call * de-lint * all read-only attrs almost done * refactor - near the end * change state/,ode mapping * override temp from curr_temp * all read-only properties working * working now * ready for PR, but need to remove logging * de-lint * de-linted, ready for merge * de-linted, ready for merge 2 * didn't like import in climate/__init__ * improve footprint logic * add manifest.json * add manifest.json 2 * correct a regression * fix regression with device.is_on() * use latest client library * update to latest client library, 3.3.6 * delint and shoudl be OK to go --- .coveragerc | 1 + .../components/geniushub/__init__.py | 52 ++++++ homeassistant/components/geniushub/climate.py | 154 ++++++++++++++++++ .../components/geniushub/manifest.json | 10 ++ requirements_all.txt | 3 + 5 files changed, 220 insertions(+) create mode 100644 homeassistant/components/geniushub/__init__.py create mode 100644 homeassistant/components/geniushub/climate.py create mode 100644 homeassistant/components/geniushub/manifest.json diff --git a/.coveragerc b/.coveragerc index 43f0274190f..077b6bf5753 100644 --- a/.coveragerc +++ b/.coveragerc @@ -206,6 +206,7 @@ omit = homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py homeassistant/components/gc100/* + homeassistant/components/geniushub/* homeassistant/components/gearbest/sensor.py homeassistant/components/geizhals/sensor.py homeassistant/components/github/sensor.py diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py new file mode 100644 index 00000000000..90e04db0111 --- /dev/null +++ b/homeassistant/components/geniushub/__init__.py @@ -0,0 +1,52 @@ +"""This module connects to the Genius hub and shares the data.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, 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 + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'geniushub' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Required(CONF_HOST): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, hass_config): + """Create a Genius Hub system.""" + from geniushubclient import GeniusHubClient # noqa; pylint: disable=no-name-in-module + + host = hass_config[DOMAIN].get(CONF_HOST) + username = hass_config[DOMAIN].get(CONF_USERNAME) + password = hass_config[DOMAIN].get(CONF_PASSWORD) + + geniushub_data = hass.data[DOMAIN] = {} + + try: + client = geniushub_data['client'] = GeniusHubClient( + host, username, password, + session=async_get_clientsession(hass) + ) + + await client.hub.update() + + except AssertionError: # assert response.status == HTTP_OK + _LOGGER.warning( + "setup(): Failed, check your configuration.", + exc_info=True) + return False + + hass.async_create_task(async_load_platform( + hass, 'climate', DOMAIN, {}, hass_config)) + + return True diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py new file mode 100644 index 00000000000..bc72b73c0ed --- /dev/null +++ b/homeassistant/components/geniushub/climate.py @@ -0,0 +1,154 @@ +"""Supports Genius hub to provide climate controls.""" +import asyncio +import logging + +from homeassistant.components.climate import ClimateDevice +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) + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +GENIUSHUB_SUPPORT_FLAGS = \ + SUPPORT_TARGET_TEMPERATURE | \ + SUPPORT_ON_OFF | \ + SUPPORT_OPERATION_MODE + +GENIUSHUB_MAX_TEMP = 28.0 +GENIUSHUB_MIN_TEMP = 4.0 + +# Genius supports only Off, Override/Boost, Footprint & Timer modes +HA_OPMODE_TO_GH = { + STATE_AUTO: 'timer', + STATE_ECO: 'footprint', + STATE_MANUAL: 'override', +} +GH_OPMODE_OFF = 'off' +GH_STATE_TO_HA = { + 'timer': STATE_AUTO, + 'footprint': STATE_ECO, + 'away': None, + 'override': STATE_MANUAL, + 'early': STATE_HEAT, + 'test': None, + 'linked': None, + 'other': None, +} # intentionally missing 'off': None +GH_DEVICE_STATE_ATTRS = ['temperature', 'type', 'occupied', 'override'] + + +async def async_setup_platform(hass, hass_config, async_add_entities, + discovery_info=None): + """Set up the Genius hub climate devices.""" + client = hass.data[DOMAIN]['client'] + + zones = [] + for zone in client.hub.zone_objs: + if hasattr(zone, 'temperature'): + zones.append(GeniusClimate(client, zone)) + + async_add_entities(zones) + + +class GeniusClimate(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 + + # Only some zones have movement detectors, which allows footprint mode + op_list = list(HA_OPMODE_TO_GH) + if not hasattr(self._objref, 'occupied'): + op_list.remove(STATE_ECO) + self._operation_list = op_list + + @property + def name(self): + """Return the name of the climate device.""" + return self._objref.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} + + return {'status': state} + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._objref.temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._objref.setpoint + + @property + def min_temp(self): + """Return max valid temperature that can be set.""" + return GENIUSHUB_MIN_TEMP + + @property + def max_temp(self): + """Return max valid temperature that can be set.""" + return GENIUSHUB_MAX_TEMP + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def supported_features(self): + """Return the list of supported features.""" + return GENIUSHUB_SUPPORT_FLAGS + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self._operation_list + + @property + def current_operation(self): + """Return the current operation mode.""" + return GH_STATE_TO_HA.get(self._objref.mode) + + @property + def is_on(self): + """Return True if the device is on.""" + return self._objref.mode in GH_STATE_TO_HA + + 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)) + + 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 + + async def async_turn_on(self): + """Turn on this heating zone.""" + await self._objref.set_mode(HA_OPMODE_TO_GH.get(STATE_AUTO)) + + 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) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json new file mode 100644 index 00000000000..4546be8078b --- /dev/null +++ b/homeassistant/components/geniushub/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "geniushub", + "name": "Genius Hub", + "documentation": "https://www.home-assistant.io/components/geniushub", + "requirements": [ + "geniushub-client==0.3.6" + ], + "dependencies": [], + "codeowners": [] +} diff --git a/requirements_all.txt b/requirements_all.txt index 0662c6a00fe..93fb390a7b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,6 +458,9 @@ gearbest_parser==1.0.7 # homeassistant.components.geizhals geizhals==0.0.9 +# homeassistant.components.geniushub +geniushub-client==0.3.6 + # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed # homeassistant.components.usgs_earthquakes_feed