mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Homekit Thermostat: Better support for temperature ranges (#14679)
* Support for obtaining temperature range * Fallback to Defaults * Fixed unit conversion * Added test
This commit is contained in:
parent
6cd69b413c
commit
ab3717af76
@ -4,15 +4,16 @@ import logging
|
|||||||
from pyhap.const import CATEGORY_THERMOSTAT
|
from pyhap.const import CATEGORY_THERMOSTAT
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE,
|
ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP,
|
||||||
|
ATTR_OPERATION_LIST, ATTR_OPERATION_MODE,
|
||||||
ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||||
|
DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP,
|
||||||
DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO,
|
DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO,
|
||||||
STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF,
|
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import debounce, HomeAccessory
|
from .accessories import debounce, HomeAccessory
|
||||||
@ -20,7 +21,7 @@ from .const import (
|
|||||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING,
|
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING,
|
||||||
CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING,
|
CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING,
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE,
|
CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE,
|
||||||
CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT)
|
CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT)
|
||||||
from .util import temperature_to_homekit, temperature_to_states
|
from .util import temperature_to_homekit, temperature_to_states
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -42,17 +43,18 @@ class Thermostat(HomeAccessory):
|
|||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
"""Initialize a Thermostat accessory object."""
|
"""Initialize a Thermostat accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_THERMOSTAT)
|
super().__init__(*args, category=CATEGORY_THERMOSTAT)
|
||||||
self._unit = TEMP_CELSIUS
|
self._unit = self.hass.config.units.temperature_unit
|
||||||
self.support_power_state = False
|
self.support_power_state = False
|
||||||
self.heat_cool_flag_target_state = False
|
self.heat_cool_flag_target_state = False
|
||||||
self.temperature_flag_target_state = False
|
self.temperature_flag_target_state = False
|
||||||
self.coolingthresh_flag_target_state = False
|
self.coolingthresh_flag_target_state = False
|
||||||
self.heatingthresh_flag_target_state = False
|
self.heatingthresh_flag_target_state = False
|
||||||
|
min_temp, max_temp = self.get_temperature_range()
|
||||||
|
|
||||||
# Add additional characteristics if auto mode is supported
|
# Add additional characteristics if auto mode is supported
|
||||||
self.chars = []
|
self.chars = []
|
||||||
features = self.hass.states.get(self.entity_id) \
|
features = self.hass.states.get(self.entity_id) \
|
||||||
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if features & SUPPORT_ON_OFF:
|
if features & SUPPORT_ON_OFF:
|
||||||
self.support_power_state = True
|
self.support_power_state = True
|
||||||
if features & SUPPORT_TEMP_RANGE:
|
if features & SUPPORT_TEMP_RANGE:
|
||||||
@ -73,6 +75,8 @@ class Thermostat(HomeAccessory):
|
|||||||
CHAR_CURRENT_TEMPERATURE, value=21.0)
|
CHAR_CURRENT_TEMPERATURE, value=21.0)
|
||||||
self.char_target_temp = serv_thermostat.configure_char(
|
self.char_target_temp = serv_thermostat.configure_char(
|
||||||
CHAR_TARGET_TEMPERATURE, value=21.0,
|
CHAR_TARGET_TEMPERATURE, value=21.0,
|
||||||
|
properties={PROP_MIN_VALUE: min_temp,
|
||||||
|
PROP_MAX_VALUE: max_temp},
|
||||||
setter_callback=self.set_target_temperature)
|
setter_callback=self.set_target_temperature)
|
||||||
|
|
||||||
# Display units characteristic
|
# Display units characteristic
|
||||||
@ -85,12 +89,30 @@ class Thermostat(HomeAccessory):
|
|||||||
if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
|
if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
|
||||||
self.char_cooling_thresh_temp = serv_thermostat.configure_char(
|
self.char_cooling_thresh_temp = serv_thermostat.configure_char(
|
||||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0,
|
CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0,
|
||||||
|
properties={PROP_MIN_VALUE: min_temp,
|
||||||
|
PROP_MAX_VALUE: max_temp},
|
||||||
setter_callback=self.set_cooling_threshold)
|
setter_callback=self.set_cooling_threshold)
|
||||||
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
|
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
|
||||||
self.char_heating_thresh_temp = serv_thermostat.configure_char(
|
self.char_heating_thresh_temp = serv_thermostat.configure_char(
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0,
|
CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0,
|
||||||
|
properties={PROP_MIN_VALUE: min_temp,
|
||||||
|
PROP_MAX_VALUE: max_temp},
|
||||||
setter_callback=self.set_heating_threshold)
|
setter_callback=self.set_heating_threshold)
|
||||||
|
|
||||||
|
def get_temperature_range(self):
|
||||||
|
"""Return min and max temperature range."""
|
||||||
|
max_temp = self.hass.states.get(self.entity_id) \
|
||||||
|
.attributes.get(ATTR_MAX_TEMP)
|
||||||
|
max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \
|
||||||
|
else DEFAULT_MAX_TEMP
|
||||||
|
|
||||||
|
min_temp = self.hass.states.get(self.entity_id) \
|
||||||
|
.attributes.get(ATTR_MIN_TEMP)
|
||||||
|
min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \
|
||||||
|
else DEFAULT_MIN_TEMP
|
||||||
|
|
||||||
|
return min_temp, max_temp
|
||||||
|
|
||||||
def set_heat_cool(self, value):
|
def set_heat_cool(self, value):
|
||||||
"""Move operation mode to value if call came from HomeKit."""
|
"""Move operation mode to value if call came from HomeKit."""
|
||||||
if value in HC_HOMEKIT_TO_HASS:
|
if value in HC_HOMEKIT_TO_HASS:
|
||||||
@ -147,9 +169,6 @@ class Thermostat(HomeAccessory):
|
|||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
"""Update security state after state changed."""
|
"""Update security state after state changed."""
|
||||||
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
|
|
||||||
TEMP_CELSIUS)
|
|
||||||
|
|
||||||
# Update current temperature
|
# Update current temperature
|
||||||
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
|
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
|
||||||
if isinstance(current_temp, (int, float)):
|
if isinstance(current_temp, (int, float)):
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
"""Test different accessory types: Thermostats."""
|
"""Test different accessory types: Thermostats."""
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW,
|
ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TEMPERATURE,
|
||||||
ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE,
|
||||||
DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT)
|
ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN,
|
||||||
|
STATE_AUTO, STATE_COOL, STATE_HEAT)
|
||||||
|
from homeassistant.components.homekit.const import (
|
||||||
|
PROP_MAX_VALUE, PROP_MIN_VALUE)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||||
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
@ -31,7 +35,7 @@ async def test_default_thermostat(hass, hk_driver, cls):
|
|||||||
"""Test if accessory and HA are updated accordingly."""
|
"""Test if accessory and HA are updated accordingly."""
|
||||||
entity_id = 'climate.test'
|
entity_id = 'climate.test'
|
||||||
|
|
||||||
hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0})
|
hass.states.async_set(entity_id, STATE_OFF)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None)
|
acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None)
|
||||||
await hass.async_add_job(acc.run)
|
await hass.async_add_job(acc.run)
|
||||||
@ -48,6 +52,9 @@ async def test_default_thermostat(hass, hk_driver, cls):
|
|||||||
assert acc.char_cooling_thresh_temp is None
|
assert acc.char_cooling_thresh_temp is None
|
||||||
assert acc.char_heating_thresh_temp is None
|
assert acc.char_heating_thresh_temp is None
|
||||||
|
|
||||||
|
assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP
|
||||||
|
|
||||||
hass.states.async_set(entity_id, STATE_HEAT,
|
hass.states.async_set(entity_id, STATE_HEAT,
|
||||||
{ATTR_OPERATION_MODE: STATE_HEAT,
|
{ATTR_OPERATION_MODE: STATE_HEAT,
|
||||||
ATTR_TEMPERATURE: 22.0,
|
ATTR_TEMPERATURE: 22.0,
|
||||||
@ -181,6 +188,15 @@ async def test_auto_thermostat(hass, hk_driver, cls):
|
|||||||
assert acc.char_cooling_thresh_temp.value == 23.0
|
assert acc.char_cooling_thresh_temp.value == 23.0
|
||||||
assert acc.char_heating_thresh_temp.value == 19.0
|
assert acc.char_heating_thresh_temp.value == 19.0
|
||||||
|
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] \
|
||||||
|
== DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] \
|
||||||
|
== DEFAULT_MIN_TEMP
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] \
|
||||||
|
== DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] \
|
||||||
|
== DEFAULT_MIN_TEMP
|
||||||
|
|
||||||
hass.states.async_set(entity_id, STATE_AUTO,
|
hass.states.async_set(entity_id, STATE_AUTO,
|
||||||
{ATTR_OPERATION_MODE: STATE_AUTO,
|
{ATTR_OPERATION_MODE: STATE_AUTO,
|
||||||
ATTR_TARGET_TEMP_HIGH: 22.0,
|
ATTR_TARGET_TEMP_HIGH: 22.0,
|
||||||
@ -307,7 +323,9 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls):
|
|||||||
# support_auto = True
|
# support_auto = True
|
||||||
hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6})
|
hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None)
|
with patch.object(hass.config.units, 'temperature_unit',
|
||||||
|
new=TEMP_FAHRENHEIT):
|
||||||
|
acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None)
|
||||||
await hass.async_add_job(acc.run)
|
await hass.async_add_job(acc.run)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -349,3 +367,23 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls):
|
|||||||
assert call_set_temperature[2]
|
assert call_set_temperature[2]
|
||||||
assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id
|
assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id
|
||||||
assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2
|
assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_temperature_range(hass, hk_driver, cls):
|
||||||
|
"""Test if temperature range is evaluated correctly."""
|
||||||
|
entity_id = 'climate.test'
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_OFF)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None)
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_OFF,
|
||||||
|
{ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.get_temperature_range() == (20, 25)
|
||||||
|
|
||||||
|
acc._unit = TEMP_FAHRENHEIT
|
||||||
|
hass.states.async_set(entity_id, STATE_OFF,
|
||||||
|
{ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.get_temperature_range() == (15.6, 21.1)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user