Merge branch 'clean-up-heat-control' into dev
Conflicts: homeassistant/components/thermostat/heat_control.py
This commit is contained in:
commit
b0c0659acc
5 changed files with 216 additions and 108 deletions
|
@ -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):
|
||||||
|
|
|
@ -1,163 +1,155 @@
|
||||||
"""
|
"""
|
||||||
homeassistant.components.thermostat.heat_control
|
homeassistant.components.thermostat.heat_control
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Adds support for a thermostat.
|
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/thermostat.heat_control.html
|
https://home-assistant.io/components/thermostat.heat_control.html
|
||||||
"""
|
"""
|
||||||
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):
|
||||||
""" Returns if away mode is on. """
|
""" If the heater is currently heating. """
|
||||||
return self._away
|
return switch.is_on(self.hass, self.heater_entity_id)
|
||||||
|
|
||||||
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 maximum temperature. """
|
|
||||||
return self._max_temp
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
0
tests/components/thermostat/__init__.py
Normal file
0
tests/components/thermostat/__init__.py
Normal file
115
tests/components/thermostat/test_heat_control.py
Normal file
115
tests/components/thermostat/test_heat_control.py
Normal 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)
|
Loading…
Add table
Reference in a new issue