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:
parent
8a0facb747
commit
9eda04b787
8 changed files with 126 additions and 65 deletions
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue