diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 001bf8806acf7f..fa22b7aebe4b8c 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -7,44 +7,67 @@ import logging import socket -from homeassistant.components.climate import ClimateDevice +import voluptuous as vol + +from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.2.1'] _LOGGER = logging.getLogger(__name__) -CONF_AWAY_TEMP = "away_temperature" -DEFAULT_AWAY_TEMP = 16 +ATTR_FAN = 'fan' +ATTR_FANMODE = 'fanmode' +ATTR_SYSTEM_MODE = 'system_mode' + +CONF_AWAY_TEMPERATURE = 'away_temperature' +CONF_REGION = 'region' + +DEFAULT_AWAY_TEMPERATURE = 16 +DEFAULT_REGION = 'eu' +REGIONS = ['eu', 'us'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_AWAY_TEMPERATURE, default=DEFAULT_AWAY_TEMPERATURE): + vol.Coerce(float), + vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the HoneywelL thermostat.""" + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + region = config.get(CONF_REGION) + + if region == 'us': + return _setup_us(username, password, config, add_devices) + else: + return _setup_round(username, password, config, add_devices) def _setup_round(username, password, config, add_devices): """Setup rounding function.""" from evohomeclient import EvohomeClient - try: - 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) - return False - + away_temp = config.get(CONF_AWAY_TEMPERATURE) evo_api = EvohomeClient(username, password) try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, - zone['id'], - i == 0, - away_temp)]) + add_devices( + [RoundThermostat(evo_api, zone['id'], i == 0, away_temp)] + ) except socket.error: _LOGGER.error( - "Connection error logging into the honeywell evohome web service" - ) + "Connection error logging into the honeywell evohome web service") return False return True @@ -74,26 +97,6 @@ def _setup_us(username, password, config, add_devices): return True -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the honeywel thermostat.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - region = config.get('region', 'eu').lower() - - if username is None or password is None: - _LOGGER.error("Missing required configuration items %s or %s", - CONF_USERNAME, CONF_PASSWORD) - return False - if region not in ('us', 'eu'): - _LOGGER.error('Region `%s` is invalid (use either us or eu)', region) - return False - - if region == 'us': - return _setup_us(username, password, config, add_devices) - else: - return _setup_round(username, password, config, add_devices) - - class RoundThermostat(ClimateDevice): """Representation of a Honeywell Round Connected thermostat.""" @@ -103,7 +106,7 @@ def __init__(self, device, zone_id, master, away_temp): self.device = device self._current_temperature = None self._target_temperature = None - self._name = "round connected" + self._name = 'round connected' self._id = zone_id self._master = master self._is_dhw = False @@ -143,7 +146,7 @@ def set_temperature(self, **kwargs): @property def current_operation(self: ClimateDevice) -> str: """Get the current operation of the system.""" - return getattr(self.device, 'system_mode', None) + return getattr(self.device, ATTR_SYSTEM_MODE, None) @property def is_away_mode_on(self): @@ -152,7 +155,7 @@ def is_away_mode_on(self): def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: """Set the HVAC mode for the thermostat.""" - if hasattr(self.device, 'system_mode'): + if hasattr(self.device, ATTR_SYSTEM_MODE): self.device.system_mode = operation_mode def turn_away_mode_on(self): @@ -186,8 +189,8 @@ def update(self): self._current_temperature = data['temp'] self._target_temperature = data['setpoint'] - if data['thermostat'] == "DOMESTIC_HOT_WATER": - self._name = "Hot Water" + if data['thermostat'] == 'DOMESTIC_HOT_WATER': + self._name = 'Hot Water' self._is_dhw = True else: self._name = data['name'] @@ -236,7 +239,7 @@ def target_temperature(self): @property def current_operation(self: ClimateDevice) -> str: """Return current operation ie. heat, cool, idle.""" - return getattr(self._device, 'system_mode', None) + return getattr(self._device, ATTR_SYSTEM_MODE, None) def set_temperature(self, **kwargs): """Set target temperature.""" @@ -255,9 +258,11 @@ def set_temperature(self, **kwargs): @property def device_state_attributes(self): """Return the device specific state attributes.""" - return {'fan': (self.is_fan_on and 'running' or 'idle'), - 'fanmode': self._device.fan_mode, - 'system_mode': self._device.system_mode} + return { + ATTR_FAN: (self.is_fan_on and 'running' or 'idle'), + ATTR_FANMODE: self._device.fan_mode, + ATTR_SYSTEM_MODE: self._device.system_mode, + } def turn_away_mode_on(self): """Turn away on.""" @@ -269,5 +274,5 @@ def turn_away_mode_off(self): def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: """Set the system mode (Cool, Heat, etc).""" - if hasattr(self._device, 'system_mode'): + if hasattr(self._device, ATTR_SYSTEM_MODE): self._device.system_mode = operation_mode diff --git a/tests/components/climate/test_honeywell.py b/tests/components/climate/test_honeywell.py index 75a4d1081f3c6c..470e280faa7168 100644 --- a/tests/components/climate/test_honeywell.py +++ b/tests/components/climate/test_honeywell.py @@ -3,10 +3,11 @@ import unittest from unittest import mock +import voluptuous as vol import somecomfort -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, - TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.components.climate.honeywell as honeywell @@ -21,17 +22,30 @@ def test_setup_us(self, mock_ht, mock_sc): config = { CONF_USERNAME: 'user', CONF_PASSWORD: 'pass', - 'region': 'us', + honeywell.CONF_REGION: 'us', } bad_pass_config = { CONF_USERNAME: 'user', - 'region': 'us', + honeywell.CONF_REGION: 'us', } bad_region_config = { CONF_USERNAME: 'user', CONF_PASSWORD: 'pass', - 'region': 'un', + honeywell.CONF_REGION: 'un', } + + with self.assertRaises(vol.Invalid): + honeywell.PLATFORM_SCHEMA(None) + + with self.assertRaises(vol.Invalid): + honeywell.PLATFORM_SCHEMA({}) + + with self.assertRaises(vol.Invalid): + honeywell.PLATFORM_SCHEMA(bad_pass_config) + + with self.assertRaises(vol.Invalid): + honeywell.PLATFORM_SCHEMA(bad_region_config) + hass = mock.MagicMock() add_devices = mock.MagicMock() @@ -46,10 +60,6 @@ def test_setup_us(self, mock_ht, mock_sc): 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, bad_pass_config, add_devices) - self.assertFalse(result) - result = honeywell.setup_platform(hass, bad_region_config, add_devices) - self.assertFalse(result) result = honeywell.setup_platform(hass, config, add_devices) self.assertTrue(result) mock_sc.assert_called_once_with('user', 'pass') @@ -67,7 +77,7 @@ def test_setup_us_failures(self, mock_sc): config = { CONF_USERNAME: 'user', CONF_PASSWORD: 'pass', - 'region': 'us', + honeywell.CONF_REGION: 'us', } mock_sc.side_effect = somecomfort.AuthError @@ -88,7 +98,7 @@ def _test_us_filtered_devices(self, mock_ht, mock_sc, loc=None, dev=None): config = { CONF_USERNAME: 'user', CONF_PASSWORD: 'pass', - 'region': 'us', + honeywell.CONF_REGION: 'us', 'location': loc, 'thermostat': dev, } @@ -152,12 +162,12 @@ def test_us_filtered_location_2(self): @mock.patch('homeassistant.components.climate.honeywell.' 'RoundThermostat') def test_eu_setup_full_config(self, mock_round, mock_evo): - """Test the EU setup wwith complete configuration.""" + """Test the EU setup with complete configuration.""" config = { CONF_USERNAME: 'user', CONF_PASSWORD: 'pass', - honeywell.CONF_AWAY_TEMP: 20, - 'region': 'eu', + honeywell.CONF_AWAY_TEMPERATURE: 20.0, + honeywell.CONF_REGION: 'eu', } mock_evo.return_value.temperatures.return_value = [ {'id': 'foo'}, {'id': 'bar'}] @@ -168,8 +178,8 @@ def test_eu_setup_full_config(self, mock_round, mock_evo): 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), + mock.call(mock_evo.return_value, 'foo', True, 20.0), + mock.call(mock_evo.return_value, 'bar', False, 20.0), ]) self.assertEqual(2, add_devices.call_count) @@ -181,17 +191,20 @@ def test_eu_setup_partial_config(self, mock_round, mock_evo): config = { CONF_USERNAME: 'user', CONF_PASSWORD: 'pass', - 'region': 'eu', + honeywell.CONF_REGION: 'eu', } + mock_evo.return_value.temperatures.return_value = [ {'id': 'foo'}, {'id': 'bar'}] + config[honeywell.CONF_AWAY_TEMPERATURE] = \ + honeywell.DEFAULT_AWAY_TEMPERATURE + 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.call(mock_evo.return_value, 'foo', True, 16), + mock.call(mock_evo.return_value, 'bar', False, 16), ]) @mock.patch('evohomeclient.EvohomeClient') @@ -202,10 +215,12 @@ 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', + honeywell.CONF_AWAY_TEMPERATURE: 'ponies', + honeywell.CONF_REGION: 'eu', } - self.assertFalse(honeywell.setup_platform(None, config, None)) + + with self.assertRaises(vol.Invalid): + honeywell.PLATFORM_SCHEMA(config) @mock.patch('evohomeclient.EvohomeClient') @mock.patch('homeassistant.components.climate.honeywell.' @@ -215,8 +230,8 @@ def test_eu_setup_error(self, mock_round, mock_evo): config = { CONF_USERNAME: 'user', CONF_PASSWORD: 'pass', - honeywell.CONF_AWAY_TEMP: 20, - 'region': 'eu', + honeywell.CONF_AWAY_TEMPERATURE: 20, + honeywell.CONF_REGION: 'eu', } mock_evo.return_value.temperatures.side_effect = socket.error add_devices = mock.MagicMock() @@ -356,9 +371,9 @@ def test_set_temp_fail(self): def test_attributes(self): """Test the attributes.""" expected = { - 'fan': 'running', - 'fanmode': 'auto', - 'system_mode': 'heat', + honeywell.ATTR_FAN: 'running', + honeywell.ATTR_FANMODE: 'auto', + honeywell.ATTR_SYSTEM_MODE: 'heat', } self.assertEqual(expected, self.honeywell.device_state_attributes) expected['fan'] = 'idle' @@ -370,8 +385,8 @@ def test_with_no_fan(self): self.device.fan_running = False self.device.fan_mode = None expected = { - 'fan': 'idle', - 'fanmode': None, - 'system_mode': 'heat', + honeywell.ATTR_FAN: 'idle', + honeywell.ATTR_FANMODE: None, + honeywell.ATTR_SYSTEM_MODE: 'heat', } self.assertEqual(expected, self.honeywell.device_state_attributes)