Homekit: Bugfix Thermostat Fahrenheit support (#13477)

* Bugfix thermostat temperature conversion
* util -> temperature_to_homekit
* util -> temperature_to_states
* util -> convert_to_float
* Added tests, deleted log msg
This commit is contained in:
cdce8p 2018-03-27 11:31:18 +02:00 committed by GitHub
parent 8a0facb747
commit 9eda04b787
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 65 deletions

View file

@ -186,9 +186,6 @@ class HomeKit():
for state in self._hass.states.all():
self.add_bridge_accessory(state)
for entity_id in self._config:
_LOGGER.warning('The entity "%s" was not setup when HomeKit '
'was started', entity_id)
self.bridge.set_broker(self.driver)
if not self.bridge.paired:

View file

@ -2,8 +2,7 @@
import logging
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS)
from homeassistant.util.temperature import fahrenheit_to_celsius
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
from . import TYPES
from .accessories import (
@ -11,33 +10,12 @@ from .accessories import (
from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
from .util import convert_to_float, temperature_to_homekit
_LOGGER = logging.getLogger(__name__)
def calc_temperature(state, unit=TEMP_CELSIUS):
"""Calculate temperature from state and unit.
Always return temperature as Celsius value.
Conversion is handled on the device.
"""
try:
value = float(state)
except ValueError:
return None
return fahrenheit_to_celsius(value) if unit == TEMP_FAHRENHEIT else value
def calc_humidity(state):
"""Calculate humidity from state."""
try:
return float(state)
except ValueError:
return None
@TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory):
"""Generate a TemperatureSensor accessory for a temperature sensor.
@ -63,9 +41,10 @@ class TemperatureSensor(HomeAccessory):
if new_state is None:
return
unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
temperature = calc_temperature(new_state.state, unit)
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
temperature = convert_to_float(new_state.state)
if temperature:
temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature, should_callback=False)
_LOGGER.debug('%s: Current temperature set to %d°C',
self._entity_id, temperature)
@ -92,8 +71,8 @@ class HumiditySensor(HomeAccessory):
if new_state is None:
return
humidity = calc_humidity(new_state.state)
humidity = convert_to_float(new_state.state)
if humidity:
self.char_humidity.set_value(humidity, should_callback=False)
_LOGGER.debug('%s: Current humidity set to %d%%',
_LOGGER.debug('%s: Percent set to %d%%',
self._entity_id, humidity)

View file

@ -16,6 +16,7 @@ from .const import (
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__)
@ -40,6 +41,7 @@ class Thermostat(HomeAccessory):
self._hass = hass
self._entity_id = entity_id
self._call_timer = None
self._unit = TEMP_CELSIUS
self.heat_cool_flag_target_state = False
self.temperature_flag_target_state = False
@ -107,33 +109,38 @@ class Thermostat(HomeAccessory):
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f',
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
self._entity_id, value)
self.coolingthresh_flag_target_state = True
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
low = self.char_heating_thresh_temp.value
low = temperature_to_states(low, self._unit)
value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature(
entity_id=self._entity_id, target_temp_high=value,
target_temp_low=low)
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heating threshold temperature to %.2f',
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
self._entity_id, value)
self.heatingthresh_flag_target_state = True
self.char_heating_thresh_temp.set_value(value, should_callback=False)
# Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit)
value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature(
entity_id=self._entity_id, target_temp_high=high,
target_temp_low=value)
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f',
_LOGGER.debug('%s: Set target temperature to %.2f°C',
self._entity_id, value)
self.temperature_flag_target_state = True
self.char_target_temp.set_value(value, should_callback=False)
value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature(
temperature=value, entity_id=self._entity_id)
@ -142,14 +149,19 @@ class Thermostat(HomeAccessory):
if new_state is None:
return
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS)
# Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(current_temp, (int, float)):
current_temp = temperature_to_homekit(current_temp, self._unit)
self.char_current_temp.set_value(current_temp)
# Update target temperature
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(target_temp, (int, float)):
target_temp = temperature_to_homekit(target_temp, self._unit)
if not self.temperature_flag_target_state:
self.char_target_temp.set_value(target_temp,
should_callback=False)
@ -158,7 +170,9 @@ class Thermostat(HomeAccessory):
# Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
if cooling_thresh:
if isinstance(cooling_thresh, (int, float)):
cooling_thresh = temperature_to_homekit(cooling_thresh,
self._unit)
if not self.coolingthresh_flag_target_state:
self.char_cooling_thresh_temp.set_value(
cooling_thresh, should_callback=False)
@ -167,18 +181,17 @@ class Thermostat(HomeAccessory):
# Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp:
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
if heating_thresh:
if isinstance(heating_thresh, (int, float)):
heating_thresh = temperature_to_homekit(heating_thresh,
self._unit)
if not self.heatingthresh_flag_target_state:
self.char_heating_thresh_temp.set_value(
heating_thresh, should_callback=False)
self.heatingthresh_flag_target_state = False
# Update display units
display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if display_units \
and display_units in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(
UNIT_HASS_TO_HOMEKIT[display_units])
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit])
# Update target operation mode
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)

View file

@ -5,8 +5,9 @@ import voluptuous as vol
from homeassistant.core import split_entity_id
from homeassistant.const import (
ATTR_CODE)
ATTR_CODE, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.temperature as temp_util
from .const import HOMEKIT_NOTIFY_ID
_LOGGER = logging.getLogger(__name__)
@ -44,3 +45,21 @@ def show_setup_message(bridge, hass):
def dismiss_setup_message(hass):
"""Dismiss persistent notification and remove QR code."""
hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID)
def convert_to_float(state):
"""Return float of state, catch errors."""
try:
return float(state)
except (ValueError, TypeError):
return None
def temperature_to_homekit(temperature, unit):
"""Convert temperature to Celsius for HomeKit."""
return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)
def temperature_to_states(temperature, unit):
"""Convert temperature back from Celsius to Home Assistant unit."""
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1)

View file

@ -16,6 +16,20 @@ _LOGGER = logging.getLogger(__name__)
CONFIG = {}
def test_get_accessory_invalid_aid(caplog):
"""Test with unsupported component."""
assert get_accessory(None, State('light.demo', 'on'),
aid=None, config=None) is None
assert caplog.records[0].levelname == 'WARNING'
assert 'invalid aid' in caplog.records[0].msg
def test_not_supported():
"""Test if none is returned if entity isn't supported."""
assert get_accessory(None, State('demo.demo', 'on'), aid=2, config=None) \
is None
class TestGetAccessories(unittest.TestCase):
"""Methods to test the get_accessory method."""

View file

@ -3,32 +3,13 @@ import unittest
from homeassistant.components.homekit.const import PROP_CELSIUS
from homeassistant.components.homekit.type_sensors import (
TemperatureSensor, HumiditySensor, calc_temperature, calc_humidity)
TemperatureSensor, HumiditySensor)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from tests.common import get_test_home_assistant
def test_calc_temperature():
"""Test if temperature in Celsius is calculated correctly."""
assert calc_temperature(STATE_UNKNOWN) is None
assert calc_temperature('test') is None
assert calc_temperature('20') == 20
assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12
assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24
def test_calc_humidity():
"""Test if humidity is a integer."""
assert calc_humidity(STATE_UNKNOWN) is None
assert calc_humidity('test') is None
assert calc_humidity('20') == 20
assert calc_humidity('75.2') == 75.2
class TestHomekitSensors(unittest.TestCase):
"""Test class for all accessory types regarding sensors."""

View file

@ -10,7 +10,7 @@ from homeassistant.components.homekit.type_thermostats import (
Thermostat, STATE_OFF)
from homeassistant.const import (
ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA,
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from tests.common import get_test_home_assistant
@ -238,3 +238,42 @@ class TestHomekitThermostats(unittest.TestCase):
self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH],
25.0)
self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0)
def test_thermostat_fahrenheit(self):
"""Test if accessory and HA are updated accordingly."""
climate = 'climate.test'
acc = Thermostat(self.hass, climate, 'Climate', True)
acc.run()
self.hass.states.set(climate, STATE_AUTO,
{ATTR_OPERATION_MODE: STATE_AUTO,
ATTR_TARGET_TEMP_HIGH: 75.2,
ATTR_TARGET_TEMP_LOW: 68,
ATTR_TEMPERATURE: 71.6,
ATTR_CURRENT_TEMPERATURE: 73.4,
ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT})
self.hass.block_till_done()
self.assertEqual(acc.char_heating_thresh_temp.value, 20.0)
self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0)
self.assertEqual(acc.char_current_temp.value, 23.0)
self.assertEqual(acc.char_target_temp.value, 22.0)
self.assertEqual(acc.char_display_units.value, 1)
# Set from HomeKit
acc.char_cooling_thresh_temp.set_value(23)
self.hass.block_till_done()
service_data = self.events[-1].data[ATTR_SERVICE_DATA]
self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4)
self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68)
acc.char_heating_thresh_temp.set_value(22)
self.hass.block_till_done()
service_data = self.events[-1].data[ATTR_SERVICE_DATA]
self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4)
self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6)
acc.char_target_temp.set_value(24.0)
self.hass.block_till_done()
service_data = self.events[-1].data[ATTR_SERVICE_DATA]
self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2)

View file

@ -7,13 +7,15 @@ from homeassistant.core import callback
from homeassistant.components.homekit.accessories import HomeBridge
from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID
from homeassistant.components.homekit.util import (
show_setup_message, dismiss_setup_message, ATTR_CODE)
show_setup_message, dismiss_setup_message, convert_to_float,
temperature_to_homekit, temperature_to_states, ATTR_CODE)
from homeassistant.components.homekit.util import validate_entity_config \
as vec
from homeassistant.components.persistent_notification import (
SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID)
from homeassistant.const import (
EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA)
EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA,
TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN)
from tests.common import get_test_home_assistant
@ -81,3 +83,20 @@ class TestUtil(unittest.TestCase):
self.assertEqual(
data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None),
HOMEKIT_NOTIFY_ID)
def test_convert_to_float(self):
"""Test convert_to_float method."""
self.assertEqual(convert_to_float(12), 12)
self.assertEqual(convert_to_float(12.4), 12.4)
self.assertIsNone(convert_to_float(STATE_UNKNOWN))
self.assertIsNone(convert_to_float(None))
def test_temperature_to_homekit(self):
"""Test temperature conversion from HA to HomeKit."""
self.assertEqual(temperature_to_homekit(20.46, TEMP_CELSIUS), 20.5)
self.assertEqual(temperature_to_homekit(92.1, TEMP_FAHRENHEIT), 33.4)
def test_temperature_to_states(self):
"""Test temperature conversion from HomeKit to HA."""
self.assertEqual(temperature_to_states(20, TEMP_CELSIUS), 20.0)
self.assertEqual(temperature_to_states(20.2, TEMP_FAHRENHEIT), 68.4)