Skip to content

Commit

Permalink
Improved Fritz!Box thermostat support (#14789)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaskr authored and syssi committed Jun 5, 2018
1 parent 21d05a8 commit 549abd9
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ omit =
homeassistant/components/*/envisalink.py

homeassistant/components/fritzbox.py
homeassistant/components/*/fritzbox.py
homeassistant/components/switch/fritzbox.py

homeassistant/components/eufy.py
homeassistant/components/*/eufy.py
Expand Down
25 changes: 21 additions & 4 deletions homeassistant/components/climate/fritzbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,27 @@
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED)
from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (
ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS)

DEPENDENCIES = ['fritzbox']

_LOGGER = logging.getLogger(__name__)

SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)

OPERATION_LIST = [STATE_HEAT, STATE_ECO]
OPERATION_LIST = [STATE_HEAT, STATE_ECO, STATE_OFF, STATE_ON]

MIN_TEMPERATURE = 8
MAX_TEMPERATURE = 28

# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
ON_API_TEMPERATURE = 127.0
OFF_API_TEMPERATURE = 126.5
ON_REPORT_SET_TEMPERATURE = 30.0
OFF_REPORT_SET_TEMPERATURE = 0.0


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Fritzbox smarthome thermostat platform."""
Expand Down Expand Up @@ -88,6 +94,9 @@ def current_temperature(self):
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self._target_temperature in (ON_API_TEMPERATURE,
OFF_API_TEMPERATURE):
return None
return self._target_temperature

def set_temperature(self, **kwargs):
Expand All @@ -102,9 +111,13 @@ def set_temperature(self, **kwargs):
@property
def current_operation(self):
"""Return the current operation mode."""
if self._target_temperature == ON_API_TEMPERATURE:
return STATE_ON
if self._target_temperature == OFF_API_TEMPERATURE:
return STATE_OFF
if self._target_temperature == self._comfort_temperature:
return STATE_HEAT
elif self._target_temperature == self._eco_temperature:
if self._target_temperature == self._eco_temperature:
return STATE_ECO
return STATE_MANUAL

Expand All @@ -119,6 +132,10 @@ def set_operation_mode(self, operation_mode):
self.set_temperature(temperature=self._comfort_temperature)
elif operation_mode == STATE_ECO:
self.set_temperature(temperature=self._eco_temperature)
elif operation_mode == STATE_OFF:
self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
elif operation_mode == STATE_ON:
self.set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)

@property
def min_temp(self):
Expand Down
172 changes: 172 additions & 0 deletions tests/components/climate/test_fritzbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""The tests for the demo climate component."""
import unittest
from unittest.mock import Mock, patch

import requests

from homeassistant.components.climate.fritzbox import FritzboxThermostat


class TestFritzboxClimate(unittest.TestCase):
"""Test Fritz!Box heating thermostats."""

def setUp(self):
"""Create a mock device to test on."""
self.device = Mock()
self.device.name = 'Test Thermostat'
self.device.actual_temperature = 18.0
self.device.target_temperature = 19.5
self.device.comfort_temperature = 22.0
self.device.eco_temperature = 16.0
self.device.present = True
self.device.device_lock = True
self.device.lock = False
self.device.battery_low = True
self.device.set_target_temperature = Mock()
self.device.update = Mock()
mock_fritz = Mock()
mock_fritz.login = Mock()
self.thermostat = FritzboxThermostat(self.device, mock_fritz)

def test_init(self):
"""Test instance creation."""
self.assertEqual(18.0, self.thermostat._current_temperature)
self.assertEqual(19.5, self.thermostat._target_temperature)
self.assertEqual(22.0, self.thermostat._comfort_temperature)
self.assertEqual(16.0, self.thermostat._eco_temperature)

def test_supported_features(self):
"""Test supported features property."""
self.assertEqual(129, self.thermostat.supported_features)

def test_available(self):
"""Test available property."""
self.assertTrue(self.thermostat.available)
self.thermostat._device.present = False
self.assertFalse(self.thermostat.available)

def test_name(self):
"""Test name property."""
self.assertEqual('Test Thermostat', self.thermostat.name)

def test_temperature_unit(self):
"""Test temperature_unit property."""
self.assertEqual('°C', self.thermostat.temperature_unit)

def test_precision(self):
"""Test precision property."""
self.assertEqual(0.5, self.thermostat.precision)

def test_current_temperature(self):
"""Test current_temperature property incl. special temperatures."""
self.assertEqual(18, self.thermostat.current_temperature)

def test_target_temperature(self):
"""Test target_temperature property."""
self.assertEqual(19.5, self.thermostat.target_temperature)

self.thermostat._target_temperature = 126.5
self.assertEqual(None, self.thermostat.target_temperature)

self.thermostat._target_temperature = 127.0
self.assertEqual(None, self.thermostat.target_temperature)

@patch.object(FritzboxThermostat, 'set_operation_mode')
def test_set_temperature_operation_mode(self, mock_set_op):
"""Test set_temperature by operation_mode."""
self.thermostat.set_temperature(operation_mode='test_mode')
mock_set_op.assert_called_once_with('test_mode')

def test_set_temperature_temperature(self):
"""Test set_temperature by temperature."""
self.thermostat.set_temperature(temperature=23.0)
self.thermostat._device.set_target_temperature.\
assert_called_once_with(23.0)

@patch.object(FritzboxThermostat, 'set_operation_mode')
def test_set_temperature_none(self, mock_set_op):
"""Test set_temperature with no arguments."""
self.thermostat.set_temperature()
mock_set_op.assert_not_called()
self.thermostat._device.set_target_temperature.assert_not_called()

@patch.object(FritzboxThermostat, 'set_operation_mode')
def test_set_temperature_operation_mode_precedence(self, mock_set_op):
"""Test set_temperature for precedence of operation_mode arguement."""
self.thermostat.set_temperature(operation_mode='test_mode',
temperature=23.0)
mock_set_op.assert_called_once_with('test_mode')
self.thermostat._device.set_target_temperature.assert_not_called()

def test_current_operation(self):
"""Test operation mode property for different temperatures."""
self.thermostat._target_temperature = 127.0
self.assertEqual('on', self.thermostat.current_operation)
self.thermostat._target_temperature = 126.5
self.assertEqual('off', self.thermostat.current_operation)
self.thermostat._target_temperature = 22.0
self.assertEqual('heat', self.thermostat.current_operation)
self.thermostat._target_temperature = 16.0
self.assertEqual('eco', self.thermostat.current_operation)
self.thermostat._target_temperature = 12.5
self.assertEqual('manual', self.thermostat.current_operation)

def test_operation_list(self):
"""Test operation_list property."""
self.assertEqual(['heat', 'eco', 'off', 'on'],
self.thermostat.operation_list)

@patch.object(FritzboxThermostat, 'set_temperature')
def test_set_operation_mode(self, mock_set_temp):
"""Test set_operation_mode by all modes and with a non-existing one."""
values = {
'heat': 22.0,
'eco': 16.0,
'on': 30.0,
'off': 0.0}
for mode, temp in values.items():
print(mode, temp)

mock_set_temp.reset_mock()
self.thermostat.set_operation_mode(mode)
mock_set_temp.assert_called_once_with(temperature=temp)

mock_set_temp.reset_mock()
self.thermostat.set_operation_mode('non_existing_mode')
mock_set_temp.assert_not_called()

def test_min_max_temperature(self):
"""Test min_temp and max_temp properties."""
self.assertEqual(8.0, self.thermostat.min_temp)
self.assertEqual(28.0, self.thermostat.max_temp)

def test_device_state_attributes(self):
"""Test device_state property."""
attr = self.thermostat.device_state_attributes
self.assertEqual(attr['device_locked'], True)
self.assertEqual(attr['locked'], False)
self.assertEqual(attr['battery_low'], True)

def test_update(self):
"""Test update function."""
device = Mock()
device.update = Mock()
device.actual_temperature = 10.0
device.target_temperature = 11.0
device.comfort_temperature = 12.0
device.eco_temperature = 13.0
self.thermostat._device = device

self.thermostat.update()

device.update.assert_called_once_with()
self.assertEqual(10.0, self.thermostat._current_temperature)
self.assertEqual(11.0, self.thermostat._target_temperature)
self.assertEqual(12.0, self.thermostat._comfort_temperature)
self.assertEqual(13.0, self.thermostat._eco_temperature)

def test_update_http_error(self):
"""Test exception handling of update function."""
self.device.update.side_effect = requests.exceptions.HTTPError
self.thermostat.update()
self.thermostat._fritz.login.assert_called_once_with()

0 comments on commit 549abd9

Please sign in to comment.