From c91d52a5875818a794f6710f394ab8bd80203bdf Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sat, 11 Nov 2017 00:22:37 -0600 Subject: [PATCH 01/27] first stab at the nuheat components --- homeassistant/components/climate/nuheat.py | 208 +++++++++++++++++++++ homeassistant/components/nuheat.py | 47 +++++ requirements_all.txt | 3 + tests/components/climate/test_nuheat.py | 187 ++++++++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 homeassistant/components/climate/nuheat.py create mode 100644 homeassistant/components/nuheat.py create mode 100644 tests/components/climate/test_nuheat.py diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py new file mode 100644 index 00000000000..60a253e9b7c --- /dev/null +++ b/homeassistant/components/climate/nuheat.py @@ -0,0 +1,208 @@ +""" +Support for NuHeat thermostats. + +For more details about this platform, please refer to the documentation at +""" +import logging +from datetime import timedelta + +from homeassistant.components.climate import ( + ClimateDevice, + STATE_HEAT, + STATE_IDLE) +from homeassistant.const import ( + ATTR_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT) +from homeassistant.util import Throttle + +DEPENDENCIES = ["nuheat"] + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + +MODE_AUTO = "auto" # Run device schedule +MODE_AWAY = "away" +MODE_HOLD_TEMPERATURE = "temperature" +MODE_TEMPORARY_HOLD = "temporary_temperature" +# TODO: offline? + +OPERATION_LIST = [STATE_HEAT, STATE_IDLE] + +SCHEDULE_HOLD = 3 +SCHEDULE_RUN = 1 +SCHEDULE_TEMPORARY_HOLD = 2 + + +def setup_platform(hass, config, add_devices, discovery_info=None): + if discovery_info is None: + return + + _LOGGER.info("Loading NuHeat thermostat component") + + temperature_unit = hass.config.units.temperature_unit + _LOGGER.debug("temp_unit is %s", temperature_unit) + api, serial_numbers = hass.data[DATA_NUHEAT] + + thermostats = [ + NuHeatThermostat(api, serial_number, temperature_unit) + for serial_number in serial_numbers + ] + add_devices(thermostats, True) + + +class NuHeatThermostat(ClimateDevice): + """Representation of a NuHeat Thermostat.""" + def __init__(self, api, serial_number, temperature_unit): + self._thermostat = api.get_thermostat(serial_number) + self._temperature_unit = temperature_unit + self._force_update = False + + @property + def name(self): + """Return the name of the thermostat.""" + return self._thermostat.room + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + if self._temperature_unit == "C": + return TEMP_CELSIUS + + return TEMP_FAHRENHEIT + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._temperature_unit == "C": + return self._thermostat.celsius + + return self._thermostat.fahrenheit + + @property + def current_operation(self): + """Return current operation. ie. heat, idle.""" + if self._thermostat.heating: + return STATE_HEAT + + return STATE_IDLE + + @property + def min_temp(self): + """Return the minimum supported temperature for the thermostat.""" + if self._temperature_unit == "C": + return self._thermostat.min_celsius + + return self._thermostat.min_fahrenheit + + @property + def max_temp(self): + """Return the maximum supported temperature for the thermostat.""" + if self._temperature_unit == "C": + return self._thermostat.max_celsius + + return self._thermostat.max_fahrenheit + + @property + def target_temperature(self): + """Return the currently programmed temperature.""" + if self._temperature_unit == "C": + return self._thermostat.target_celsius + + return self._thermostat.target_fahrenheit + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + return self.target_temperature + + @property + def target_temperature_high(self): + """Return the upper bound temperature we try to reach.""" + return self.target_temperature + + @property + def current_hold_mode(self): + """Return current hold mode.""" + if self.is_away_mode_on: + return MODE_AWAY + + schedule_mode = self._thermostat.schedule_mode + if schedule_mode == SCHEDULE_RUN: + return MODE_AUTO + + if schedule_mode == SCHEDULE_HOLD: + return MODE_HOLD_TEMPERATURE + + if schedule_mode == SCHEDULE_TEMPORARY_HOLD: + return MODE_TEMPORARY_HOLD + + return MODE_AUTO + + @property + def operation_list(self): + """Return list of possible operation modes.""" + return OPERATION_LIST + + @property + def is_away_mode_on(self): + """ + Return true if away mode is on. + + Away mode is determined by setting and HOLDing the target temperature + to the minimum temperature supported. + """ + if self._thermostat.target_celsius > self._thermostat.min_celsius: + return False + + if self._thermostat.schedule_mode != SCHEDULE_HOLD: + return False + + return True + + def turn_away_mode_on(self): + """Turn away mode on.""" + if self.is_away_mode_on: + return + + kwargs = {} + kwargs[ATTR_TEMPERATURE] = self.min_temp + + self.set_temperature(**kwargs) + self._force_update = True + + def turn_away_mode_off(self): + """Turn away mode off.""" + if not self.is_away_mode_on: + return + + self._thermostat.resume_schedule() + self._force_update = True + + def set_temperature(self, **kwargs): + """Set a new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if self._temperature_unit == "C": + self._thermostat.target_celsius = temperature + else: + self._thermostat.target_fahrenheit = temperature + + _LOGGER.info( + "Setting NuHeat thermostat temperature to %s %s", + temperature, self.temperature_unit) + + self._force_update = True + + def update(self): + """Get the latest state from the thermostat.""" + if self._force_update: + self._throttled_update(no_throttle=True) + self._force_update = False + else: + self._throttled_update() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _throttled_update(self): + """Get the latest state from the thermostat... but throttled!""" + self._thermostat.get_data() diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py new file mode 100644 index 00000000000..969afe1ee48 --- /dev/null +++ b/homeassistant/components/nuheat.py @@ -0,0 +1,47 @@ +""" +Support for NuHeat thermostats. + +For more details about this platform, please refer to the documentation at +""" +import logging + +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery + +REQUIREMENTS = ["nuheat==0.2.0"] + +_LOGGER = logging.getLogger(__name__) + +DATA_NUHEAT = "nuheat" + +DOMAIN = "nuheat" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, cv.string) + }), +}, extra=vol.ALLOW_EXTRA) + +def setup(hass, config): + """Set up the NuHeat thermostat component.""" + import nuheat + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + devices = conf.get(CONF_DEVICES) + + api = nuheat.NuHeat(username, password) + api.authenticate() + hass.data[DATA_NUHEAT] = (api, devices) + + discovery.load_platform(hass, "climate", DOMAIN, {}, config) + _LOGGER.debug("NuHeat initialized") + return True diff --git a/requirements_all.txt b/requirements_all.txt index dec8f96f39a..ef8c952f485 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -484,6 +484,9 @@ neurio==0.3.1 # homeassistant.components.sensor.nederlandse_spoorwegen nsapi==2.7.4 +# homeassistant.components.nuheat +nuheat==0.2.0 + # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv numpy==1.13.3 diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py new file mode 100644 index 00000000000..33a1c3e02f7 --- /dev/null +++ b/tests/components/climate/test_nuheat.py @@ -0,0 +1,187 @@ +"""The test for the NuHeat thermostat module.""" +import unittest +from unittest.mock import PropertyMock, Mock, patch + +from homeassistant.components.climate import STATE_HEAT, STATE_IDLE +import homeassistant.components.climate.nuheat as nuheat +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + +SCHEDULE_HOLD = 3 +SCHEDULE_RUN = 1 +SCHEDULE_TEMPORARY_HOLD = 2 + + +class TestNuHeat(unittest.TestCase): + """Tests for NuHeat climate.""" + + def setUp(self): + + serial_number = "12345" + temperature_unit = "F" + + thermostat = Mock( + serial_number=serial_number, + room="Master bathroom", + online=True, + heating=True, + temperature=2222, + celsius=22, + fahrenheit=72, + max_celsius=69, + max_fahrenheit=157, + min_celsius=5, + min_fahrenheit=41, + schedule_mode=SCHEDULE_RUN, + target_celsius=22, + target_fahrenheit=72) + + api = Mock() + api.get_thermostat.return_value = thermostat + + self.thermostat = nuheat.NuHeatThermostat( + api, serial_number, temperature_unit) + + def test_name(self): + """Test name property.""" + self.assertEqual(self.thermostat.name, "Master bathroom") + + def test_temperature_unit(self): + """Test temperature unit.""" + self.assertEqual(self.thermostat.temperature_unit, TEMP_FAHRENHEIT) + + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.temperature_unit, TEMP_CELSIUS) + + def test_current_temperature(self): + """Test current temperature.""" + self.assertEqual(self.thermostat.current_temperature, 72) + + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.current_temperature, 22) + + def test_current_operation(self): + """Test current operation.""" + self.assertEqual(self.thermostat.current_operation, STATE_HEAT) + + self.thermostat._thermostat.heating = False + self.assertEqual(self.thermostat.current_operation, STATE_IDLE) + + def test_min_temp(self): + """Test min temp.""" + self.assertEqual(self.thermostat.min_temp, 41) + + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.min_temp, 5) + + def test_max_temp(self): + """Test max temp.""" + self.assertEqual(self.thermostat.max_temp, 157) + + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.max_temp, 69) + + def test_target_temperature(self): + """Test target temperature.""" + self.assertEqual(self.thermostat.target_temperature, 72) + + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.target_temperature, 22) + + + def test_target_temperature_low(self): + """Test low target temperature.""" + self.assertEqual(self.thermostat.target_temperature_low, 72) + + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.target_temperature_low, 22) + + def test_target_temperature_high(self): + """Test high target temperature.""" + self.assertEqual(self.thermostat.target_temperature_high, 72) + + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.target_temperature_high, 22) + + @patch.object( + nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) + def test_current_hold_mode_away(self, is_away_mode_on): + """Test current hold mode while away.""" + is_away_mode_on.return_value = True + self.assertEqual(self.thermostat.current_hold_mode, nuheat.MODE_AWAY) + + @patch.object( + nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) + def test_current_hold_mode(self, is_away_mode_on): + """Test current hold mode.""" + is_away_mode_on.return_value = False + + self.thermostat._thermostat.schedule_mode = SCHEDULE_RUN + self.assertEqual(self.thermostat.current_hold_mode, nuheat.MODE_AUTO) + + self.thermostat._thermostat.schedule_mode = SCHEDULE_HOLD + self.assertEqual( + self.thermostat.current_hold_mode, nuheat.MODE_HOLD_TEMPERATURE) + + self.thermostat._thermostat.schedule_mode = SCHEDULE_TEMPORARY_HOLD + self.assertEqual( + self.thermostat.current_hold_mode, nuheat.MODE_TEMPORARY_HOLD) + + def test_is_away_mode_on(self): + """Test is away mode on.""" + _thermostat = self.thermostat._thermostat + _thermostat.target_celsius = _thermostat.min_celsius + _thermostat.schedule_mode = SCHEDULE_HOLD + self.assertTrue(self.thermostat.is_away_mode_on) + + _thermostat.target_celsius = _thermostat.min_celsius + 1 + self.assertFalse(self.thermostat.is_away_mode_on) + + _thermostat.target_celsius = _thermostat.min_celsius + _thermostat.schedule_mode = SCHEDULE_RUN + self.assertFalse(self.thermostat.is_away_mode_on) + + @patch.object( + nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) + @patch.object(nuheat.NuHeatThermostat, "set_temperature") + def test_turn_away_mode_on_while_home(self, set_temp, is_away_mode_on): + """Test turn away mode on when not away.""" + is_away_mode_on.return_value = False + self.thermostat.turn_away_mode_on() + set_temp.assert_called_once_with(temperature=self.thermostat.min_temp) + self.assertTrue(self.thermostat._force_update) + + @patch.object( + nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) + @patch.object(nuheat.NuHeatThermostat, "set_temperature") + def test_turn_away_mode_on_while_away(self, set_temp, is_away_mode_on): + """Test turn away mode on when away.""" + is_away_mode_on.return_value = True + self.thermostat.turn_away_mode_on() + set_temp.assert_not_called() + + def test_set_temperature(self): + """Test set temperature.""" + self.thermostat.set_temperature(temperature=85) + self.assertEqual(self.thermostat._thermostat.target_fahrenheit, 85) + self.assertTrue(self.thermostat._force_update) + + self.thermostat._temperature_unit = "C" + self.thermostat.set_temperature(temperature=23) + self.assertEqual(self.thermostat._thermostat.target_celsius, 23) + self.assertTrue(self.thermostat._force_update) + + @patch.object(nuheat.NuHeatThermostat, "_throttled_update") + def test_forced_update(self, throttled_update): + """Test update without throttle.""" + self.thermostat._force_update = True + self.thermostat.update() + throttled_update.assert_called_once_with(no_throttle=True) + self.assertFalse(self.thermostat._force_update) + + @patch.object(nuheat.NuHeatThermostat, "_throttled_update") + def test_throttled_update(self, throttled_update): + """Test update with throttle.""" + self.thermostat._force_update = False + self.thermostat.update() + throttled_update.assert_called_once_with() + self.assertFalse(self.thermostat._force_update) From 5fe2db228c7e8be9f9ede27b1a9743f4f1c282bc Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sat, 11 Nov 2017 10:18:32 -0600 Subject: [PATCH 02/27] bug fixes and linting --- .coveragerc | 3 ++ homeassistant/components/climate/nuheat.py | 14 ++++------ homeassistant/components/nuheat.py | 5 ++-- tests/components/climate/test_nuheat.py | 32 +++++++++++++++------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3bfd983dc30..4de7d138f71 100644 --- a/.coveragerc +++ b/.coveragerc @@ -143,6 +143,9 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py + homeassistant/components/nuheat.py + homeassistant/components/*/nuheat.py + homeassistant/components/octoprint.py homeassistant/components/*/octoprint.py diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index 60a253e9b7c..63369ed5769 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -10,6 +10,7 @@ from homeassistant.components.climate import ( ClimateDevice, STATE_HEAT, STATE_IDLE) +from homeassistant.components.nuheat import DATA_NUHEAT from homeassistant.const import ( ATTR_TEMPERATURE, TEMP_CELSIUS, @@ -26,7 +27,6 @@ MODE_AUTO = "auto" # Run device schedule MODE_AWAY = "away" MODE_HOLD_TEMPERATURE = "temperature" MODE_TEMPORARY_HOLD = "temporary_temperature" -# TODO: offline? OPERATION_LIST = [STATE_HEAT, STATE_IDLE] @@ -36,15 +36,13 @@ SCHEDULE_TEMPORARY_HOLD = 2 def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the NuHeat thermostat(s).""" if discovery_info is None: return - _LOGGER.info("Loading NuHeat thermostat component") - + _LOGGER.info("Loading NuHeat thermostat climate component") temperature_unit = hass.config.units.temperature_unit - _LOGGER.debug("temp_unit is %s", temperature_unit) api, serial_numbers = hass.data[DATA_NUHEAT] - thermostats = [ NuHeatThermostat(api, serial_number, temperature_unit) for serial_number in serial_numbers @@ -77,7 +75,7 @@ class NuHeatThermostat(ClimateDevice): """Return the current temperature.""" if self._temperature_unit == "C": return self._thermostat.celsius - + return self._thermostat.fahrenheit @property @@ -158,7 +156,7 @@ class NuHeatThermostat(ClimateDevice): if self._thermostat.schedule_mode != SCHEDULE_HOLD: return False - + return True def turn_away_mode_on(self): @@ -203,6 +201,6 @@ class NuHeatThermostat(ClimateDevice): self._throttled_update() @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _throttled_update(self): + def _throttled_update(self, **kwargs): """Get the latest state from the thermostat... but throttled!""" self._thermostat.get_data() diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 969afe1ee48..a3e110b71da 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at """ import logging -from datetime import timedelta - import voluptuous as vol from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES @@ -25,7 +23,8 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, cv.string) + vol.Required(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.string]), }), }, extra=vol.ALLOW_EXTRA) diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 33a1c3e02f7..c1b86d5d9e1 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -13,6 +13,7 @@ SCHEDULE_TEMPORARY_HOLD = 2 class TestNuHeat(unittest.TestCase): """Tests for NuHeat climate.""" + # pylint: disable=protected-access, no-self-use def setUp(self): @@ -41,6 +42,25 @@ class TestNuHeat(unittest.TestCase): self.thermostat = nuheat.NuHeatThermostat( api, serial_number, temperature_unit) + @patch("homeassistant.components.climate.nuheat.NuHeatThermostat") + def test_setup_platform(self, mocked_thermostat): + """Test setup_platform.""" + api = Mock() + data = {"nuheat": (api, ["12345"])} + + hass = Mock() + hass.config.units.temperature_unit.return_value = "F" + hass.data = Mock() + hass.data.__getitem__ = Mock(side_effect=data.__getitem__) + + config = {} + add_devices = Mock() + discovery_info = {} + + nuheat.setup_platform(hass, config, add_devices, discovery_info) + thermostats = [mocked_thermostat(api, "12345", "F")] + add_devices.assert_called_once_with(thermostats, True) + def test_name(self): """Test name property.""" self.assertEqual(self.thermostat.name, "Master bathroom") @@ -48,42 +68,36 @@ class TestNuHeat(unittest.TestCase): def test_temperature_unit(self): """Test temperature unit.""" self.assertEqual(self.thermostat.temperature_unit, TEMP_FAHRENHEIT) - self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.temperature_unit, TEMP_CELSIUS) def test_current_temperature(self): """Test current temperature.""" self.assertEqual(self.thermostat.current_temperature, 72) - self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.current_temperature, 22) def test_current_operation(self): """Test current operation.""" self.assertEqual(self.thermostat.current_operation, STATE_HEAT) - self.thermostat._thermostat.heating = False self.assertEqual(self.thermostat.current_operation, STATE_IDLE) def test_min_temp(self): """Test min temp.""" self.assertEqual(self.thermostat.min_temp, 41) - self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.min_temp, 5) def test_max_temp(self): """Test max temp.""" self.assertEqual(self.thermostat.max_temp, 157) - self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.max_temp, 69) def test_target_temperature(self): """Test target temperature.""" self.assertEqual(self.thermostat.target_temperature, 72) - self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.target_temperature, 22) @@ -91,14 +105,12 @@ class TestNuHeat(unittest.TestCase): def test_target_temperature_low(self): """Test low target temperature.""" self.assertEqual(self.thermostat.target_temperature_low, 72) - self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.target_temperature_low, 22) def test_target_temperature_high(self): """Test high target temperature.""" self.assertEqual(self.thermostat.target_temperature_high, 72) - self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.target_temperature_high, 22) @@ -143,7 +155,7 @@ class TestNuHeat(unittest.TestCase): @patch.object( nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) @patch.object(nuheat.NuHeatThermostat, "set_temperature") - def test_turn_away_mode_on_while_home(self, set_temp, is_away_mode_on): + def test_turn_away_mode_on_home(self, set_temp, is_away_mode_on): """Test turn away mode on when not away.""" is_away_mode_on.return_value = False self.thermostat.turn_away_mode_on() @@ -153,7 +165,7 @@ class TestNuHeat(unittest.TestCase): @patch.object( nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) @patch.object(nuheat.NuHeatThermostat, "set_temperature") - def test_turn_away_mode_on_while_away(self, set_temp, is_away_mode_on): + def test_turn_away_mode_on_away(self, set_temp, is_away_mode_on): """Test turn away mode on when away.""" is_away_mode_on.return_value = True self.thermostat.turn_away_mode_on() From 37be81c20c0c80a57331beb7dabb70c01339610a Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sat, 11 Nov 2017 16:21:14 -0600 Subject: [PATCH 03/27] add ability to resume program... and add in a forgotten test --- homeassistant/components/climate/nuheat.py | 5 ++++ tests/components/climate/test_nuheat.py | 28 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index 63369ed5769..ff1d1158ad1 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -175,6 +175,11 @@ class NuHeatThermostat(ClimateDevice): if not self.is_away_mode_on: return + self.resume_program() + self._force_update = True + + def resume_program(self): + """Resume the thermostat's programmed schedule.""" self._thermostat.resume_schedule() self._force_update = True diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index c1b86d5d9e1..83dbfac8449 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -36,6 +36,8 @@ class TestNuHeat(unittest.TestCase): target_celsius=22, target_fahrenheit=72) + thermostat.resume_schedule = Mock() + api = Mock() api.get_thermostat.return_value = thermostat @@ -171,6 +173,32 @@ class TestNuHeat(unittest.TestCase): self.thermostat.turn_away_mode_on() set_temp.assert_not_called() + @patch.object( + nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) + @patch.object(nuheat.NuHeatThermostat, "resume_program") + def test_turn_away_mode_off_home(self, resume, is_away_mode_on): + """Test turn away mode off when home.""" + is_away_mode_on.return_value = False + self.thermostat.turn_away_mode_off() + self.assertFalse(self.thermostat._force_update) + resume.assert_not_called() + + @patch.object( + nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) + @patch.object(nuheat.NuHeatThermostat, "resume_program") + def test_turn_away_mode_off_away(self, resume, is_away_mode_on): + """Test turn away mode off when away.""" + is_away_mode_on.return_value = True + self.thermostat.turn_away_mode_off() + self.assertTrue(self.thermostat._force_update) + resume.assert_called_once() + + def test_resume_program(self): + """Test resume schedule.""" + self.thermostat.resume_program() + self.thermostat._thermostat.resume_schedule.assert_called_once() + self.assertTrue(self.thermostat._force_update) + def test_set_temperature(self): """Test set temperature.""" self.thermostat.set_temperature(temperature=85) From 9b373901fa3217d4545a651658f587637ddaa15e Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sat, 11 Nov 2017 16:31:35 -0600 Subject: [PATCH 04/27] add documentation links --- homeassistant/components/climate/nuheat.py | 1 + homeassistant/components/nuheat.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index ff1d1158ad1..3d922edeb2c 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -2,6 +2,7 @@ Support for NuHeat thermostats. For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.nuheat/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index a3e110b71da..4c3bb84e6b4 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -2,6 +2,7 @@ Support for NuHeat thermostats. For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/nuheat/ """ import logging From 2c44e4fb1278ab2bbe1d376a31674eb8b504d8dd Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sat, 11 Nov 2017 16:47:12 -0600 Subject: [PATCH 05/27] address initial houndbot suggestions --- homeassistant/components/nuheat.py | 1 + tests/components/climate/test_nuheat.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 4c3bb84e6b4..9fa555b380e 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -29,6 +29,7 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) + def setup(hass, config): """Set up the NuHeat thermostat component.""" import nuheat diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 83dbfac8449..df4c72b59ce 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -103,7 +103,6 @@ class TestNuHeat(unittest.TestCase): self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.target_temperature, 22) - def test_target_temperature_low(self): """Test low target temperature.""" self.assertEqual(self.thermostat.target_temperature_low, 72) From 7859b76429bd28abdc89829667f4cd1a9d1626ab Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 10:35:48 -0600 Subject: [PATCH 06/27] kill target_temperature_low and high. They don't make sense here --- homeassistant/components/climate/nuheat.py | 10 ---------- tests/components/climate/test_nuheat.py | 12 ------------ 2 files changed, 22 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index 3d922edeb2c..1979c0dfa00 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -111,16 +111,6 @@ class NuHeatThermostat(ClimateDevice): return self._thermostat.target_fahrenheit - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - return self.target_temperature - - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - return self.target_temperature - @property def current_hold_mode(self): """Return current hold mode.""" diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index df4c72b59ce..7a86157d469 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -103,18 +103,6 @@ class TestNuHeat(unittest.TestCase): self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.target_temperature, 22) - def test_target_temperature_low(self): - """Test low target temperature.""" - self.assertEqual(self.thermostat.target_temperature_low, 72) - self.thermostat._temperature_unit = "C" - self.assertEqual(self.thermostat.target_temperature_low, 22) - - def test_target_temperature_high(self): - """Test high target temperature.""" - self.assertEqual(self.thermostat.target_temperature_high, 72) - self.thermostat._temperature_unit = "C" - self.assertEqual(self.thermostat.target_temperature_high, 22) - @patch.object( nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) def test_current_hold_mode_away(self, is_away_mode_on): From f21b9988e91dae8539db28fcd3c700c11c3fa858 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 11:00:33 -0600 Subject: [PATCH 07/27] allow for the configuring of a minimum away temperature --- homeassistant/components/climate/nuheat.py | 17 +++++++++++++---- homeassistant/components/nuheat.py | 6 +++++- tests/components/climate/test_nuheat.py | 13 ++++++++++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index 1979c0dfa00..569726fc684 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -43,9 +43,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.info("Loading NuHeat thermostat climate component") temperature_unit = hass.config.units.temperature_unit - api, serial_numbers = hass.data[DATA_NUHEAT] + api, serial_numbers, min_away_temp = hass.data[DATA_NUHEAT] thermostats = [ - NuHeatThermostat(api, serial_number, temperature_unit) + NuHeatThermostat(api, serial_number, min_away_temp, temperature_unit) for serial_number in serial_numbers ] add_devices(thermostats, True) @@ -53,9 +53,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class NuHeatThermostat(ClimateDevice): """Representation of a NuHeat Thermostat.""" - def __init__(self, api, serial_number, temperature_unit): + def __init__(self, api, serial_number, min_away_temp, temperature_unit): self._thermostat = api.get_thermostat(serial_number) self._temperature_unit = temperature_unit + self._min_away_temp = min_away_temp self._force_update = False @property @@ -87,6 +88,14 @@ class NuHeatThermostat(ClimateDevice): return STATE_IDLE + @property + def min_away_temp(self): + """Return the minimum target temperature to be used in away mode.""" + if self._min_away_temp: + return self._min_away_temp + + return self.min_temp + @property def min_temp(self): """Return the minimum supported temperature for the thermostat.""" @@ -156,7 +165,7 @@ class NuHeatThermostat(ClimateDevice): return kwargs = {} - kwargs[ATTR_TEMPERATURE] = self.min_temp + kwargs[ATTR_TEMPERATURE] = self.min_away_temp self.set_temperature(**kwargs) self._force_update = True diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 9fa555b380e..5358932cd78 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -20,12 +20,15 @@ DATA_NUHEAT = "nuheat" DOMAIN = "nuheat" +CONF_MIN_AWAY_TEMP = 'min_away_temp' + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_MIN_AWAY_TEMP): cv.string, }), }, extra=vol.ALLOW_EXTRA) @@ -38,10 +41,11 @@ def setup(hass, config): username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) devices = conf.get(CONF_DEVICES) + min_away_temp = conf.get(CONF_MIN_AWAY_TEMP) api = nuheat.NuHeat(username, password) api.authenticate() - hass.data[DATA_NUHEAT] = (api, devices) + hass.data[DATA_NUHEAT] = (api, devices, min_away_temp) discovery.load_platform(hass, "climate", DOMAIN, {}, config) _LOGGER.debug("NuHeat initialized") diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 7a86157d469..bcb1b0f3312 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -18,6 +18,7 @@ class TestNuHeat(unittest.TestCase): def setUp(self): serial_number = "12345" + min_away_temp = None temperature_unit = "F" thermostat = Mock( @@ -42,13 +43,13 @@ class TestNuHeat(unittest.TestCase): api.get_thermostat.return_value = thermostat self.thermostat = nuheat.NuHeatThermostat( - api, serial_number, temperature_unit) + api, serial_number, min_away_temp, temperature_unit) @patch("homeassistant.components.climate.nuheat.NuHeatThermostat") def test_setup_platform(self, mocked_thermostat): """Test setup_platform.""" api = Mock() - data = {"nuheat": (api, ["12345"])} + data = {"nuheat": (api, ["12345"], 50)} hass = Mock() hass.config.units.temperature_unit.return_value = "F" @@ -60,7 +61,7 @@ class TestNuHeat(unittest.TestCase): discovery_info = {} nuheat.setup_platform(hass, config, add_devices, discovery_info) - thermostats = [mocked_thermostat(api, "12345", "F")] + thermostats = [mocked_thermostat(api, "12345", 50, "F")] add_devices.assert_called_once_with(thermostats, True) def test_name(self): @@ -85,6 +86,12 @@ class TestNuHeat(unittest.TestCase): self.thermostat._thermostat.heating = False self.assertEqual(self.thermostat.current_operation, STATE_IDLE) + def test_min_away_temp(self): + """Test the minimum target temperature to be used in away mode.""" + self.assertEqual(self.thermostat.min_away_temp, 41) + self.thermostat._min_away_temp = 60 + self.assertEqual(self.thermostat.min_away_temp, 60) + def test_min_temp(self): """Test min temp.""" self.assertEqual(self.thermostat.min_temp, 41) From ef5edb95ba59b19cfd9f9f8a473f297a3d69608c Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 11:38:08 -0600 Subject: [PATCH 08/27] Update home/auto hold mode to be consistent with current documentation --- homeassistant/components/climate/nuheat.py | 4 +++- tests/components/climate/test_nuheat.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index 569726fc684..fc49399c2cc 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -14,6 +14,7 @@ from homeassistant.components.climate import ( from homeassistant.components.nuheat import DATA_NUHEAT from homeassistant.const import ( ATTR_TEMPERATURE, + STATE_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) from homeassistant.util import Throttle @@ -24,7 +25,8 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -MODE_AUTO = "auto" # Run device schedule +# Hold modes +MODE_AUTO = STATE_HOME # Run device schedule MODE_AWAY = "away" MODE_HOLD_TEMPERATURE = "temperature" MODE_TEMPORARY_HOLD = "temporary_temperature" diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index bcb1b0f3312..ba0f6c8a6d6 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -4,7 +4,7 @@ from unittest.mock import PropertyMock, Mock, patch from homeassistant.components.climate import STATE_HEAT, STATE_IDLE import homeassistant.components.climate.nuheat as nuheat -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import STATE_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT SCHEDULE_HOLD = 3 SCHEDULE_RUN = 1 From 766893253a7164536c9247f2d435bdf38f80aa74 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 12:28:09 -0600 Subject: [PATCH 09/27] make sure is_away_mode_on supports user-defined minimum away temps --- homeassistant/components/climate/nuheat.py | 16 ++++++++++++++-- tests/components/climate/test_nuheat.py | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index fc49399c2cc..d49d060f032 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -151,9 +151,21 @@ class NuHeatThermostat(ClimateDevice): Return true if away mode is on. Away mode is determined by setting and HOLDing the target temperature - to the minimum temperature supported. + to the user-defined minimum away temperature or the minimum + temperature supported by the thermostat. """ - if self._thermostat.target_celsius > self._thermostat.min_celsius: + if self._min_away_temp: + if self._temperature_unit == "C": + min_target = self._min_away_temp + target = self._thermostat.target_celsius + else: + min_target = self._min_away_temp + target = self._thermostat.target_fahrenheit + + if target > min_target: + return False + + elif self._thermostat.target_celsius > self._thermostat.min_celsius: return False if self._thermostat.schedule_mode != SCHEDULE_HOLD: diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index ba0f6c8a6d6..558122dd366 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -137,13 +137,29 @@ class TestNuHeat(unittest.TestCase): def test_is_away_mode_on(self): """Test is away mode on.""" _thermostat = self.thermostat._thermostat - _thermostat.target_celsius = _thermostat.min_celsius _thermostat.schedule_mode = SCHEDULE_HOLD + + # user-defined minimum fahrenheit + self.thermostat._min_away_temp = 59 + _thermostat.target_fahrenheit = 59 self.assertTrue(self.thermostat.is_away_mode_on) + # user-defined minimum celsius + self.thermostat._temperature_unit = "C" + self.thermostat._min_away_temp = 15 + _thermostat.target_celsius = 15 + self.assertTrue(self.thermostat.is_away_mode_on) + + # thermostat's minimum supported temperature + self.thermostat._min_away_temp = None + _thermostat.target_celsius = _thermostat.min_celsius + self.assertTrue(self.thermostat.is_away_mode_on) + + # thermostat held at a temperature above the minimum _thermostat.target_celsius = _thermostat.min_celsius + 1 self.assertFalse(self.thermostat.is_away_mode_on) + # thermostat not on HOLD _thermostat.target_celsius = _thermostat.min_celsius _thermostat.schedule_mode = SCHEDULE_RUN self.assertFalse(self.thermostat.is_away_mode_on) From 6892033556968abb857c5027e592a57837209715 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 12:31:15 -0600 Subject: [PATCH 10/27] remove that unused constant --- tests/components/climate/test_nuheat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 558122dd366..5a9f1d95a3f 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -4,7 +4,7 @@ from unittest.mock import PropertyMock, Mock, patch from homeassistant.components.climate import STATE_HEAT, STATE_IDLE import homeassistant.components.climate.nuheat as nuheat -from homeassistant.const import STATE_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT SCHEDULE_HOLD = 3 SCHEDULE_RUN = 1 From f1fe8e95baafcbc96dab12e9b45e78158fca1db6 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 22:40:18 -0600 Subject: [PATCH 11/27] clean up a couple away temperature settings --- homeassistant/components/climate/nuheat.py | 21 ++++++++------------- homeassistant/components/nuheat.py | 11 ++++++++++- tests/components/climate/test_nuheat.py | 6 ++++++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index d49d060f032..c5893ecdb00 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -93,8 +93,8 @@ class NuHeatThermostat(ClimateDevice): @property def min_away_temp(self): """Return the minimum target temperature to be used in away mode.""" - if self._min_away_temp: - return self._min_away_temp + if self._min_away_temp and self._min_away_temp > self.min_temp: + return int(self._min_away_temp) return self.min_temp @@ -154,18 +154,13 @@ class NuHeatThermostat(ClimateDevice): to the user-defined minimum away temperature or the minimum temperature supported by the thermostat. """ - if self._min_away_temp: - if self._temperature_unit == "C": - min_target = self._min_away_temp - target = self._thermostat.target_celsius - else: - min_target = self._min_away_temp - target = self._thermostat.target_fahrenheit + min_target = self.min_away_temp + if self._temperature_unit == "C": + target = self._thermostat.target_celsius + else: + target = self._thermostat.target_fahrenheit - if target > min_target: - return False - - elif self._thermostat.target_celsius > self._thermostat.min_celsius: + if target > min_target: return False if self._thermostat.schedule_mode != SCHEDULE_HOLD: diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 5358932cd78..5a888457e40 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -41,7 +41,16 @@ def setup(hass, config): username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) devices = conf.get(CONF_DEVICES) - min_away_temp = conf.get(CONF_MIN_AWAY_TEMP) + + min_away_temp = None + _min_away_temp = conf.get(CONF_MIN_AWAY_TEMP) + if _min_away_temp: + try: + min_away_temp = int(_min_away_temp) + except ValueError: + _LOGGER.error( + "Configuration error. %s.%s=%s is invalid. Please provide a " + "numeric value.", DATA_NUHEAT, CONF_MIN_AWAY_TEMP, _min_away_temp) api = nuheat.NuHeat(username, password) api.authenticate() diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 5a9f1d95a3f..ab5624e82f3 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -89,9 +89,15 @@ class TestNuHeat(unittest.TestCase): def test_min_away_temp(self): """Test the minimum target temperature to be used in away mode.""" self.assertEqual(self.thermostat.min_away_temp, 41) + + # User defined minimum self.thermostat._min_away_temp = 60 self.assertEqual(self.thermostat.min_away_temp, 60) + # User defined minimum below the thermostat's supported minimum + self.thermostat._min_away_temp = 0 + self.assertEqual(self.thermostat.min_away_temp, 41) + def test_min_temp(self): """Test min temp.""" self.assertEqual(self.thermostat.min_temp, 41) From 959f6386b47d46922ce6da4eff9834e2b1339fa3 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 22:43:11 -0600 Subject: [PATCH 12/27] shorten that long line --- homeassistant/components/nuheat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 5a888457e40..69cd674eed2 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -50,7 +50,8 @@ def setup(hass, config): except ValueError: _LOGGER.error( "Configuration error. %s.%s=%s is invalid. Please provide a " - "numeric value.", DATA_NUHEAT, CONF_MIN_AWAY_TEMP, _min_away_temp) + "numeric value.", DATA_NUHEAT, CONF_MIN_AWAY_TEMP, + _min_away_temp) api = nuheat.NuHeat(username, password) api.authenticate() From c0c439c549ec01a9d4dd61ea7ff375746dda0df9 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 22:45:29 -0600 Subject: [PATCH 13/27] that int() casting was redundant --- homeassistant/components/climate/nuheat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index c5893ecdb00..deaf9b071b9 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -94,7 +94,7 @@ class NuHeatThermostat(ClimateDevice): def min_away_temp(self): """Return the minimum target temperature to be used in away mode.""" if self._min_away_temp and self._min_away_temp > self.min_temp: - return int(self._min_away_temp) + return self._min_away_temp return self.min_temp From afcb0b876713a382feb930288e0280a17151e94f Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 23:13:04 -0600 Subject: [PATCH 14/27] fix up some docstrings --- homeassistant/components/climate/nuheat.py | 11 ++++++++++- tests/components/climate/test_nuheat.py | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index deaf9b071b9..b1779ba5e5f 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -23,6 +23,8 @@ DEPENDENCIES = ["nuheat"] _LOGGER = logging.getLogger(__name__) +ICON = "mdi:thermometer" + MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) # Hold modes @@ -55,7 +57,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class NuHeatThermostat(ClimateDevice): """Representation of a NuHeat Thermostat.""" + def __init__(self, api, serial_number, min_away_temp, temperature_unit): + """Initialize the thermostat.""" self._thermostat = api.get_thermostat(serial_number) self._temperature_unit = temperature_unit self._min_away_temp = min_away_temp @@ -66,6 +70,11 @@ class NuHeatThermostat(ClimateDevice): """Return the name of the thermostat.""" return self._thermostat.room + @property + def icon(self): + """Return the icon to use in the frontend.""" + return ICON + @property def temperature_unit(self): """Return the unit of measurement.""" @@ -216,5 +225,5 @@ class NuHeatThermostat(ClimateDevice): @Throttle(MIN_TIME_BETWEEN_UPDATES) def _throttled_update(self, **kwargs): - """Get the latest state from the thermostat... but throttled!""" + """Get the latest state from the thermostat with a throttle.""" self._thermostat.get_data() diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index ab5624e82f3..1d178e255fa 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -13,10 +13,11 @@ SCHEDULE_TEMPORARY_HOLD = 2 class TestNuHeat(unittest.TestCase): """Tests for NuHeat climate.""" + # pylint: disable=protected-access, no-self-use def setUp(self): - + """Set up test variables.""" serial_number = "12345" min_away_temp = None temperature_unit = "F" @@ -68,6 +69,10 @@ class TestNuHeat(unittest.TestCase): """Test name property.""" self.assertEqual(self.thermostat.name, "Master bathroom") + def test_icon(self): + """Test name property.""" + self.assertEqual(self.thermostat.icon, "mdi:thermometer") + def test_temperature_unit(self): """Test temperature unit.""" self.assertEqual(self.thermostat.temperature_unit, TEMP_FAHRENHEIT) From a3c6211c04110edce78687e6edef2c33a9095f97 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Mon, 13 Nov 2017 23:20:16 -0600 Subject: [PATCH 15/27] python 3.5 seems to not like assert_called_once() --- tests/components/climate/test_nuheat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 1d178e255fa..0b3b8c91ef4 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -212,12 +212,12 @@ class TestNuHeat(unittest.TestCase): is_away_mode_on.return_value = True self.thermostat.turn_away_mode_off() self.assertTrue(self.thermostat._force_update) - resume.assert_called_once() + resume.assert_called_once_with() def test_resume_program(self): """Test resume schedule.""" self.thermostat.resume_program() - self.thermostat._thermostat.resume_schedule.assert_called_once() + self.thermostat._thermostat.resume_schedule.assert_called_once_with() self.assertTrue(self.thermostat._force_update) def test_set_temperature(self): From a9feafd571c400a317a78f0d1dd76d5a6aa6f885 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sat, 18 Nov 2017 10:26:36 -0600 Subject: [PATCH 16/27] add nuheat to coverage reports --- .coveragerc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 4de7d138f71..3bfd983dc30 100644 --- a/.coveragerc +++ b/.coveragerc @@ -143,9 +143,6 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py - homeassistant/components/nuheat.py - homeassistant/components/*/nuheat.py - homeassistant/components/octoprint.py homeassistant/components/*/octoprint.py From 36d5fff8e04f447d21b935cb9dc397908dbe091b Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Wed, 6 Dec 2017 21:52:44 -0600 Subject: [PATCH 17/27] address feedback on log lines --- homeassistant/components/climate/nuheat.py | 3 +-- homeassistant/components/nuheat.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index b1779ba5e5f..696a6961cb3 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - _LOGGER.info("Loading NuHeat thermostat climate component") temperature_unit = hass.config.units.temperature_unit api, serial_numbers, min_away_temp = hass.data[DATA_NUHEAT] thermostats = [ @@ -209,7 +208,7 @@ class NuHeatThermostat(ClimateDevice): else: self._thermostat.target_fahrenheit = temperature - _LOGGER.info( + _LOGGER.debug( "Setting NuHeat thermostat temperature to %s %s", temperature, self.temperature_unit) diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 69cd674eed2..6a85a0dafa1 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -58,5 +58,4 @@ def setup(hass, config): hass.data[DATA_NUHEAT] = (api, devices, min_away_temp) discovery.load_platform(hass, "climate", DOMAIN, {}, config) - _LOGGER.debug("NuHeat initialized") return True From c262a387dc83c2813c8cdf861fcb58b05a7b5b1b Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Wed, 6 Dec 2017 22:24:54 -0600 Subject: [PATCH 18/27] add supported_features functionality --- homeassistant/components/climate/nuheat.py | 12 ++++++++++++ tests/components/climate/test_nuheat.py | 14 +++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index 696a6961cb3..ae3aff8c673 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -9,6 +9,10 @@ from datetime import timedelta from homeassistant.components.climate import ( ClimateDevice, + SUPPORT_AWAY_MODE, + SUPPORT_HOLD_MODE, + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, STATE_HEAT, STATE_IDLE) from homeassistant.components.nuheat import DATA_NUHEAT @@ -39,6 +43,9 @@ SCHEDULE_HOLD = 3 SCHEDULE_RUN = 1 SCHEDULE_TEMPORARY_HOLD = 2 +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE | + SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the NuHeat thermostat(s).""" @@ -74,6 +81,11 @@ class NuHeatThermostat(ClimateDevice): """Return the icon to use in the frontend.""" return ICON + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 0b3b8c91ef4..a9946c49bce 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -2,7 +2,13 @@ import unittest from unittest.mock import PropertyMock, Mock, patch -from homeassistant.components.climate import STATE_HEAT, STATE_IDLE +from homeassistant.components.climate import ( + SUPPORT_AWAY_MODE, + SUPPORT_HOLD_MODE, + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + STATE_HEAT, + STATE_IDLE) import homeassistant.components.climate.nuheat as nuheat from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -73,6 +79,12 @@ class TestNuHeat(unittest.TestCase): """Test name property.""" self.assertEqual(self.thermostat.icon, "mdi:thermometer") + def test_supported_features(self): + """Test name property.""" + features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE | + SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE) + self.assertEqual(self.thermostat.supported_features, features) + def test_temperature_unit(self): """Test temperature unit.""" self.assertEqual(self.thermostat.temperature_unit, TEMP_FAHRENHEIT) From 3193e825d5afeba7a8d1518d34403e2ed1326090 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Wed, 6 Dec 2017 22:34:13 -0600 Subject: [PATCH 19/27] remove nuheat away functionality. :( --- homeassistant/components/climate/nuheat.py | 64 +------------ homeassistant/components/nuheat.py | 16 +--- tests/components/climate/test_nuheat.py | 106 ++------------------- 3 files changed, 11 insertions(+), 175 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index ae3aff8c673..f08c2d7b7d5 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -9,7 +9,6 @@ from datetime import timedelta from homeassistant.components.climate import ( ClimateDevice, - SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, @@ -33,7 +32,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) # Hold modes MODE_AUTO = STATE_HOME # Run device schedule -MODE_AWAY = "away" MODE_HOLD_TEMPERATURE = "temperature" MODE_TEMPORARY_HOLD = "temporary_temperature" @@ -44,7 +42,7 @@ SCHEDULE_RUN = 1 SCHEDULE_TEMPORARY_HOLD = 2 SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE | - SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE) + SUPPORT_OPERATION_MODE) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -53,9 +51,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return temperature_unit = hass.config.units.temperature_unit - api, serial_numbers, min_away_temp = hass.data[DATA_NUHEAT] + api, serial_numbers = hass.data[DATA_NUHEAT] thermostats = [ - NuHeatThermostat(api, serial_number, min_away_temp, temperature_unit) + NuHeatThermostat(api, serial_number, temperature_unit) for serial_number in serial_numbers ] add_devices(thermostats, True) @@ -64,11 +62,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class NuHeatThermostat(ClimateDevice): """Representation of a NuHeat Thermostat.""" - def __init__(self, api, serial_number, min_away_temp, temperature_unit): + def __init__(self, api, serial_number, temperature_unit): """Initialize the thermostat.""" self._thermostat = api.get_thermostat(serial_number) self._temperature_unit = temperature_unit - self._min_away_temp = min_away_temp self._force_update = False @property @@ -110,14 +107,6 @@ class NuHeatThermostat(ClimateDevice): return STATE_IDLE - @property - def min_away_temp(self): - """Return the minimum target temperature to be used in away mode.""" - if self._min_away_temp and self._min_away_temp > self.min_temp: - return self._min_away_temp - - return self.min_temp - @property def min_temp(self): """Return the minimum supported temperature for the thermostat.""" @@ -145,9 +134,6 @@ class NuHeatThermostat(ClimateDevice): @property def current_hold_mode(self): """Return current hold mode.""" - if self.is_away_mode_on: - return MODE_AWAY - schedule_mode = self._thermostat.schedule_mode if schedule_mode == SCHEDULE_RUN: return MODE_AUTO @@ -165,48 +151,6 @@ class NuHeatThermostat(ClimateDevice): """Return list of possible operation modes.""" return OPERATION_LIST - @property - def is_away_mode_on(self): - """ - Return true if away mode is on. - - Away mode is determined by setting and HOLDing the target temperature - to the user-defined minimum away temperature or the minimum - temperature supported by the thermostat. - """ - min_target = self.min_away_temp - if self._temperature_unit == "C": - target = self._thermostat.target_celsius - else: - target = self._thermostat.target_fahrenheit - - if target > min_target: - return False - - if self._thermostat.schedule_mode != SCHEDULE_HOLD: - return False - - return True - - def turn_away_mode_on(self): - """Turn away mode on.""" - if self.is_away_mode_on: - return - - kwargs = {} - kwargs[ATTR_TEMPERATURE] = self.min_away_temp - - self.set_temperature(**kwargs) - self._force_update = True - - def turn_away_mode_off(self): - """Turn away mode off.""" - if not self.is_away_mode_on: - return - - self.resume_program() - self._force_update = True - def resume_program(self): """Resume the thermostat's programmed schedule.""" self._thermostat.resume_schedule() diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 6a85a0dafa1..08941359dc8 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -20,15 +20,12 @@ DATA_NUHEAT = "nuheat" DOMAIN = "nuheat" -CONF_MIN_AWAY_TEMP = 'min_away_temp' - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_MIN_AWAY_TEMP): cv.string, }), }, extra=vol.ALLOW_EXTRA) @@ -42,20 +39,9 @@ def setup(hass, config): password = conf.get(CONF_PASSWORD) devices = conf.get(CONF_DEVICES) - min_away_temp = None - _min_away_temp = conf.get(CONF_MIN_AWAY_TEMP) - if _min_away_temp: - try: - min_away_temp = int(_min_away_temp) - except ValueError: - _LOGGER.error( - "Configuration error. %s.%s=%s is invalid. Please provide a " - "numeric value.", DATA_NUHEAT, CONF_MIN_AWAY_TEMP, - _min_away_temp) - api = nuheat.NuHeat(username, password) api.authenticate() - hass.data[DATA_NUHEAT] = (api, devices, min_away_temp) + hass.data[DATA_NUHEAT] = (api, devices) discovery.load_platform(hass, "climate", DOMAIN, {}, config) return True diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index a9946c49bce..b2b3e6cddff 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -1,9 +1,8 @@ """The test for the NuHeat thermostat module.""" import unittest -from unittest.mock import PropertyMock, Mock, patch +from unittest.mock import Mock, patch from homeassistant.components.climate import ( - SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, @@ -25,7 +24,6 @@ class TestNuHeat(unittest.TestCase): def setUp(self): """Set up test variables.""" serial_number = "12345" - min_away_temp = None temperature_unit = "F" thermostat = Mock( @@ -50,13 +48,13 @@ class TestNuHeat(unittest.TestCase): api.get_thermostat.return_value = thermostat self.thermostat = nuheat.NuHeatThermostat( - api, serial_number, min_away_temp, temperature_unit) + api, serial_number, temperature_unit) @patch("homeassistant.components.climate.nuheat.NuHeatThermostat") def test_setup_platform(self, mocked_thermostat): """Test setup_platform.""" api = Mock() - data = {"nuheat": (api, ["12345"], 50)} + data = {"nuheat": (api, ["12345"])} hass = Mock() hass.config.units.temperature_unit.return_value = "F" @@ -68,7 +66,7 @@ class TestNuHeat(unittest.TestCase): discovery_info = {} nuheat.setup_platform(hass, config, add_devices, discovery_info) - thermostats = [mocked_thermostat(api, "12345", 50, "F")] + thermostats = [mocked_thermostat(api, "12345", "F")] add_devices.assert_called_once_with(thermostats, True) def test_name(self): @@ -82,7 +80,7 @@ class TestNuHeat(unittest.TestCase): def test_supported_features(self): """Test name property.""" features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE | - SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE) + SUPPORT_OPERATION_MODE) self.assertEqual(self.thermostat.supported_features, features) def test_temperature_unit(self): @@ -103,18 +101,6 @@ class TestNuHeat(unittest.TestCase): self.thermostat._thermostat.heating = False self.assertEqual(self.thermostat.current_operation, STATE_IDLE) - def test_min_away_temp(self): - """Test the minimum target temperature to be used in away mode.""" - self.assertEqual(self.thermostat.min_away_temp, 41) - - # User defined minimum - self.thermostat._min_away_temp = 60 - self.assertEqual(self.thermostat.min_away_temp, 60) - - # User defined minimum below the thermostat's supported minimum - self.thermostat._min_away_temp = 0 - self.assertEqual(self.thermostat.min_away_temp, 41) - def test_min_temp(self): """Test min temp.""" self.assertEqual(self.thermostat.min_temp, 41) @@ -133,19 +119,8 @@ class TestNuHeat(unittest.TestCase): self.thermostat._temperature_unit = "C" self.assertEqual(self.thermostat.target_temperature, 22) - @patch.object( - nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) - def test_current_hold_mode_away(self, is_away_mode_on): - """Test current hold mode while away.""" - is_away_mode_on.return_value = True - self.assertEqual(self.thermostat.current_hold_mode, nuheat.MODE_AWAY) - - @patch.object( - nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) - def test_current_hold_mode(self, is_away_mode_on): + def test_current_hold_mode(self): """Test current hold mode.""" - is_away_mode_on.return_value = False - self.thermostat._thermostat.schedule_mode = SCHEDULE_RUN self.assertEqual(self.thermostat.current_hold_mode, nuheat.MODE_AUTO) @@ -157,75 +132,6 @@ class TestNuHeat(unittest.TestCase): self.assertEqual( self.thermostat.current_hold_mode, nuheat.MODE_TEMPORARY_HOLD) - def test_is_away_mode_on(self): - """Test is away mode on.""" - _thermostat = self.thermostat._thermostat - _thermostat.schedule_mode = SCHEDULE_HOLD - - # user-defined minimum fahrenheit - self.thermostat._min_away_temp = 59 - _thermostat.target_fahrenheit = 59 - self.assertTrue(self.thermostat.is_away_mode_on) - - # user-defined minimum celsius - self.thermostat._temperature_unit = "C" - self.thermostat._min_away_temp = 15 - _thermostat.target_celsius = 15 - self.assertTrue(self.thermostat.is_away_mode_on) - - # thermostat's minimum supported temperature - self.thermostat._min_away_temp = None - _thermostat.target_celsius = _thermostat.min_celsius - self.assertTrue(self.thermostat.is_away_mode_on) - - # thermostat held at a temperature above the minimum - _thermostat.target_celsius = _thermostat.min_celsius + 1 - self.assertFalse(self.thermostat.is_away_mode_on) - - # thermostat not on HOLD - _thermostat.target_celsius = _thermostat.min_celsius - _thermostat.schedule_mode = SCHEDULE_RUN - self.assertFalse(self.thermostat.is_away_mode_on) - - @patch.object( - nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) - @patch.object(nuheat.NuHeatThermostat, "set_temperature") - def test_turn_away_mode_on_home(self, set_temp, is_away_mode_on): - """Test turn away mode on when not away.""" - is_away_mode_on.return_value = False - self.thermostat.turn_away_mode_on() - set_temp.assert_called_once_with(temperature=self.thermostat.min_temp) - self.assertTrue(self.thermostat._force_update) - - @patch.object( - nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) - @patch.object(nuheat.NuHeatThermostat, "set_temperature") - def test_turn_away_mode_on_away(self, set_temp, is_away_mode_on): - """Test turn away mode on when away.""" - is_away_mode_on.return_value = True - self.thermostat.turn_away_mode_on() - set_temp.assert_not_called() - - @patch.object( - nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) - @patch.object(nuheat.NuHeatThermostat, "resume_program") - def test_turn_away_mode_off_home(self, resume, is_away_mode_on): - """Test turn away mode off when home.""" - is_away_mode_on.return_value = False - self.thermostat.turn_away_mode_off() - self.assertFalse(self.thermostat._force_update) - resume.assert_not_called() - - @patch.object( - nuheat.NuHeatThermostat, "is_away_mode_on", new_callable=PropertyMock) - @patch.object(nuheat.NuHeatThermostat, "resume_program") - def test_turn_away_mode_off_away(self, resume, is_away_mode_on): - """Test turn away mode off when away.""" - is_away_mode_on.return_value = True - self.thermostat.turn_away_mode_off() - self.assertTrue(self.thermostat._force_update) - resume.assert_called_once_with() - def test_resume_program(self): """Test resume schedule.""" self.thermostat.resume_program() From 419ec7f7a7df5dc7d8fd1bfb40a2ca09cfc66ce4 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sun, 24 Dec 2017 09:43:56 -0700 Subject: [PATCH 20/27] bump to python-nuheat 0.3.0 --- homeassistant/components/nuheat.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 08941359dc8..41db3e51842 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery -REQUIREMENTS = ["nuheat==0.2.0"] +REQUIREMENTS = ["nuheat==0.3.0"] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 66f5abffd09..e7d0f348e42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -501,7 +501,7 @@ neurio==0.3.1 nsapi==2.7.4 # homeassistant.components.nuheat -nuheat==0.2.0 +nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv From fb90dab471d5078936693030888a64ebc53ec2f3 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sun, 24 Dec 2017 10:09:27 -0700 Subject: [PATCH 21/27] add ability to change the Nuheat thermostat hold mode --- homeassistant/components/climate/nuheat.py | 18 ++++++++++++++++-- tests/components/climate/test_nuheat.py | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index f08c2d7b7d5..c1cb4651c6c 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -12,12 +12,12 @@ from homeassistant.components.climate import ( SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + STATE_AUTO, STATE_HEAT, STATE_IDLE) from homeassistant.components.nuheat import DATA_NUHEAT from homeassistant.const import ( ATTR_TEMPERATURE, - STATE_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) from homeassistant.util import Throttle @@ -31,7 +31,7 @@ ICON = "mdi:thermometer" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) # Hold modes -MODE_AUTO = STATE_HOME # Run device schedule +MODE_AUTO = STATE_AUTO # Run device schedule MODE_HOLD_TEMPERATURE = "temperature" MODE_TEMPORARY_HOLD = "temporary_temperature" @@ -156,6 +156,20 @@ class NuHeatThermostat(ClimateDevice): self._thermostat.resume_schedule() self._force_update = True + def set_hold_mode(self, hold_mode, **kwargs): + """Update the hold mode of the thermostat.""" + if hold_mode == MODE_AUTO: + schedule_mode = SCHEDULE_RUN + + if hold_mode == MODE_HOLD_TEMPERATURE: + schedule_mode = SCHEDULE_HOLD + + if hold_mode == MODE_TEMPORARY_HOLD: + schedule_mode = SCHEDULE_TEMPORARY_HOLD + + self._thermostat.schedule_mode = schedule_mode + self._force_update = True + def set_temperature(self, **kwargs): """Set a new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index b2b3e6cddff..3e30e7caaf7 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -138,6 +138,20 @@ class TestNuHeat(unittest.TestCase): self.thermostat._thermostat.resume_schedule.assert_called_once_with() self.assertTrue(self.thermostat._force_update) + def test_set_hold_mode(self): + """Test set hold mode.""" + self.thermostat.set_hold_mode("temperature") + self.assertEqual(self.thermostat._thermostat.schedule_mode, SCHEDULE_HOLD) + self.assertTrue(self.thermostat._force_update) + + self.thermostat.set_hold_mode("temporary_temperature") + self.assertEqual(self.thermostat._thermostat.schedule_mode, SCHEDULE_TEMPORARY_HOLD) + self.assertTrue(self.thermostat._force_update) + + self.thermostat.set_hold_mode("auto") + self.assertEqual(self.thermostat._thermostat.schedule_mode, SCHEDULE_RUN) + self.assertTrue(self.thermostat._force_update) + def test_set_temperature(self): """Test set temperature.""" self.thermostat.set_temperature(temperature=85) From 7de3c62846e8367e64d9c43647c380b8ac2145bd Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sun, 24 Dec 2017 11:10:22 -0700 Subject: [PATCH 22/27] register nuheat_resume_program service --- homeassistant/components/climate/nuheat.py | 37 +++++++++++++++++++ .../components/climate/services.yaml | 7 ++++ 2 files changed, 44 insertions(+) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index c1cb4651c6c..226c92a6e65 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -6,9 +6,13 @@ https://home-assistant.io/components/climate.nuheat/ """ import logging from datetime import timedelta +from os import path + +import voluptuous as vol from homeassistant.components.climate import ( ClimateDevice, + DOMAIN, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, @@ -16,10 +20,13 @@ from homeassistant.components.climate import ( STATE_HEAT, STATE_IDLE) from homeassistant.components.nuheat import DATA_NUHEAT +from homeassistant.config import load_yaml_config_file from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle DEPENDENCIES = ["nuheat"] @@ -41,6 +48,12 @@ SCHEDULE_HOLD = 3 SCHEDULE_RUN = 1 SCHEDULE_TEMPORARY_HOLD = 2 +SERVICE_RESUME_PROGRAM = "nuheat_resume_program" + +RESUME_PROGRAM_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids +}) + SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE) @@ -58,6 +71,30 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ] add_devices(thermostats, True) + def resume_program_set_service(service): + """Resume the program on the target thermostats.""" + entity_id = service.data.get(ATTR_ENTITY_ID) + + if entity_id: + target_thermostats = [device for device in thermostats + if device.entity_id in entity_id] + else: + target_thermostats = thermostats + + for thermostat in target_thermostats: + thermostat.resume_program() + + thermostat.schedule_update_ha_state(True) + + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), "services.yaml")) + + hass.services.register( + DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, + descriptions.get(SERVICE_RESUME_PROGRAM), + schema=RESUME_PROGRAM_SCHEMA) + + class NuHeatThermostat(ClimateDevice): """Representation of a NuHeat Thermostat.""" diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 193c5107575..5d7f30d252d 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -100,3 +100,10 @@ ecobee_resume_program: resume_all: description: Resume all events and return to the scheduled program. This default to false which removes only the top event. example: true + +nuheat_resume_program: + description: Resume the programmed schedule. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' From 8ef8dbc8688495d74b30dad990c9f84e8e2a24f6 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Sun, 24 Dec 2017 11:15:50 -0700 Subject: [PATCH 23/27] pleasin the hound --- homeassistant/components/climate/nuheat.py | 1 - tests/components/climate/test_nuheat.py | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index 226c92a6e65..a4d1cad68a4 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -95,7 +95,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): schema=RESUME_PROGRAM_SCHEMA) - class NuHeatThermostat(ClimateDevice): """Representation of a NuHeat Thermostat.""" diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 3e30e7caaf7..aedb925277e 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -141,15 +141,18 @@ class TestNuHeat(unittest.TestCase): def test_set_hold_mode(self): """Test set hold mode.""" self.thermostat.set_hold_mode("temperature") - self.assertEqual(self.thermostat._thermostat.schedule_mode, SCHEDULE_HOLD) + self.assertEqual( + self.thermostat._thermostat.schedule_mode, SCHEDULE_HOLD) self.assertTrue(self.thermostat._force_update) self.thermostat.set_hold_mode("temporary_temperature") - self.assertEqual(self.thermostat._thermostat.schedule_mode, SCHEDULE_TEMPORARY_HOLD) + self.assertEqual( + self.thermostat._thermostat.schedule_mode, SCHEDULE_TEMPORARY_HOLD) self.assertTrue(self.thermostat._force_update) self.thermostat.set_hold_mode("auto") - self.assertEqual(self.thermostat._thermostat.schedule_mode, SCHEDULE_RUN) + self.assertEqual( + self.thermostat._thermostat.schedule_mode, SCHEDULE_RUN) self.assertTrue(self.thermostat._force_update) def test_set_temperature(self): From f0244d7982b11e98a4f3d4f2fcd9270243187f65 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Tue, 26 Dec 2017 11:12:28 -0800 Subject: [PATCH 24/27] add a bit more test coverage --- homeassistant/components/nuheat.py | 4 +-- tests/components/climate/test_nuheat.py | 21 ++++++++++-- tests/components/test_nuheat.py | 44 +++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tests/components/test_nuheat.py diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py index 41db3e51842..fb14f119dbd 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat.py @@ -16,8 +16,6 @@ REQUIREMENTS = ["nuheat==0.3.0"] _LOGGER = logging.getLogger(__name__) -DATA_NUHEAT = "nuheat" - DOMAIN = "nuheat" CONFIG_SCHEMA = vol.Schema({ @@ -41,7 +39,7 @@ def setup(hass, config): api = nuheat.NuHeat(username, password) api.authenticate() - hass.data[DATA_NUHEAT] = (api, devices) + hass.data[DOMAIN] = (api, devices) discovery.load_platform(hass, "climate", DOMAIN, {}, config) return True diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index aedb925277e..b2ad57731ba 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -42,6 +42,7 @@ class TestNuHeat(unittest.TestCase): target_celsius=22, target_fahrenheit=72) + thermostat.get_data = Mock() thermostat.resume_schedule = Mock() api = Mock() @@ -132,6 +133,17 @@ class TestNuHeat(unittest.TestCase): self.assertEqual( self.thermostat.current_hold_mode, nuheat.MODE_TEMPORARY_HOLD) + self.thermostat._thermostat.schedule_mode = None + self.assertEqual( + self.thermostat.current_hold_mode, nuheat.MODE_AUTO) + + def test_operation_list(self): + """Test the operation list.""" + self.assertEqual( + self.thermostat.operation_list, + [STATE_HEAT, STATE_IDLE] + ) + def test_resume_program(self): """Test resume schedule.""" self.thermostat.resume_program() @@ -167,7 +179,7 @@ class TestNuHeat(unittest.TestCase): self.assertTrue(self.thermostat._force_update) @patch.object(nuheat.NuHeatThermostat, "_throttled_update") - def test_forced_update(self, throttled_update): + def test_update_without_throttle(self, throttled_update): """Test update without throttle.""" self.thermostat._force_update = True self.thermostat.update() @@ -175,9 +187,14 @@ class TestNuHeat(unittest.TestCase): self.assertFalse(self.thermostat._force_update) @patch.object(nuheat.NuHeatThermostat, "_throttled_update") - def test_throttled_update(self, throttled_update): + def test_update_with_throttle(self, throttled_update): """Test update with throttle.""" self.thermostat._force_update = False self.thermostat.update() throttled_update.assert_called_once_with() self.assertFalse(self.thermostat._force_update) + + def test_throttled_update(self): + """Test update with throttle.""" + self.thermostat._throttled_update() + self.thermostat._thermostat.get_data.assert_called_once_with() diff --git a/tests/components/test_nuheat.py b/tests/components/test_nuheat.py new file mode 100644 index 00000000000..6b091b8df35 --- /dev/null +++ b/tests/components/test_nuheat.py @@ -0,0 +1,44 @@ +"""NuHeat component tests.""" +import unittest + +from unittest.mock import patch +from tests.common import get_test_home_assistant, MockDependency + +from homeassistant.components import nuheat + +VALID_CONFIG = { + "nuheat": { + "username": "warm", + "password": "feet", + "devices": "thermostat123" + } +} + + +class TestNuHeat(unittest.TestCase): + """Test the NuHeat component.""" + + def setUp(self): + """Initialize the values for this test class.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): + """Teardown this test class. Stop hass.""" + self.hass.stop() + + @MockDependency("nuheat") + @patch("homeassistant.helpers.discovery.load_platform") + def test_setup(self, mocked_nuheat, mocked_load): + """Test setting up the NuHeat component.""" + nuheat.setup(self.hass, self.config) + + mocked_nuheat.NuHeat.assert_called_with("warm", "feet") + self.assertIn(nuheat.DOMAIN, self.hass.data) + self.assertEquals(2, len(self.hass.data[nuheat.DOMAIN])) + self.assertEquals(self.hass.data[nuheat.DOMAIN][0], "thermostat123") + self.assertEquals(self.hass.data[nuheat.DOMAIN][1], "thermostat123") + + mocked_load.assert_called_with( + self.hass, "climate", nuheat.DOMAIN, {}, self.config + ) From 63d9bd4a9cb034501d855f691eb167704536b8e1 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Wed, 27 Dec 2017 12:42:56 -0800 Subject: [PATCH 25/27] test resume program service --- homeassistant/components/climate/nuheat.py | 7 +-- tests/components/climate/test_nuheat.py | 56 +++++++++++++++++----- tests/components/test_nuheat.py | 4 +- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index a4d1cad68a4..67540a2347d 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( STATE_AUTO, STATE_HEAT, STATE_IDLE) -from homeassistant.components.nuheat import DATA_NUHEAT +from homeassistant.components.nuheat import DOMAIN as NUHEAT_DOMAIN from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, @@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return temperature_unit = hass.config.units.temperature_unit - api, serial_numbers = hass.data[DATA_NUHEAT] + api, serial_numbers = hass.data[NUHEAT_DOMAIN] thermostats = [ NuHeatThermostat(api, serial_number, temperature_unit) for serial_number in serial_numbers @@ -74,7 +74,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def resume_program_set_service(service): """Resume the program on the target thermostats.""" entity_id = service.data.get(ATTR_ENTITY_ID) - if entity_id: target_thermostats = [device for device in thermostats if device.entity_id in entity_id] @@ -94,6 +93,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): descriptions.get(SERVICE_RESUME_PROGRAM), schema=RESUME_PROGRAM_SCHEMA) + return True + class NuHeatThermostat(ClimateDevice): """Representation of a NuHeat Thermostat.""" diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index b2ad57731ba..6ec63646bec 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -1,6 +1,7 @@ """The test for the NuHeat thermostat module.""" import unittest from unittest.mock import Mock, patch +from tests.common import get_test_home_assistant from homeassistant.components.climate import ( SUPPORT_HOLD_MODE, @@ -21,7 +22,7 @@ class TestNuHeat(unittest.TestCase): # pylint: disable=protected-access, no-self-use - def setUp(self): + def setUp(self): # pylint: disable=invalid-name """Set up test variables.""" serial_number = "12345" temperature_unit = "F" @@ -45,31 +46,62 @@ class TestNuHeat(unittest.TestCase): thermostat.get_data = Mock() thermostat.resume_schedule = Mock() - api = Mock() - api.get_thermostat.return_value = thermostat + self.api = Mock() + self.api.get_thermostat.return_value = thermostat + self.hass = get_test_home_assistant() self.thermostat = nuheat.NuHeatThermostat( - api, serial_number, temperature_unit) + self.api, serial_number, temperature_unit) + + def tearDown(self): # pylint: disable=invalid-name + """Stop hass.""" + self.hass.stop() @patch("homeassistant.components.climate.nuheat.NuHeatThermostat") def test_setup_platform(self, mocked_thermostat): """Test setup_platform.""" - api = Mock() - data = {"nuheat": (api, ["12345"])} + mocked_thermostat.return_value = self.thermostat + thermostat = mocked_thermostat(self.api, "12345", "F") + thermostats = [thermostat] - hass = Mock() - hass.config.units.temperature_unit.return_value = "F" - hass.data = Mock() - hass.data.__getitem__ = Mock(side_effect=data.__getitem__) + self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) config = {} add_devices = Mock() discovery_info = {} - nuheat.setup_platform(hass, config, add_devices, discovery_info) - thermostats = [mocked_thermostat(api, "12345", "F")] + nuheat.setup_platform(self.hass, config, add_devices, discovery_info) add_devices.assert_called_once_with(thermostats, True) + @patch("homeassistant.components.climate.nuheat.NuHeatThermostat") + def test_resume_program_service(self, mocked_thermostat): + """Test resume program service.""" + mocked_thermostat.return_value = self.thermostat + thermostat = mocked_thermostat(self.api, "12345", "F") + thermostat.resume_program = Mock() + thermostat.schedule_update_ha_state = Mock() + thermostat.entity_id = "climate.master_bathroom" + + self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + nuheat.setup_platform(self.hass, {}, Mock(), {}) + + # Explicit entity + self.hass.services.call(nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, + {"entity_id": "climate.master_bathroom"}, True) + + thermostat.resume_program.assert_called_with() + thermostat.schedule_update_ha_state.assert_called_with(True) + + thermostat.resume_program.reset_mock() + thermostat.schedule_update_ha_state.reset_mock() + + # All entities + self.hass.services.call( + nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True) + + thermostat.resume_program.assert_called_with() + thermostat.schedule_update_ha_state.assert_called_with(True) + def test_name(self): """Test name property.""" self.assertEqual(self.thermostat.name, "Master bathroom") diff --git a/tests/components/test_nuheat.py b/tests/components/test_nuheat.py index 6b091b8df35..6b486c6afcc 100644 --- a/tests/components/test_nuheat.py +++ b/tests/components/test_nuheat.py @@ -18,12 +18,12 @@ VALID_CONFIG = { class TestNuHeat(unittest.TestCase): """Test the NuHeat component.""" - def setUp(self): + def setUp(self): # pylint: disable=invalid-name """Initialize the values for this test class.""" self.hass = get_test_home_assistant() self.config = VALID_CONFIG - def tearDown(self): + def tearDown(self): # pylint: disable=invalid-name """Teardown this test class. Stop hass.""" self.hass.stop() From 29c26e0015e2f6172366cc1b0f0a7a001f269f57 Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Wed, 27 Dec 2017 13:06:04 -0800 Subject: [PATCH 26/27] fix bad nuheat component test --- tests/components/test_nuheat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/test_nuheat.py b/tests/components/test_nuheat.py index 6b486c6afcc..91a8b326bf9 100644 --- a/tests/components/test_nuheat.py +++ b/tests/components/test_nuheat.py @@ -36,7 +36,9 @@ class TestNuHeat(unittest.TestCase): mocked_nuheat.NuHeat.assert_called_with("warm", "feet") self.assertIn(nuheat.DOMAIN, self.hass.data) self.assertEquals(2, len(self.hass.data[nuheat.DOMAIN])) - self.assertEquals(self.hass.data[nuheat.DOMAIN][0], "thermostat123") + self.assertIsInstance( + self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat()) + ) self.assertEquals(self.hass.data[nuheat.DOMAIN][1], "thermostat123") mocked_load.assert_called_with( From 00352d41a7bc4f3e9c1d7eb275f7a62994827a2d Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Wed, 27 Dec 2017 18:20:12 -0800 Subject: [PATCH 27/27] remove return value as requested --- homeassistant/components/climate/nuheat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index 67540a2347d..a62a684299d 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -93,8 +93,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): descriptions.get(SERVICE_RESUME_PROGRAM), schema=RESUME_PROGRAM_SCHEMA) - return True - class NuHeatThermostat(ClimateDevice): """Representation of a NuHeat Thermostat."""