From 062fe79b3f05e00dc2bb0ccbc20d6e7ef1c77b54 Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Sat, 6 Feb 2016 04:09:46 +0000 Subject: [PATCH] Add Honeywell US thermostat support This adds support for the US variant of the Honeywell connected thermostat. The interface is super simple, so this doesn't add any external dependencies. It supports basic temperature, setpoint, and control. Issue #998 notes that the existing honeywell module doesn't work for US models, which is because they are totally different. In order to indicate to the honeywell platform module that the thermostat is a US-type, we key off of whether or not the thermostat id is provided. This is something that US people have (and require to identify one of potentially multiple thermostats in their account) and EU people will not. --- .../components/thermostat/honeywell.py | 151 ++++++++++++++++-- 1 file changed, 141 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 5475e1ce306..84c8813405b 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -9,33 +9,32 @@ https://home-assistant.io/components/thermostat.honeywell/ import logging import socket +import requests + from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS, + TEMP_FAHRENHEIT) REQUIREMENTS = ['evohomeclient==0.2.4'] _LOGGER = logging.getLogger(__name__) CONF_AWAY_TEMP = "away_temperature" +US_SYSTEM_SWITCH_POSITIONS = {1: 'Heat', + 2: 'Off', + 3: 'Cool'} +US_BASEURL = 'https://mytotalconnectcomfort.com/portal' -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the honeywel thermostat. """ +def _setup_round(username, password, config, add_devices): from evohomeclient import EvohomeClient - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) try: away_temp = float(config.get(CONF_AWAY_TEMP, 16)) except ValueError: _LOGGER.error("value entered for item %s should convert to a number", CONF_AWAY_TEMP) return False - if username is None or password is None: - _LOGGER.error("Missing required configuration items %s or %s", - CONF_USERNAME, CONF_PASSWORD) - return False evo_api = EvohomeClient(username, password) @@ -53,6 +52,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the honeywel thermostat. """ + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + thermostat_id = config.get('id') + + if username is None or password is None: + _LOGGER.error("Missing required configuration items %s or %s", + CONF_USERNAME, CONF_PASSWORD) + return False + + if thermostat_id: + add_devices([HoneywellUSThermostat(thermostat_id, username, password)]) + else: + return _setup_round(username, password, config, add_devices) + + class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ @@ -135,3 +152,117 @@ class RoundThermostat(ThermostatDevice): else: self._name = data['name'] self._is_dhw = False + + +class HoneywellUSThermostat(ThermostatDevice): + """ Represents a Honeywell US Thermostat. """ + + def __init__(self, ident, username, password): + self._ident = ident + self._username = username + self._password = password + self._session = requests.Session() + # Maybe this should be configurable? + self._timeout = 30 + # Yeah, really. + self._session.headers['X-Requested-With'] = 'XMLHttpRequest' + self._update() + + def _login(self): + self._session.get(US_BASEURL, timeout=self._timeout) + params = {'UserName': self._username, + 'Password': self._password, + 'RememberMe': 'false', + 'timeOffset': 480} + resp = self._session.post(US_BASEURL, params=params, + timeout=self._timeout) + if resp.status_code != 200: + _LOGGER('Login failed for user %(user)s', + dict(user=self._username)) + return False + else: + return True + + def _get_data(self): + if not self._login(): + return + url = '%s/Device/CheckDataSession/%s' % (US_BASEURL, self._ident) + resp = self._session.get(url, timeout=self._timeout) + if resp.status_code < 300: + return resp.json() + else: + return {'error': resp.status_code} + + def _set_data(self, data): + if not self._login(): + return + url = '%s/Device/SubmitControlScreenChanges' % US_BASEURL + data['DeviceID'] = self._ident + resp = self._session.post(url, data=data, timeout=self._timeout) + if resp.status_code < 300: + return resp.json() + else: + return {'error': resp.status_code} + + def _update(self): + self._data = self._get_data()['latestData'] + + @property + def is_fan_on(self): + return self._data['fanData']['fanIsRunning'] + + @property + def name(self): + return 'honeywell' + + @property + def unit_of_measurement(self): + unit = self._data['uiData']['DisplayUnits'] + if unit == 'F': + return TEMP_FAHRENHEIT + else: + return TEMP_CELCIUS + + @property + def current_temperature(self): + self._update() + return self._data['uiData']['DispTemperature'] + + @property + def target_temperature(self): + setpoint = US_SYSTEM_SWITCH_POSITIONS.get( + self._data['uiData']['SystemSwitchPosition'], + 'Off') + return self._data['uiData']['%sSetpoint' % setpoint] + + def set_temperature(self, temperature): + """ Set target temperature. """ + data = {'SystemSwitch': None, + 'HeatSetpoint': None, + 'CoolSetpoint': None, + 'HeatNextPeriod': None, + 'CoolNextPeriod': None, + 'StatusHeat': None, + 'StatusCool': None, + 'FanMode': None} + setpoint = US_SYSTEM_SWITCH_POSITIONS.get( + self._data['uiData']['SystemSwitchPosition'], + 'Off') + data['%sSetpoint' % setpoint] = temperature + self._set_data(data) + + @property + def device_state_attributes(self): + """ Return device specific state attributes. """ + fanmodes = {0: "auto", + 1: "on", + 2: "circulate"} + return {"fan": (self._data['fanData']['fanIsRunning'] and + 'running' or 'idle'), + "fanmode": fanmodes[self._data['fanData']['fanMode']]} + + def turn_away_mode_on(self): + pass + + def turn_away_mode_off(self): + pass