Fix issues with generic thermostat (#11805)

* Fixes for #11757 #11798 #11763

* Adjustments based on feedback
This commit is contained in:
Marius 2018-01-22 13:12:03 +02:00 committed by Martin Hjelmare
parent 0f26ebe954
commit c8d26d99f0
2 changed files with 49 additions and 28 deletions

View file

@ -17,7 +17,8 @@ from homeassistant.components.climate import (
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_UNKNOWN)
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval) async_track_state_change, async_track_time_interval)
@ -30,7 +31,6 @@ DEPENDENCIES = ['switch', 'sensor']
DEFAULT_TOLERANCE = 0.3 DEFAULT_TOLERANCE = 0.3
DEFAULT_NAME = 'Generic Thermostat' DEFAULT_NAME = 'Generic Thermostat'
DEFAULT_AWAY_TEMP = 16
CONF_HEATER = 'heater' CONF_HEATER = 'heater'
CONF_SENSOR = 'target_sensor' CONF_SENSOR = 'target_sensor'
@ -44,7 +44,7 @@ CONF_HOT_TOLERANCE = 'hot_tolerance'
CONF_KEEP_ALIVE = 'keep_alive' CONF_KEEP_ALIVE = 'keep_alive'
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode' CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
CONF_AWAY_TEMP = 'away_temp' CONF_AWAY_TEMP = 'away_temp'
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_OPERATION_MODE) SUPPORT_OPERATION_MODE)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -64,8 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
cv.time_period, cv.positive_timedelta), cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_INITIAL_OPERATION_MODE): vol.Optional(CONF_INITIAL_OPERATION_MODE):
vol.In([STATE_AUTO, STATE_OFF]), vol.In([STATE_AUTO, STATE_OFF]),
vol.Optional(CONF_AWAY_TEMP, vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float)
default=DEFAULT_AWAY_TEMP): vol.Coerce(float)
}) })
@ -119,6 +118,7 @@ class GenericThermostat(ClimateDevice):
self._operation_list = [STATE_HEAT, STATE_OFF] self._operation_list = [STATE_HEAT, STATE_OFF]
if initial_operation_mode == STATE_OFF: if initial_operation_mode == STATE_OFF:
self._enabled = False self._enabled = False
self._current_operation = STATE_OFF
else: else:
self._enabled = True self._enabled = True
self._active = False self._active = False
@ -127,6 +127,9 @@ class GenericThermostat(ClimateDevice):
self._max_temp = max_temp self._max_temp = max_temp
self._target_temp = target_temp self._target_temp = target_temp
self._unit = hass.config.units.temperature_unit self._unit = hass.config.units.temperature_unit
self._support_flags = SUPPORT_FLAGS
if away_temp is not None:
self._support_flags = SUPPORT_FLAGS | SUPPORT_AWAY_MODE
self._away_temp = away_temp self._away_temp = away_temp
self._is_away = False self._is_away = False
@ -139,6 +142,10 @@ class GenericThermostat(ClimateDevice):
async_track_time_interval( async_track_time_interval(
hass, self._async_keep_alive, self._keep_alive) hass, self._async_keep_alive, self._keep_alive)
sensor_state = hass.states.get(sensor_entity_id)
if sensor_state and sensor_state.state != STATE_UNKNOWN:
self._async_update_temp(sensor_state)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Run when entity about to be added.""" """Run when entity about to be added."""
@ -154,19 +161,29 @@ class GenericThermostat(ClimateDevice):
self._target_temp = self.max_temp self._target_temp = self.max_temp
else: else:
self._target_temp = self.min_temp self._target_temp = self.min_temp
_LOGGER.warning('Undefined target temperature, \ _LOGGER.warning("Undefined target temperature,"
falling back to %s', self._target_temp) "falling back to %s", self._target_temp)
else: else:
self._target_temp = float( self._target_temp = float(
old_state.attributes[ATTR_TEMPERATURE]) old_state.attributes[ATTR_TEMPERATURE])
self._is_away = True if str( if old_state.attributes[ATTR_AWAY_MODE] is not None:
old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON else False self._is_away = str(
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF: old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON
self._current_operation = STATE_OFF if (self._initial_operation_mode is None and
self._enabled = False old_state.attributes[ATTR_OPERATION_MODE] is not None):
if self._initial_operation_mode is None: self._current_operation = \
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF: old_state.attributes[ATTR_OPERATION_MODE]
self._enabled = False if self._current_operation != STATE_OFF:
self._enabled = True
else:
# No previous state, try and restore defaults
if self._target_temp is None:
if self.ac_mode:
self._target_temp = self.max_temp
else:
self._target_temp = self.min_temp
_LOGGER.warning("No previously saved temperature, setting to %s",
self._target_temp)
@property @property
def state(self): def state(self):
@ -230,7 +247,7 @@ class GenericThermostat(ClimateDevice):
if self._is_device_active: if self._is_device_active:
self._heater_turn_off() self._heater_turn_off()
else: else:
_LOGGER.error('Unrecognized operation mode: %s', operation_mode) _LOGGER.error("Unrecognized operation mode: %s", operation_mode)
return return
# Ensure we updae the current operation after changing the mode # Ensure we updae the current operation after changing the mode
self.schedule_update_ha_state() self.schedule_update_ha_state()
@ -299,7 +316,7 @@ class GenericThermostat(ClimateDevice):
self._cur_temp = self.hass.config.units.temperature( self._cur_temp = self.hass.config.units.temperature(
float(state.state), unit) float(state.state), unit)
except ValueError as ex: except ValueError as ex:
_LOGGER.error('Unable to update from sensor: %s', ex) _LOGGER.error("Unable to update from sensor: %s", ex)
@callback @callback
def _async_control_heating(self): def _async_control_heating(self):
@ -307,8 +324,9 @@ class GenericThermostat(ClimateDevice):
if not self._active and None not in (self._cur_temp, if not self._active and None not in (self._cur_temp,
self._target_temp): self._target_temp):
self._active = True self._active = True
_LOGGER.info('Obtained current and target temperature. ' _LOGGER.info("Obtained current and target temperature. "
'Generic thermostat active.') "Generic thermostat active. %s, %s",
self._cur_temp, self._target_temp)
if not self._active: if not self._active:
return return
@ -333,13 +351,13 @@ class GenericThermostat(ClimateDevice):
too_cold = self._target_temp - self._cur_temp >= \ too_cold = self._target_temp - self._cur_temp >= \
self._cold_tolerance self._cold_tolerance
if too_cold: if too_cold:
_LOGGER.info('Turning off AC %s', self.heater_entity_id) _LOGGER.info("Turning off AC %s", self.heater_entity_id)
self._heater_turn_off() self._heater_turn_off()
else: else:
too_hot = self._cur_temp - self._target_temp >= \ too_hot = self._cur_temp - self._target_temp >= \
self._hot_tolerance self._hot_tolerance
if too_hot: if too_hot:
_LOGGER.info('Turning on AC %s', self.heater_entity_id) _LOGGER.info("Turning on AC %s", self.heater_entity_id)
self._heater_turn_on() self._heater_turn_on()
else: else:
is_heating = self._is_device_active is_heating = self._is_device_active
@ -347,14 +365,14 @@ class GenericThermostat(ClimateDevice):
too_hot = self._cur_temp - self._target_temp >= \ too_hot = self._cur_temp - self._target_temp >= \
self._hot_tolerance self._hot_tolerance
if too_hot: if too_hot:
_LOGGER.info('Turning off heater %s', _LOGGER.info("Turning off heater %s",
self.heater_entity_id) self.heater_entity_id)
self._heater_turn_off() self._heater_turn_off()
else: else:
too_cold = self._target_temp - self._cur_temp >= \ too_cold = self._target_temp - self._cur_temp >= \
self._cold_tolerance self._cold_tolerance
if too_cold: if too_cold:
_LOGGER.info('Turning on heater %s', self.heater_entity_id) _LOGGER.info("Turning on heater %s", self.heater_entity_id)
self._heater_turn_on() self._heater_turn_on()
@property @property
@ -365,7 +383,7 @@ class GenericThermostat(ClimateDevice):
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS return self._support_flags
@callback @callback
def _heater_turn_on(self): def _heater_turn_on(self):

View file

@ -160,7 +160,8 @@ class TestClimateGenericThermostat(unittest.TestCase):
'cold_tolerance': 2, 'cold_tolerance': 2,
'hot_tolerance': 4, 'hot_tolerance': 4,
'heater': ENT_SWITCH, 'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR 'target_sensor': ENT_SENSOR,
'away_temp': 16
}}) }})
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
@ -176,7 +177,7 @@ class TestClimateGenericThermostat(unittest.TestCase):
state = self.hass.states.get(ENTITY) state = self.hass.states.get(ENTITY)
self.assertEqual(7, state.attributes.get('min_temp')) self.assertEqual(7, state.attributes.get('min_temp'))
self.assertEqual(35, state.attributes.get('max_temp')) self.assertEqual(35, state.attributes.get('max_temp'))
self.assertEqual(None, state.attributes.get('temperature')) self.assertEqual(7, state.attributes.get('temperature'))
def test_get_operation_modes(self): def test_get_operation_modes(self):
"""Test that the operation list returns the correct modes.""" """Test that the operation list returns the correct modes."""
@ -266,7 +267,7 @@ class TestClimateGenericThermostat(unittest.TestCase):
self.hass.block_till_done() self.hass.block_till_done()
climate.set_temperature(self.hass, 25) climate.set_temperature(self.hass, 25)
self.hass.block_till_done() self.hass.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(2, len(self.calls))
call = self.calls[0] call = self.calls[0]
self.assertEqual('homeassistant', call.domain) self.assertEqual('homeassistant', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(SERVICE_TURN_OFF, call.service)
@ -414,7 +415,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase):
self.hass.block_till_done() self.hass.block_till_done()
climate.set_temperature(self.hass, 30) climate.set_temperature(self.hass, 30)
self.hass.block_till_done() self.hass.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(2, len(self.calls))
call = self.calls[0] call = self.calls[0]
self.assertEqual('homeassistant', call.domain) self.assertEqual('homeassistant', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(SERVICE_TURN_OFF, call.service)
@ -750,6 +751,7 @@ class TestClimateGenericThermostatACKeepAlive(unittest.TestCase):
'cold_tolerance': 0.3, 'cold_tolerance': 0.3,
'hot_tolerance': 0.3, 'hot_tolerance': 0.3,
'heater': ENT_SWITCH, 'heater': ENT_SWITCH,
'target_temp': 25,
'target_sensor': ENT_SENSOR, 'target_sensor': ENT_SENSOR,
'ac_mode': True, 'ac_mode': True,
'keep_alive': datetime.timedelta(minutes=10) 'keep_alive': datetime.timedelta(minutes=10)
@ -841,6 +843,7 @@ class TestClimateGenericThermostatKeepAlive(unittest.TestCase):
'name': 'test', 'name': 'test',
'cold_tolerance': 0.3, 'cold_tolerance': 0.3,
'hot_tolerance': 0.3, 'hot_tolerance': 0.3,
'target_temp': 25,
'heater': ENT_SWITCH, 'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR, 'target_sensor': ENT_SENSOR,
'keep_alive': datetime.timedelta(minutes=10) 'keep_alive': datetime.timedelta(minutes=10)