Add ecobee fan mode (#12732)

* add ability to set fan on

* add tests and change "not on" status to "auto"

* hound fix

* more hounds

* I don't understand new lines

* fix linting errors

* more linting fixes

* change method signature

* lint fixes

* hopefully last lint fix

* correct temp ranges according to ecobee API docs

* update dependency to latest version

* update tests with values from new temp logic

* fix linting issue

* more linting fixes

* add SUPPORT_FAN_MODE to capabilities

* add fan_list to attributes.
restore current fan state to OFF if fan is not running.
change target high/low temps from null to target temp when not in auto mode.
change target temp from null to high/low temp when in auto mode
change mode attribute to climate_mode for consistency with other lists.

* remove unused import

* simplify logic

* lint fixes

* revert change for target temps
This commit is contained in:
uchagani 2018-03-18 12:02:07 -04:00 committed by Paulus Schoutsen
parent 022d8fb816
commit 1dcc51cbdf
4 changed files with 88 additions and 44 deletions

View file

@ -14,10 +14,10 @@ from homeassistant.components.climate import (
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_LOW)
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE,
SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
_CONFIGURING = {}
@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW)
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -122,6 +122,7 @@ class Thermostat(ClimateDevice):
self._climate_list = self.climate_list
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
'heat', 'off']
self._fan_list = ['auto', 'on']
self.update_without_throttle = False
def update(self):
@ -180,24 +181,29 @@ class Thermostat(ClimateDevice):
return self.thermostat['runtime']['desiredCool'] / 10.0
return None
@property
def desired_fan_mode(self):
"""Return the desired fan mode of operation."""
return self.thermostat['runtime']['desiredFanMode']
@property
def fan(self):
"""Return the current fan state."""
"""Return the current fan status."""
if 'fan' in self.thermostat['equipmentStatus']:
return STATE_ON
return STATE_OFF
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self.thermostat['runtime']['desiredFanMode']
@property
def current_hold_mode(self):
"""Return current hold mode."""
mode = self._current_hold_mode
return None if mode == AWAY_MODE else mode
@property
def fan_list(self):
"""Return the available fan modes."""
return self._fan_list
@property
def _current_hold_mode(self):
events = self.thermostat['events']
@ -206,7 +212,7 @@ class Thermostat(ClimateDevice):
if event['type'] == 'hold':
if event['holdClimateRef'] == 'away':
if int(event['endDate'][0:4]) - \
int(event['startDate'][0:4]) <= 1:
int(event['startDate'][0:4]) <= 1:
# A temporary hold from away climate is a hold
return 'away'
# A permanent hold from away climate
@ -228,7 +234,7 @@ class Thermostat(ClimateDevice):
def current_operation(self):
"""Return current operation."""
if self.operation_mode == 'auxHeatOnly' or \
self.operation_mode == 'heatPump':
self.operation_mode == 'heatPump':
return STATE_HEAT
return self.operation_mode
@ -271,10 +277,11 @@ class Thermostat(ClimateDevice):
operation = STATE_HEAT
else:
operation = status
return {
"actual_humidity": self.thermostat['runtime']['actualHumidity'],
"fan": self.fan,
"mode": self.mode,
"climate_mode": self.mode,
"operation": operation,
"climate_list": self.climate_list,
"fan_min_on_time": self.fan_min_on_time
@ -342,25 +349,46 @@ class Thermostat(ClimateDevice):
cool_temp_setpoint, heat_temp_setpoint,
self.hold_preference())
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
"cool=%s, is=%s", heat_temp, isinstance(
heat_temp, (int, float)), cool_temp,
"cool=%s, is=%s", heat_temp,
isinstance(heat_temp, (int, float)), cool_temp,
isinstance(cool_temp, (int, float)))
self.update_without_throttle = True
def set_fan_mode(self, fan_mode):
"""Set the fan mode. Valid values are "on" or "auto"."""
if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO):
error = "Invalid fan_mode value: Valid values are 'on' or 'auto'"
_LOGGER.error(error)
return
cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0
heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0
self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode,
cool_temp, heat_temp,
self.hold_preference())
_LOGGER.info("Setting fan mode to: %s", fan_mode)
def set_temp_hold(self, temp):
"""Set temperature hold in modes other than auto."""
# Set arbitrary range when not in auto mode
if self.current_operation == STATE_HEAT:
"""Set temperature hold in modes other than auto.
Ecobee API: It is good practice to set the heat and cool hold
temperatures to be the same, if the thermostat is in either heat, cool,
auxHeatOnly, or off mode. If the thermostat is in auto mode, an
additional rule is required. The cool hold temperature must be greater
than the heat hold temperature by at least the amount in the
heatCoolMinDelta property.
https://www.ecobee.com/home/developer/api/examples/ex5.shtml
"""
if self.current_operation == STATE_HEAT or self.current_operation == \
STATE_COOL:
heat_temp = temp
cool_temp = temp + 20
elif self.current_operation == STATE_COOL:
heat_temp = temp - 20
cool_temp = temp
else:
# In auto mode set temperature between
heat_temp = temp - 10
cool_temp = temp + 10
delta = self.thermostat['settings']['heatCoolMinDelta'] / 10
heat_temp = temp - delta
cool_temp = temp + delta
self.set_auto_temp_hold(heat_temp, cool_temp)
def set_temperature(self, **kwargs):
@ -369,8 +397,8 @@ class Thermostat(ClimateDevice):
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
temp = kwargs.get(ATTR_TEMPERATURE)
if self.current_operation == STATE_AUTO and (low_temp is not None or
high_temp is not None):
if self.current_operation == STATE_AUTO and \
(low_temp is not None or high_temp is not None):
self.set_auto_temp_hold(low_temp, high_temp)
elif temp is not None:
self.set_temp_hold(temp)

View file

@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle
from homeassistant.util.json import save_json
REQUIREMENTS = ['python-ecobee-api==0.0.15']
REQUIREMENTS = ['python-ecobee-api==0.0.17']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)

View file

@ -911,7 +911,7 @@ python-clementine-remote==1.0.1
python-digitalocean==1.13.2
# homeassistant.components.ecobee
python-ecobee-api==0.0.15
python-ecobee-api==0.0.17
# homeassistant.components.climate.eq3btsmart
# python-eq3bt==0.1.9

View file

@ -3,6 +3,7 @@ import unittest
from unittest import mock
import homeassistant.const as const
import homeassistant.components.climate.ecobee as ecobee
from homeassistant.components.climate import STATE_OFF
class TestEcobee(unittest.TestCase):
@ -23,6 +24,7 @@ class TestEcobee(unittest.TestCase):
'desiredFanMode': 'on'},
'settings': {'hvacMode': 'auto',
'fanMinOnTime': 10,
'heatCoolMinDelta': 50,
'holdAction': 'nextTransition'},
'equipmentStatus': 'fan',
'events': [{'name': 'Event1',
@ -81,17 +83,17 @@ class TestEcobee(unittest.TestCase):
def test_desired_fan_mode(self):
"""Test desired fan mode property."""
self.assertEqual('on', self.thermostat.desired_fan_mode)
self.assertEqual('on', self.thermostat.current_fan_mode)
self.ecobee['runtime']['desiredFanMode'] = 'auto'
self.assertEqual('auto', self.thermostat.desired_fan_mode)
self.assertEqual('auto', self.thermostat.current_fan_mode)
def test_fan(self):
"""Test fan property."""
self.assertEqual(const.STATE_ON, self.thermostat.fan)
self.ecobee['equipmentStatus'] = ''
self.assertEqual(const.STATE_OFF, self.thermostat.fan)
self.assertEqual(STATE_OFF, self.thermostat.fan)
self.ecobee['equipmentStatus'] = 'heatPump, heatPump2'
self.assertEqual(const.STATE_OFF, self.thermostat.fan)
self.assertEqual(STATE_OFF, self.thermostat.fan)
def test_current_hold_mode_away_temporary(self):
"""Test current hold mode when away."""
@ -180,7 +182,7 @@ class TestEcobee(unittest.TestCase):
'climate_list': ['Climate1', 'Climate2'],
'fan': 'off',
'fan_min_on_time': 10,
'mode': 'Climate1',
'climate_mode': 'Climate1',
'operation': 'heat'},
self.thermostat.device_state_attributes)
@ -189,7 +191,7 @@ class TestEcobee(unittest.TestCase):
'climate_list': ['Climate1', 'Climate2'],
'fan': 'off',
'fan_min_on_time': 10,
'mode': 'Climate1',
'climate_mode': 'Climate1',
'operation': 'heat'},
self.thermostat.device_state_attributes)
self.ecobee['equipmentStatus'] = 'compCool1'
@ -197,7 +199,7 @@ class TestEcobee(unittest.TestCase):
'climate_list': ['Climate1', 'Climate2'],
'fan': 'off',
'fan_min_on_time': 10,
'mode': 'Climate1',
'climate_mode': 'Climate1',
'operation': 'cool'},
self.thermostat.device_state_attributes)
self.ecobee['equipmentStatus'] = ''
@ -205,7 +207,7 @@ class TestEcobee(unittest.TestCase):
'climate_list': ['Climate1', 'Climate2'],
'fan': 'off',
'fan_min_on_time': 10,
'mode': 'Climate1',
'climate_mode': 'Climate1',
'operation': 'idle'},
self.thermostat.device_state_attributes)
@ -214,7 +216,7 @@ class TestEcobee(unittest.TestCase):
'climate_list': ['Climate1', 'Climate2'],
'fan': 'off',
'fan_min_on_time': 10,
'mode': 'Climate1',
'climate_mode': 'Climate1',
'operation': 'Unknown'},
self.thermostat.device_state_attributes)
@ -321,7 +323,7 @@ class TestEcobee(unittest.TestCase):
self.assertFalse(self.data.ecobee.delete_vacation.called)
self.assertFalse(self.data.ecobee.resume_program.called)
self.data.ecobee.set_hold_temp.assert_has_calls(
[mock.call(1, 40.0, 20.0, 'nextTransition')])
[mock.call(1, 35.0, 25.0, 'nextTransition')])
self.assertFalse(self.data.ecobee.set_climate_hold.called)
def test_set_auto_temp_hold(self):
@ -337,21 +339,21 @@ class TestEcobee(unittest.TestCase):
self.data.reset_mock()
self.thermostat.set_temp_hold(30.0)
self.data.ecobee.set_hold_temp.assert_has_calls(
[mock.call(1, 40.0, 20.0, 'nextTransition')])
[mock.call(1, 35.0, 25.0, 'nextTransition')])
# Heat mode
self.data.reset_mock()
self.ecobee['settings']['hvacMode'] = 'heat'
self.thermostat.set_temp_hold(30)
self.data.ecobee.set_hold_temp.assert_has_calls(
[mock.call(1, 50, 30, 'nextTransition')])
[mock.call(1, 30, 30, 'nextTransition')])
# Cool mode
self.data.reset_mock()
self.ecobee['settings']['hvacMode'] = 'cool'
self.thermostat.set_temp_hold(30)
self.data.ecobee.set_hold_temp.assert_has_calls(
[mock.call(1, 30, 10, 'nextTransition')])
[mock.call(1, 30, 30, 'nextTransition')])
def test_set_temperature(self):
"""Test set temperature."""
@ -366,21 +368,21 @@ class TestEcobee(unittest.TestCase):
self.data.reset_mock()
self.thermostat.set_temperature(temperature=20)
self.data.ecobee.set_hold_temp.assert_has_calls(
[mock.call(1, 30, 10, 'nextTransition')])
[mock.call(1, 25, 15, 'nextTransition')])
# Cool -> Hold
self.data.reset_mock()
self.ecobee['settings']['hvacMode'] = 'cool'
self.thermostat.set_temperature(temperature=20.5)
self.data.ecobee.set_hold_temp.assert_has_calls(
[mock.call(1, 20.5, 0.5, 'nextTransition')])
[mock.call(1, 20.5, 20.5, 'nextTransition')])
# Heat -> Hold
self.data.reset_mock()
self.ecobee['settings']['hvacMode'] = 'heat'
self.thermostat.set_temperature(temperature=20)
self.data.ecobee.set_hold_temp.assert_has_calls(
[mock.call(1, 40, 20, 'nextTransition')])
[mock.call(1, 20, 20, 'nextTransition')])
# Heat -> Auto
self.data.reset_mock()
@ -450,3 +452,17 @@ class TestEcobee(unittest.TestCase):
"""Test climate list property."""
self.assertEqual(['Climate1', 'Climate2'],
self.thermostat.climate_list)
def test_set_fan_mode_on(self):
"""Test set fan mode to on."""
self.data.reset_mock()
self.thermostat.set_fan_mode('on')
self.data.ecobee.set_fan_mode.assert_has_calls(
[mock.call(1, 'on', 20, 40, 'nextTransition')])
def test_set_fan_mode_auto(self):
"""Test set fan mode to auto."""
self.data.reset_mock()
self.thermostat.set_fan_mode('auto')
self.data.ecobee.set_fan_mode.assert_has_calls(
[mock.call(1, 'auto', 20, 40, 'nextTransition')])