Merge pull request #1241 from kk7ds/update-honeywell-somecomfort

Update honeywell somecomfort
pull/1247/head
Paulus Schoutsen 2016-02-13 19:44:58 -08:00
commit 9204c44eec
4 changed files with 358 additions and 148 deletions

View File

@ -149,7 +149,6 @@ omit =
homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/heatmiser.py
homeassistant/components/thermostat/homematic.py
homeassistant/components/thermostat/honeywell.py
homeassistant/components/thermostat/proliphix.py
homeassistant/components/thermostat/radiotherm.py

View File

@ -9,28 +9,24 @@ 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,
TEMP_FAHRENHEIT)
REQUIREMENTS = ['evohomeclient==0.2.4']
REQUIREMENTS = ['evohomeclient==0.2.4',
'somecomfort==0.2.0']
_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'
DEFAULT_AWAY_TEMP = 16
def _setup_round(username, password, config, add_devices):
from evohomeclient import EvohomeClient
try:
away_temp = float(config.get(CONF_AWAY_TEMP, 16))
away_temp = float(config.get(CONF_AWAY_TEMP, DEFAULT_AWAY_TEMP))
except ValueError:
_LOGGER.error("value entered for item %s should convert to a number",
CONF_AWAY_TEMP)
@ -50,25 +46,32 @@ def _setup_round(username, password, config, add_devices):
"Connection error logging into the honeywell evohome web service"
)
return False
return True
# config will be used later
# pylint: disable=unused-argument
def _setup_us(username, password, config, add_devices):
session = requests.Session()
if not HoneywellUSThermostat.do_login(session, username, password):
import somecomfort
try:
client = somecomfort.SomeComfort(username, password)
except somecomfort.AuthError:
_LOGGER.error('Failed to login to honeywell account %s', username)
return False
thermostats = HoneywellUSThermostat.get_devices(session)
if not thermostats:
_LOGGER.error('No thermostats found in account %s', username)
except somecomfort.SomeComfortError as ex:
_LOGGER.error('Failed to initialize honeywell client: %s', str(ex))
return False
add_devices([HoneywellUSThermostat(id_, username, password,
name=name,
session=session)
for id_, name in thermostats.items()])
dev_id = config.get('thermostat')
loc_id = config.get('location')
add_devices([HoneywellUSThermostat(client, device)
for location in client.locations_by_id.values()
for device in location.devices_by_id.values()
if ((not loc_id or location.locationid == loc_id) and
(not dev_id or device.deviceid == dev_id))])
return True
# pylint: disable=unused-argument
@ -179,157 +182,52 @@ class RoundThermostat(ThermostatDevice):
class HoneywellUSThermostat(ThermostatDevice):
""" Represents a Honeywell US Thermostat. """
# pylint: disable=too-many-arguments
def __init__(self, ident, username, password, name='honeywell',
session=None):
self._ident = ident
self._username = username
self._password = password
self._name = name
if not session:
self._session = requests.Session()
self._login()
self._session = session
# Maybe this should be configurable?
self._timeout = 30
# Yeah, really.
self._session.headers['X-Requested-With'] = 'XMLHttpRequest'
self._update()
@staticmethod
def get_devices(session):
""" Return a dict of devices.
:param session: A session already primed from do_login
:returns: A dict of devices like: device_id=name
"""
url = '%s/Location/GetLocationListData' % US_BASEURL
resp = session.post(url, params={'page': 1, 'filter': ''})
if resp.status_code == 200:
return {device['DeviceID']: device['Name']
for device in resp.json()[0]['Devices']}
else:
return None
@staticmethod
def do_login(session, username, password, timeout=30):
""" Log into mytotalcomfort.com
:param session: A requests.Session object to use
:param username: Account username
:param password: Account password
:param timeout: Timeout to use with requests
:returns: A boolean indicating success
"""
session.headers['X-Requested-With'] = 'XMLHttpRequest'
session.get(US_BASEURL, timeout=timeout)
params = {'UserName': username,
'Password': password,
'RememberMe': 'false',
'timeOffset': 480}
resp = session.post(US_BASEURL, params=params,
timeout=timeout)
if resp.status_code != 200:
_LOGGER('Login failed for user %s', username)
return False
else:
return True
def _login(self):
return self.do_login(self._session, self._username, self._password,
timeout=self._timeout)
def _keepalive(self):
resp = self._session.get('%s/Account/KeepAlive')
if resp.status_code != 200:
if self._login():
_LOGGER.info('Re-logged into honeywell account')
else:
_LOGGER.error('Failed to re-login to honeywell account')
return False
else:
_LOGGER.debug('Keepalive succeeded')
return True
def _get_data(self):
if not self._keepalive:
return {'error': 'not logged in'}
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._keepalive:
return {'error': 'not logged in'}
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):
data = self._get_data()['latestData']
if 'error' not in data:
self._data = data
def __init__(self, client, device):
self._client = client
self._device = device
@property
def is_fan_on(self):
return self._data['fanData']['fanIsRunning']
return self._device.fan_running
@property
def name(self):
return self._name
return self._device.name
@property
def unit_of_measurement(self):
unit = self._data['uiData']['DisplayUnits']
if unit == 'F':
return TEMP_FAHRENHEIT
else:
return TEMP_CELCIUS
return (TEMP_CELCIUS if self._device.temperature_unit == 'C'
else TEMP_FAHRENHEIT)
@property
def current_temperature(self):
self._update()
return self._data['uiData']['DispTemperature']
self._device.refresh()
return self._device.current_temperature
@property
def target_temperature(self):
setpoint = US_SYSTEM_SWITCH_POSITIONS.get(
self._data['uiData']['SystemSwitchPosition'],
'Off')
return self._data['uiData']['%sSetpoint' % setpoint]
if self._device.system_mode == 'cool':
return self._device.setpoint_cool
else:
return self._device.setpoint_heat
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)
import somecomfort
try:
if self._device.system_mode == 'cool':
self._device.setpoint_cool = temperature
else:
self._device.setpoint_heat = temperature
except somecomfort.SomeComfortError:
_LOGGER.error('Temperature %.1f out of range', temperature)
@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']]}
return {'fan': (self.is_fan_on and 'running' or 'idle'),
'fanmode': self._device.fan_mode,
'system_mode': self._device.system_mode}
def turn_away_mode_on(self):
pass

View File

@ -230,6 +230,9 @@ sleekxmpp==1.3.1
# homeassistant.components.media_player.snapcast
snapcast==1.1.1
# homeassistant.components.thermostat.honeywell
somecomfort==0.2.0
# homeassistant.components.sensor.speedtest
speedtest-cli==0.3.4

View File

@ -0,0 +1,310 @@
"""
tests.components.thermostat.honeywell
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the Honeywell thermostat module.
"""
import socket
import unittest
from unittest import mock
import somecomfort
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
TEMP_CELCIUS, TEMP_FAHRENHEIT)
import homeassistant.components.thermostat.honeywell as honeywell
class TestHoneywell(unittest.TestCase):
@mock.patch('somecomfort.SomeComfort')
@mock.patch('homeassistant.components.thermostat.'
'honeywell.HoneywellUSThermostat')
def test_setup_us(self, mock_ht, mock_sc):
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'us',
}
hass = mock.MagicMock()
add_devices = mock.MagicMock()
locations = [
mock.MagicMock(),
mock.MagicMock(),
]
devices_1 = [mock.MagicMock()]
devices_2 = [mock.MagicMock(), mock.MagicMock]
mock_sc.return_value.locations_by_id.values.return_value = \
locations
locations[0].devices_by_id.values.return_value = devices_1
locations[1].devices_by_id.values.return_value = devices_2
result = honeywell.setup_platform(hass, config, add_devices)
self.assertTrue(result)
mock_sc.assert_called_once_with('user', 'pass')
mock_ht.assert_has_calls([
mock.call(mock_sc.return_value, devices_1[0]),
mock.call(mock_sc.return_value, devices_2[0]),
mock.call(mock_sc.return_value, devices_2[1]),
])
@mock.patch('somecomfort.SomeComfort')
def test_setup_us_failures(self, mock_sc):
hass = mock.MagicMock()
add_devices = mock.MagicMock()
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'us',
}
mock_sc.side_effect = somecomfort.AuthError
result = honeywell.setup_platform(hass, config, add_devices)
self.assertFalse(result)
self.assertFalse(add_devices.called)
mock_sc.side_effect = somecomfort.SomeComfortError
result = honeywell.setup_platform(hass, config, add_devices)
self.assertFalse(result)
self.assertFalse(add_devices.called)
@mock.patch('somecomfort.SomeComfort')
@mock.patch('homeassistant.components.thermostat.'
'honeywell.HoneywellUSThermostat')
def _test_us_filtered_devices(self, mock_ht, mock_sc, loc=None, dev=None):
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'us',
'location': loc,
'thermostat': dev,
}
locations = {
1: mock.MagicMock(locationid=mock.sentinel.loc1,
devices_by_id={
11: mock.MagicMock(
deviceid=mock.sentinel.loc1dev1),
12: mock.MagicMock(
deviceid=mock.sentinel.loc1dev2),
}),
2: mock.MagicMock(locationid=mock.sentinel.loc2,
devices_by_id={
21: mock.MagicMock(
deviceid=mock.sentinel.loc2dev1),
}),
3: mock.MagicMock(locationid=mock.sentinel.loc3,
devices_by_id={
31: mock.MagicMock(
deviceid=mock.sentinel.loc3dev1),
}),
}
mock_sc.return_value = mock.MagicMock(locations_by_id=locations)
hass = mock.MagicMock()
add_devices = mock.MagicMock()
self.assertEqual(True,
honeywell.setup_platform(hass, config, add_devices))
return mock_ht.call_args_list, mock_sc
def test_us_filtered_thermostat_1(self):
result, client = self._test_us_filtered_devices(
dev=mock.sentinel.loc1dev1)
devices = [x[0][1].deviceid for x in result]
self.assertEqual([mock.sentinel.loc1dev1], devices)
def test_us_filtered_thermostat_2(self):
result, client = self._test_us_filtered_devices(
dev=mock.sentinel.loc2dev1)
devices = [x[0][1].deviceid for x in result]
self.assertEqual([mock.sentinel.loc2dev1], devices)
def test_us_filtered_location_1(self):
result, client = self._test_us_filtered_devices(
loc=mock.sentinel.loc1)
devices = [x[0][1].deviceid for x in result]
self.assertEqual([mock.sentinel.loc1dev1,
mock.sentinel.loc1dev2], devices)
def test_us_filtered_location_2(self):
result, client = self._test_us_filtered_devices(
loc=mock.sentinel.loc2)
devices = [x[0][1].deviceid for x in result]
self.assertEqual([mock.sentinel.loc2dev1], devices)
@mock.patch('evohomeclient.EvohomeClient')
@mock.patch('homeassistant.components.thermostat.honeywell.'
'RoundThermostat')
def test_eu_setup_full_config(self, mock_round, mock_evo):
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
honeywell.CONF_AWAY_TEMP: 20,
'region': 'eu',
}
mock_evo.return_value.temperatures.return_value = [
{'id': 'foo'}, {'id': 'bar'}]
hass = mock.MagicMock()
add_devices = mock.MagicMock()
self.assertTrue(honeywell.setup_platform(hass, config, add_devices))
mock_evo.assert_called_once_with('user', 'pass')
mock_evo.return_value.temperatures.assert_called_once_with(
force_refresh=True)
mock_round.assert_has_calls([
mock.call(mock_evo.return_value, 'foo', True, 20),
mock.call(mock_evo.return_value, 'bar', False, 20),
])
self.assertEqual(2, add_devices.call_count)
@mock.patch('evohomeclient.EvohomeClient')
@mock.patch('homeassistant.components.thermostat.honeywell.'
'RoundThermostat')
def test_eu_setup_partial_config(self, mock_round, mock_evo):
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'eu',
}
mock_evo.return_value.temperatures.return_value = [
{'id': 'foo'}, {'id': 'bar'}]
hass = mock.MagicMock()
add_devices = mock.MagicMock()
self.assertTrue(honeywell.setup_platform(hass, config, add_devices))
default = honeywell.DEFAULT_AWAY_TEMP
mock_round.assert_has_calls([
mock.call(mock_evo.return_value, 'foo', True, default),
mock.call(mock_evo.return_value, 'bar', False, default),
])
@mock.patch('evohomeclient.EvohomeClient')
@mock.patch('homeassistant.components.thermostat.honeywell.'
'RoundThermostat')
def test_eu_setup_bad_temp(self, mock_round, mock_evo):
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
honeywell.CONF_AWAY_TEMP: 'ponies',
'region': 'eu',
}
self.assertFalse(honeywell.setup_platform(None, config, None))
@mock.patch('evohomeclient.EvohomeClient')
@mock.patch('homeassistant.components.thermostat.honeywell.'
'RoundThermostat')
def test_eu_setup_error(self, mock_round, mock_evo):
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
honeywell.CONF_AWAY_TEMP: 20,
'region': 'eu',
}
mock_evo.return_value.temperatures.side_effect = socket.error
add_devices = mock.MagicMock()
hass = mock.MagicMock()
self.assertFalse(honeywell.setup_platform(hass, config, add_devices))
class TestHoneywellRound(unittest.TestCase):
def setup_method(self, method):
def fake_temperatures(force_refresh=None):
temps = [
{'id': '1', 'temp': 20, 'setpoint': 21,
'thermostat': 'main', 'name': 'House'},
{'id': '2', 'temp': 21, 'setpoint': 22,
'thermostat': 'DOMESTIC_HOT_WATER'},
]
return temps
self.device = mock.MagicMock()
self.device.temperatures.side_effect = fake_temperatures
self.round1 = honeywell.RoundThermostat(self.device, '1',
True, 16)
self.round2 = honeywell.RoundThermostat(self.device, '2',
False, 17)
def test_attributes(self):
self.assertEqual('House', self.round1.name)
self.assertEqual(TEMP_CELCIUS, self.round1.unit_of_measurement)
self.assertEqual(20, self.round1.current_temperature)
self.assertEqual(21, self.round1.target_temperature)
self.assertFalse(self.round1.is_away_mode_on)
self.assertEqual('Hot Water', self.round2.name)
self.assertEqual(TEMP_CELCIUS, self.round2.unit_of_measurement)
self.assertEqual(21, self.round2.current_temperature)
self.assertEqual(None, self.round2.target_temperature)
self.assertFalse(self.round2.is_away_mode_on)
def test_away_mode(self):
self.assertFalse(self.round1.is_away_mode_on)
self.round1.turn_away_mode_on()
self.assertTrue(self.round1.is_away_mode_on)
self.device.set_temperature.assert_called_once_with('House', 16)
self.device.set_temperature.reset_mock()
self.round1.turn_away_mode_off()
self.assertFalse(self.round1.is_away_mode_on)
self.device.cancel_temp_override.assert_called_once_with('House')
def test_set_temperature(self):
self.round1.set_temperature(25)
self.device.set_temperature.assert_called_once_with('House', 25)
class TestHoneywellUS(unittest.TestCase):
def setup_method(self, method):
self.client = mock.MagicMock()
self.device = mock.MagicMock()
self.honeywell = honeywell.HoneywellUSThermostat(
self.client, self.device)
self.device.fan_running = True
self.device.name = 'test'
self.device.temperature_unit = 'F'
self.device.current_temperature = 72
self.device.setpoint_cool = 78
self.device.setpoint_heat = 65
self.device.system_mode = 'heat'
self.device.fan_mode = 'auto'
def test_properties(self):
self.assertTrue(self.honeywell.is_fan_on)
self.assertEqual('test', self.honeywell.name)
self.assertEqual(72, self.honeywell.current_temperature)
def test_unit_of_measurement(self):
self.assertEqual(TEMP_FAHRENHEIT, self.honeywell.unit_of_measurement)
self.device.temperature_unit = 'C'
self.assertEqual(TEMP_CELCIUS, self.honeywell.unit_of_measurement)
def test_target_temp(self):
self.assertEqual(65, self.honeywell.target_temperature)
self.device.system_mode = 'cool'
self.assertEqual(78, self.honeywell.target_temperature)
def test_set_temp(self):
self.honeywell.set_temperature(70)
self.assertEqual(70, self.device.setpoint_heat)
self.assertEqual(70, self.honeywell.target_temperature)
self.device.system_mode = 'cool'
self.assertEqual(78, self.honeywell.target_temperature)
self.honeywell.set_temperature(74)
self.assertEqual(74, self.device.setpoint_cool)
self.assertEqual(74, self.honeywell.target_temperature)
def test_set_temp_fail(self):
self.device.setpoint_heat = mock.MagicMock(
side_effect=somecomfort.SomeComfortError)
self.honeywell.set_temperature(123)
def test_attributes(self):
expected = {
'fan': 'running',
'fanmode': 'auto',
'system_mode': 'heat',
}
self.assertEqual(expected, self.honeywell.device_state_attributes)
expected['fan'] = 'idle'
self.device.fan_running = False
self.assertEqual(expected, self.honeywell.device_state_attributes)