From 11dc7246afd4ba6dd2c9505926f9286ae98800f9 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 May 2017 03:23:36 +0300 Subject: [PATCH] Add Sensibo climate platform (#7379) * Add Sensibo climate platform * Force update after running a service * Add sensibo to .coveragerc * Use 10s timeout * Fix schema. Remove print. * Better handle unit conversions. --- .coveragerc | 1 + homeassistant/components/climate/__init__.py | 10 + homeassistant/components/climate/sensibo.py | 257 +++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 271 insertions(+) create mode 100644 homeassistant/components/climate/sensibo.py diff --git a/.coveragerc b/.coveragerc index f9de6c87c43..479973187ea 100644 --- a/.coveragerc +++ b/.coveragerc @@ -189,6 +189,7 @@ omit = homeassistant/components/climate/oem.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py + homeassistant/components/climate/sensibo.py homeassistant/components/cover/garadget.py homeassistant/components/cover/homematic.py homeassistant/components/cover/myq.py diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 10b5984500b..2e2dfbef8ca 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -49,6 +49,7 @@ ATTR_MAX_TEMP = 'max_temp' ATTR_MIN_TEMP = 'min_temp' ATTR_TARGET_TEMP_HIGH = 'target_temp_high' ATTR_TARGET_TEMP_LOW = 'target_temp_low' +ATTR_TARGET_TEMP_STEP = 'target_temp_step' ATTR_AWAY_MODE = 'away_mode' ATTR_AUX_HEAT = 'aux_heat' ATTR_FAN_MODE = 'fan_mode' @@ -419,6 +420,10 @@ class ClimateDevice(Entity): ATTR_TEMPERATURE: self._convert_for_display(self.target_temperature), } + + if self.target_temperature_step is not None: + data[ATTR_TARGET_TEMP_STEP] = self.target_temperature_step + target_temp_high = self.target_temperature_high if target_temp_high is not None: data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( @@ -505,6 +510,11 @@ class ClimateDevice(Entity): """Return the temperature we try to reach.""" return None + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return None + @property def target_temperature_high(self): """Return the highbound target temperature we try to reach.""" diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py new file mode 100644 index 00000000000..afb04fa3c91 --- /dev/null +++ b/homeassistant/components/climate/sensibo.py @@ -0,0 +1,257 @@ +""" +Support for Sensibo wifi-enabled home thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.sensibo/ +""" + +import asyncio +import logging + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.components.climate import ( + ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util.temperature import convert as convert_temperature + +REQUIREMENTS = ['pysensibo==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +ALL = 'all' +TIMEOUT = 10 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]), +}) + +_FETCH_FIELDS = ','.join([ + 'room{name}', 'measurements', 'remoteCapabilities', + 'acState', 'connectionStatus{isAlive}']) +_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up Sensibo devices.""" + import pysensibo + + client = pysensibo.SensiboClient( + config[CONF_API_KEY], session=async_get_clientsession(hass), + timeout=TIMEOUT) + devices = [] + try: + for dev in ( + yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)): + if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]: + devices.append(SensiboClimate(client, dev)) + except aiohttp.client_exceptions.ClientConnectorError: + _LOGGER.exception('Failed to connct to Sensibo servers.') + return False + + if devices: + async_add_devices(devices) + + +class SensiboClimate(ClimateDevice): + """Representation os a Sensibo device.""" + + def __init__(self, client, data): + """Build SensiboClimate. + + client: aiohttp session. + data: initially-fetched data. + """ + self._client = client + self._id = data['id'] + self._do_update(data) + + def _do_update(self, data): + self._name = data['room']['name'] + self._measurements = data['measurements'] + self._ac_states = data['acState'] + self._status = data['connectionStatus']['isAlive'] + capabilities = data['remoteCapabilities'] + self._operations = sorted(capabilities['modes'].keys()) + self._current_capabilities = capabilities[ + 'modes'][self.current_operation] + temperature_unit_key = self._ac_states['temperatureUnit'] + self._temperature_unit = \ + TEMP_CELSIUS if temperature_unit_key == 'C' else TEMP_FAHRENHEIT + self._temperatures_list = self._current_capabilities[ + 'temperatures'][temperature_unit_key]['values'] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_CURRENT_HUMIDITY: self.current_humidity} + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return self._temperature_unit + + @property + def available(self): + """Return True if entity is available.""" + return self._status + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._ac_states['targetTemperature'] + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + if self.temperature_unit == self.unit_of_measurement: + # We are working in same units as the a/c unit. Use whole degrees + # like the API supports. + return 1 + else: + # Unit conversion is going on. No point to stick to specific steps. + return None + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._ac_states['mode'] + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._measurements['humidity'] + + @property + def current_temperature(self): + """Return the current temperature.""" + # This field is not affected by temperature_unit. + # It is always in C / nativeTemperatureUnit + if 'nativeTemperatureUnit' not in self._ac_states: + return self._measurements['temperature'] + return convert_temperature( + self._measurements['temperature'], + TEMP_CELSIUS, + self.temperature_unit) + + @property + def operation_list(self): + """List of available operation modes.""" + return self._operations + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._ac_states['fanLevel'] + + @property + def fan_list(self): + """List of available fan modes.""" + return self._current_capabilities['fanLevels'] + + @property + def current_swing_mode(self): + """Return the fan setting.""" + return self._ac_states['swing'] + + @property + def swing_list(self): + """List of available swing modes.""" + return self._current_capabilities['swing'] + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_aux_heat_on(self): + """Return true if AC is on.""" + return self._ac_states['on'] + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._temperatures_list[0] + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._temperatures_list[-1] + + @asyncio.coroutine + def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + temperature = int(temperature) + if temperature not in self._temperatures_list: + # Requested temperature is not supported. + if temperature == self.target_temperature: + return + index = self._temperatures_list.index(self.target_temperature) + if temperature > self.target_temperature and index < len( + self._temperatures_list) - 1: + temperature = self._temperatures_list[index + 1] + elif temperature < self.target_temperature and index > 0: + temperature = self._temperatures_list[index - 1] + else: + return + + with async_timeout.timeout(TIMEOUT): + yield from self._client.async_set_ac_state_property( + self._id, 'targetTemperature', temperature) + + @asyncio.coroutine + def async_set_fan_mode(self, fan): + """Set new target fan mode.""" + with async_timeout.timeout(TIMEOUT): + yield from self._client.async_set_ac_state_property( + self._id, 'fanLevel', fan) + + @asyncio.coroutine + def async_set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + with async_timeout.timeout(TIMEOUT): + yield from self._client.async_set_ac_state_property( + self._id, 'mode', operation_mode) + + @asyncio.coroutine + def async_set_swing_mode(self, swing_mode): + """Set new target swing operation.""" + with async_timeout.timeout(TIMEOUT): + yield from self._client.async_set_ac_state_property( + self._id, 'swing', swing_mode) + + @asyncio.coroutine + def async_turn_aux_heat_on(self): + """Turn Sensibo unit on.""" + with async_timeout.timeout(TIMEOUT): + yield from self._client.async_set_ac_state_property( + self._id, 'on', True) + + @asyncio.coroutine + def async_turn_aux_heat_off(self): + """Turn Sensibo unit on.""" + with async_timeout.timeout(TIMEOUT): + yield from self._client.async_set_ac_state_property( + self._id, 'on', False) + + @asyncio.coroutine + def async_update(self): + """Retrieve latest state.""" + try: + with async_timeout.timeout(TIMEOUT): + data = yield from self._client.async_get_device( + self._id, _FETCH_FIELDS) + self._do_update(data) + except aiohttp.client_exceptions.ClientError: + _LOGGER.warning('Failed to connect to Sensibo servers.') diff --git a/requirements_all.txt b/requirements_all.txt index 2af9ff81f51..fd35592fb63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,6 +610,9 @@ pyowm==2.6.1 # homeassistant.components.qwikswitch pyqwikswitch==0.4 +# homeassistant.components.climate.sensibo +pysensibo==1.0.1 + # homeassistant.components.switch.acer_projector pyserial==3.1.1