Clean up the heat control thermostat

This commit is contained in:
Paulus Schoutsen 2015-10-22 22:04:37 -07:00
parent 91a1fb0240
commit 3d972abdab
5 changed files with 216 additions and 163 deletions

View File

@ -14,7 +14,8 @@ import homeassistant.util as util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert from homeassistant.helpers.temperature import convert
from homeassistant.const import ( 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" DOMAIN = "thermostat"
DEPENDENCIES = [] DEPENDENCIES = []
@ -125,7 +126,7 @@ class ThermostatDevice(Entity):
@property @property
def state(self): def state(self):
""" Returns the current state. """ """ Returns the current state. """
return self.target_temperature return self.target_temperature or STATE_UNKNOWN
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View File

@ -1,216 +1,153 @@
""" """
homeassistant.components.thermostat.heat_control homeassistant.components.thermostat.heat_control
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adds support for a thermostat.
Specify a start time, end time and a target temperature. Thermostat based on a sensor and a switch connected to a heater.
If the the current temperature is lower than the target temperature,
and the time is between start time and end time, the heater will
be turned on. Opposite if the the temperature is higher than the
target temperature the heater will be turned off.
If away mode is activated the target temperature is sat to a min
temperature (min_temp in config). The min temperature is also used
as target temperature when no other temperature is specified.
If the heater is manually turned on, the target temperature will
be sat to 100*C. Meaning the thermostat probably will never turn
off the heater.
If the heater is manually turned off, the target temperature will
be sat according to normal rules. (Based on target temperature
for given time intervals and the min temperature.)
A target temperature sat with the set_temperature function will
override all other rules for the target temperature.
Config:
[thermostat]
platform=heat_control
name = Name of thermostat
heater = entity_id for heater switch,
must be a toggle device
target_sensor = entity_id for temperature sensor,
target_sensor.state must be temperature
time_temp = start_time-end_time:target_temp,
min_temp = minimum temperature, used when away mode is
active or no other temperature specified.
Example:
[thermostat]
platform=heat_control
name = Stue
heater = switch.Ovn_stue
target_sensor = tellstick_sensor.Stue_temperature
time_temp = 0700-0745:17,1500-1850:20
min_temp = 10
For the example the heater will turn on at 0700 if the temperature
is lower than 17*C away mode is false. Between 0700 and 0745 the
target temperature will be 17*C. Between 0745 and 1500 no temperature
is specified. so the min_temp of 10*C will be used. From 1500 to 1850
the target temperature is 20*, but if away mode is true the target
temperature will be sat to 10*C
""" """
import logging import logging
import datetime
import homeassistant.components as core
import homeassistant.util as util 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.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 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 # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the heat control thermostat. """ """ 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 # pylint: disable=too-many-instance-attributes
class HeatControl(ThermostatDevice): class HeatControl(ThermostatDevice):
""" Represents a HeatControl device. """ """ Represents a HeatControl device. """
def __init__(self, hass, config, logger): def __init__(self, hass, name, heater_entity_id, sensor_entity_id):
self.logger = logger
self.hass = hass self.hass = hass
self.heater_entity_id = config.get("heater") self._name = name
self.heater_entity_id = heater_entity_id
self.name_device = config.get("name") self._active = False
self.target_sensor_entity_id = config.get("target_sensor") self._cur_temp = None
self._target_temp = None
self._unit = None
self.time_temp = [] track_state_change(hass, sensor_entity_id, self._sensor_changed)
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) sensor_state = hass.states.get(sensor_entity_id)
self._max_temp = util.convert(config.get("max_temp"), float, 100) if sensor_state:
self._update_temp(sensor_state)
self._manual_sat_temp = None @property
self._away = False def should_poll(self):
self._heater_manual_changed = True return False
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)
@property @property
def name(self): def name(self):
""" Returns the name. """ """ Returns the name. """
return self.name_device return self._name
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
""" Returns the unit of measurement. """ """ Returns the unit of measurement. """
return TEMP_CELCIUS return self._unit
@property @property
def current_temperature(self): def current_temperature(self):
""" Returns the current temperature. """ return self._cur_temp
target_sensor = self.hass.states.get(self.target_sensor_entity_id)
if target_sensor: @property
return float(target_sensor.state) def operation(self):
else: """ Returns current operation ie. heat, cool, idle """
return None return STATE_HEAT if self._active and self._is_heating else STATE_IDLE
@property @property
def target_temperature(self): def target_temperature(self):
""" Returns the temperature we try to reach. """ """ Returns the temperature we try to reach. """
if self._manual_sat_temp: return self._target_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
def set_temperature(self, temperature): def set_temperature(self, temperature):
""" Set new target temperature. """ """ Set new target temperature. """
if temperature is None: self._target_temp = temperature
self._manual_sat_temp = None self._control_heating()
else: self.update_ha_state()
self._manual_sat_temp = float(temperature)
def update(self): def _sensor_changed(self, entity_id, old_state, new_state):
""" Update current thermostat. """ """ Called when temperature changes. """
heater = self.hass.states.get(self.heater_entity_id) if new_state is None:
if heater is None:
self.logger.error("No heater available")
return return
current_temperature = self.current_temperature self._update_temp(new_state)
if current_temperature is None: self._control_heating()
self.logger.error("No temperature available") 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 return
if (current_temperature - self.target_temperature) > \ temp = util.convert(state.state, float)
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): if temp is None:
""" Heater is turned on. """ self._cur_temp = None
if not self._heater_manual_changed: self._unit = None
pass _LOGGER.error('Unable to parse sensor temperature: %s',
else: state.state)
self.set_temperature(self.max_temp) return
self._heater_manual_changed = True self._cur_temp = temp
self._unit = unit
def _heater_turned_off(self, entity_id, old_state, new_state): def _control_heating(self):
""" Heater is turned off. """ """ Check if we need to turn heating on or off. """
if self._heater_manual_changed: if not self._active and None not in (self._cur_temp,
self.set_temperature(None) self._target_temp):
self._active = True
_LOGGER.info('Obtained current and target temperature. '
'Heat control active.')
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 @property
def is_away_mode_on(self): def _is_heating(self):
""" return switch.is_on(self.hass, self.heater_entity_id)
Returns if away mode is on.
"""
return self._away
def turn_away_mode_on(self):
""" Turns away mode on. """
self._away = True
def turn_away_mode_off(self):
""" Turns away mode off. """
self._away = False
@property
def min_temp(self):
""" Return minimum temperature. """
return self._min_temp
@property
def max_temp(self):
""" Return maxmum temperature. """
return self._max_temp

View File

@ -11,7 +11,7 @@ import homeassistant.util.temperature as temp_util
def convert(temperature, unit, to_unit): def convert(temperature, unit, to_unit):
""" Converts temperature to correct 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 return temperature
elif unit == TEMP_CELCIUS: elif unit == TEMP_CELCIUS:
return temp_util.celcius_to_fahrenheit(temperature) return temp_util.celcius_to_fahrenheit(temperature)

View File

View File

@ -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)