diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index b021ec86c35ca5d4cd0e9a012841e740e5d9157d..89f00da3318e7a44b99a1aa9eb758d39ac487783 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -14,7 +14,8 @@ import homeassistant.util as util from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import convert from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, TEMP_CELCIUS) + ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, + TEMP_CELCIUS) DOMAIN = "thermostat" DEPENDENCIES = [] @@ -125,7 +126,7 @@ class ThermostatDevice(Entity): @property def state(self): """ Returns the current state. """ - return self.target_temperature + return self.target_temperature or STATE_UNKNOWN @property def device_state_attributes(self): diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index b783b210606b6ebf950ee1a2d17403f065c93930..681b016b108ffd017aa5396ab444f2e0985e433d 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -1,163 +1,155 @@ """ homeassistant.components.thermostat.heat_control ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Adds support for a thermostat. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.heat_control.html """ import logging -import datetime -import homeassistant.components as core import homeassistant.util as util -from homeassistant.components.thermostat import ThermostatDevice +from homeassistant.components import switch +from homeassistant.components.thermostat import (ThermostatDevice, STATE_IDLE, + STATE_HEAT) from homeassistant.helpers.event import track_state_change -from homeassistant.const import TEMP_CELCIUS, STATE_ON, STATE_OFF +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELCIUS, TEMP_FAHRENHEIT) + +DEPENDENCIES = ['switch', 'sensor'] TOL_TEMP = 0.3 +CONF_NAME = 'name' +DEFAULT_NAME = 'Heat Control' +CONF_HEATER = 'heater' +CONF_SENSOR = 'target_sensor' + +_LOGGER = logging.getLogger(__name__) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the heat control thermostat. """ - logger = logging.getLogger(__name__) + name = config.get(CONF_NAME, DEFAULT_NAME) + heater_entity_id = config.get(CONF_HEATER) + sensor_entity_id = config.get(CONF_SENSOR) - add_devices([HeatControl(hass, config, logger)]) + if None in (heater_entity_id, sensor_entity_id): + _LOGGER.error('Missing required key %s or %s', CONF_HEATER, + CONF_SENSOR) + return False + + add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id)]) # pylint: disable=too-many-instance-attributes class HeatControl(ThermostatDevice): """ Represents a HeatControl device. """ - def __init__(self, hass, config, logger): - - self.logger = logger + def __init__(self, hass, name, heater_entity_id, sensor_entity_id): self.hass = hass - self.heater_entity_id = config.get("heater") - - self.name_device = config.get("name") - self.target_sensor_entity_id = config.get("target_sensor") - - self.time_temp = [] - if config.get("time_temp"): - for time_temp in list(config.get("time_temp").split(",")): - time, temp = time_temp.split(':') - time_start, time_end = time.split('-') - start_time = datetime.datetime.time( - datetime.datetime.strptime(time_start, '%H%M')) - end_time = datetime.datetime.time( - datetime.datetime.strptime(time_end, '%H%M')) - self.time_temp.append((start_time, end_time, float(temp))) - - self._min_temp = util.convert(config.get("min_temp"), float, 0) - self._max_temp = util.convert(config.get("max_temp"), float, 100) - - self._manual_sat_temp = None - self._away = False - self._heater_manual_changed = True - - track_state_change(hass, self.heater_entity_id, - self._heater_turned_on, - STATE_OFF, STATE_ON) - track_state_change(hass, self.heater_entity_id, - self._heater_turned_off, - STATE_ON, STATE_OFF) + self._name = name + self.heater_entity_id = heater_entity_id + + self._active = False + self._cur_temp = None + self._target_temp = None + self._unit = None + + track_state_change(hass, sensor_entity_id, self._sensor_changed) + + sensor_state = hass.states.get(sensor_entity_id) + if sensor_state: + self._update_temp(sensor_state) + + @property + def should_poll(self): + return False @property def name(self): """ Returns the name. """ - return self.name_device + return self._name @property def unit_of_measurement(self): """ Returns the unit of measurement. """ - return TEMP_CELCIUS + return self._unit @property def current_temperature(self): - """ Returns the current temperature. """ - target_sensor = self.hass.states.get(self.target_sensor_entity_id) - if target_sensor: - return float(target_sensor.state) - else: - return None + return self._cur_temp + + @property + def operation(self): + """ Returns current operation ie. heat, cool, idle """ + return STATE_HEAT if self._active and self._is_heating else STATE_IDLE @property def target_temperature(self): """ Returns the temperature we try to reach. """ - if self._manual_sat_temp: - return self._manual_sat_temp - elif self._away: - return self.min_temp - else: - now = datetime.datetime.time(datetime.datetime.now()) - for (start_time, end_time, temp) in self.time_temp: - if start_time < now and end_time > now: - return temp - return self.min_temp + return self._target_temp def set_temperature(self, temperature): """ Set new target temperature. """ - if temperature is None: - self._manual_sat_temp = None - else: - self._manual_sat_temp = float(temperature) - - def update(self): - """ Update current thermostat. """ - heater = self.hass.states.get(self.heater_entity_id) - if heater is None: - self.logger.error("No heater available") + self._target_temp = temperature + self._control_heating() + self.update_ha_state() + + def _sensor_changed(self, entity_id, old_state, new_state): + """ Called when temperature changes. """ + if new_state is None: return - current_temperature = self.current_temperature - if current_temperature is None: - self.logger.error("No temperature available") + self._update_temp(new_state) + self._control_heating() + self.update_ha_state() + + def _update_temp(self, state): + """ Update thermostat with latest state from sensor. """ + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if unit not in (TEMP_CELCIUS, TEMP_FAHRENHEIT): + self._cur_temp = None + self._unit = None + _LOGGER.error('Sensor has unsupported unit: %s (allowed: %s, %s)', + unit, TEMP_CELCIUS, TEMP_FAHRENHEIT) return - if (current_temperature - self.target_temperature) > \ - TOL_TEMP and heater.state is STATE_ON: - self._heater_manual_changed = False - core.turn_off(self.hass, self.heater_entity_id) - elif (self.target_temperature - self.current_temperature) > TOL_TEMP \ - and heater.state is STATE_OFF: - self._heater_manual_changed = False - core.turn_on(self.hass, self.heater_entity_id) - - def _heater_turned_on(self, entity_id, old_state, new_state): - """ Heater is turned on. """ - if not self._heater_manual_changed: - pass - else: - self.set_temperature(self.max_temp) - - self._heater_manual_changed = True - - def _heater_turned_off(self, entity_id, old_state, new_state): - """ Heater is turned off. """ - if self._heater_manual_changed: - self.set_temperature(None) + temp = util.convert(state.state, float) - @property - def is_away_mode_on(self): - """ Returns if away mode is on. """ - return self._away + if temp is None: + self._cur_temp = None + self._unit = None + _LOGGER.error('Unable to parse sensor temperature: %s', + state.state) + return - def turn_away_mode_on(self): - """ Turns away mode on. """ - self._away = True + self._cur_temp = temp + self._unit = unit - def turn_away_mode_off(self): - """ Turns away mode off. """ - self._away = False + def _control_heating(self): + """ Check if we need to turn heating on or off. """ + if not self._active and None not in (self._cur_temp, + self._target_temp): + self._active = True + _LOGGER.info('Obtained current and target temperature. ' + 'Heat control active.') - @property - def min_temp(self): - """ Return minimum temperature. """ - return self._min_temp + if not self._active: + return + + too_cold = self._target_temp - self._cur_temp > TOL_TEMP + is_heating = self._is_heating + + if too_cold and not is_heating: + _LOGGER.info('Turning on heater %s', self.heater_entity_id) + switch.turn_on(self.hass, self.heater_entity_id) + elif not too_cold and is_heating: + _LOGGER.info('Turning off heater %s', self.heater_entity_id) + switch.turn_off(self.hass, self.heater_entity_id) @property - def max_temp(self): - """ Return maximum temperature. """ - return self._max_temp + def _is_heating(self): + """ If the heater is currently heating. """ + return switch.is_on(self.hass, self.heater_entity_id) diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index eaf1f78d927217b8f9e2fbe1b5e9ce0520dde959..4a867ab2f2e863d36574fdc0b72c096375ad8ad4 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -11,7 +11,7 @@ import homeassistant.util.temperature as temp_util def convert(temperature, unit, to_unit): """ Converts temperature to correct unit. """ - if unit == to_unit: + if unit == to_unit or unit is None or to_unit is None: return temperature elif unit == TEMP_CELCIUS: return temp_util.celcius_to_fahrenheit(temperature) diff --git a/tests/components/thermostat/__init__.py b/tests/components/thermostat/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/components/thermostat/test_heat_control.py b/tests/components/thermostat/test_heat_control.py new file mode 100644 index 0000000000000000000000000000000000000000..f0b487ae86ca709146e0bfe6f85506f476e9e0a3 --- /dev/null +++ b/tests/components/thermostat/test_heat_control.py @@ -0,0 +1,115 @@ +""" +tests.components.thermostat.test_heat_control +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests heat control thermostat. +""" +import unittest + +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_OFF, + TEMP_CELCIUS, +) +import homeassistant.core as ha +from homeassistant.components import switch, thermostat + + +entity = 'thermostat.test' +ent_sensor = 'sensor.test' +ent_switch = 'switch.test' + + +class TestThermostatHeatControl(unittest.TestCase): + """ Test the Heat Control thermostat. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.hass.config.temperature_unit = TEMP_CELCIUS + thermostat.setup(self.hass, {'thermostat': { + 'platform': 'heat_control', + 'name': 'test', + 'heater': ent_switch, + 'target_sensor': ent_sensor + }}) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_setup_defaults_to_unknown(self): + self.assertEqual('unknown', self.hass.states.get(entity).state) + + def test_set_target_temp(self): + thermostat.set_temperature(self.hass, 30) + self.hass.pool.block_till_done() + self.assertEqual('30.0', self.hass.states.get(entity).state) + + def test_set_target_temp_turns_on_heater(self): + self._setup_switch(False) + self._setup_sensor(25) + self.hass.pool.block_till_done() + thermostat.set_temperature(self.hass, 30) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual(ent_switch, call.data['entity_id']) + + def test_set_target_temp_turns_off_heater(self): + self._setup_switch(True) + self._setup_sensor(30) + self.hass.pool.block_till_done() + thermostat.set_temperature(self.hass, 25) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ent_switch, call.data['entity_id']) + + def test_set_temp_change_turns_on_heater(self): + self._setup_switch(False) + thermostat.set_temperature(self.hass, 30) + self.hass.pool.block_till_done() + self._setup_sensor(25) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual(ent_switch, call.data['entity_id']) + + def test_temp_change_turns_off_heater(self): + self._setup_switch(True) + thermostat.set_temperature(self.hass, 25) + self.hass.pool.block_till_done() + self._setup_sensor(30) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ent_switch, call.data['entity_id']) + + def _setup_sensor(self, temp, unit=TEMP_CELCIUS): + """ Setup the test sensor. """ + self.hass.states.set(ent_sensor, temp, { + ATTR_UNIT_OF_MEASUREMENT: unit + }) + + def _setup_switch(self, is_on): + """ Setup the test switch. """ + self.hass.states.set(ent_switch, STATE_ON if is_on else STATE_OFF) + self.calls = [] + + def log_call(call): + """ Log service calls. """ + self.calls.append(call) + + self.hass.services.register('switch', SERVICE_TURN_ON, log_call) + self.hass.services.register('switch', SERVICE_TURN_OFF, log_call)