Skip to content

Commit

Permalink
Add ecobee fan mode (#12732)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
uchagani authored and balloob committed Mar 18, 2018
1 parent 022d8fb commit 1dcc51c
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 44 deletions.
80 changes: 54 additions & 26 deletions homeassistant/components/climate/ecobee.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
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 = {}
Expand Down Expand Up @@ -50,7 +50,7 @@
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):
Expand Down Expand Up @@ -122,6 +122,7 @@ def __init__(self, data, thermostat_index, hold_temp):
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):
Expand Down Expand Up @@ -180,24 +181,29 @@ def target_temperature(self):
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']
Expand All @@ -206,7 +212,7 @@ def _current_hold_mode(self):
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
Expand All @@ -228,7 +234,7 @@ def _current_hold_mode(self):
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

Expand Down Expand Up @@ -271,10 +277,11 @@ def device_state_attributes(self):
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
Expand Down Expand Up @@ -342,25 +349,46 @@ def set_auto_temp_hold(self, heat_temp, cool_temp):
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):
Expand All @@ -369,8 +397,8 @@ def set_temperature(self, **kwargs):
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)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/ecobee.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
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__)
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 32 additions & 16 deletions tests/components/climate/test_ecobee.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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):
Expand All @@ -23,6 +24,7 @@ def setUp(self):
'desiredFanMode': 'on'},
'settings': {'hvacMode': 'auto',
'fanMinOnTime': 10,
'heatCoolMinDelta': 50,
'holdAction': 'nextTransition'},
'equipmentStatus': 'fan',
'events': [{'name': 'Event1',
Expand Down Expand Up @@ -81,17 +83,17 @@ def test_target_temperature(self):

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."""
Expand Down Expand Up @@ -180,7 +182,7 @@ def test_device_state_attributes(self):
'climate_list': ['Climate1', 'Climate2'],
'fan': 'off',
'fan_min_on_time': 10,
'mode': 'Climate1',
'climate_mode': 'Climate1',
'operation': 'heat'},
self.thermostat.device_state_attributes)

Expand All @@ -189,23 +191,23 @@ def test_device_state_attributes(self):
'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'
self.assertEqual({'actual_humidity': 15,
'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'] = ''
self.assertEqual({'actual_humidity': 15,
'climate_list': ['Climate1', 'Climate2'],
'fan': 'off',
'fan_min_on_time': 10,
'mode': 'Climate1',
'climate_mode': 'Climate1',
'operation': 'idle'},
self.thermostat.device_state_attributes)

Expand All @@ -214,7 +216,7 @@ def test_device_state_attributes(self):
'climate_list': ['Climate1', 'Climate2'],
'fan': 'off',
'fan_min_on_time': 10,
'mode': 'Climate1',
'climate_mode': 'Climate1',
'operation': 'Unknown'},
self.thermostat.device_state_attributes)

Expand Down Expand Up @@ -321,7 +323,7 @@ def test_set_hold_mode(self):
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):
Expand All @@ -337,21 +339,21 @@ def test_set_temp_hold(self):
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."""
Expand All @@ -366,21 +368,21 @@ def test_set_temperature(self):
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()
Expand Down Expand Up @@ -450,3 +452,17 @@ def test_climate_list(self):
"""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')])

0 comments on commit 1dcc51c

Please sign in to comment.