mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Prevent HomeKit from going unavailable when min/max is reversed (#135892)
This commit is contained in:
parent
fc1b6292cd
commit
43fe4ebbbe
@ -52,6 +52,7 @@ from .const import (
|
||||
PROP_MIN_VALUE,
|
||||
SERV_LIGHTBULB,
|
||||
)
|
||||
from .util import get_min_max
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -120,12 +121,14 @@ class Light(HomeAccessory):
|
||||
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
|
||||
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
self.min_mireds = color_temperature_kelvin_to_mired(
|
||||
min_mireds = color_temperature_kelvin_to_mired(
|
||||
attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN, DEFAULT_MAX_COLOR_TEMP)
|
||||
)
|
||||
self.max_mireds = color_temperature_kelvin_to_mired(
|
||||
max_mireds = color_temperature_kelvin_to_mired(
|
||||
attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN, DEFAULT_MIN_COLOR_TEMP)
|
||||
)
|
||||
# Ensure min is less than max
|
||||
self.min_mireds, self.max_mireds = get_min_max(min_mireds, max_mireds)
|
||||
if not self.color_temp_supported and not self.rgbww_supported:
|
||||
self.max_mireds = self.min_mireds
|
||||
self.char_color_temp = serv_light.configure_char(
|
||||
|
@ -14,6 +14,7 @@ from homeassistant.components.climate import (
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MIN_TEMP,
|
||||
@ -21,6 +22,7 @@ from homeassistant.components.climate import (
|
||||
ATTR_SWING_MODES,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DEFAULT_MIN_TEMP,
|
||||
@ -90,7 +92,7 @@ from .const import (
|
||||
SERV_FANV2,
|
||||
SERV_THERMOSTAT,
|
||||
)
|
||||
from .util import temperature_to_homekit, temperature_to_states
|
||||
from .util import get_min_max, temperature_to_homekit, temperature_to_states
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -208,7 +210,10 @@ class Thermostat(HomeAccessory):
|
||||
self.fan_chars: list[str] = []
|
||||
|
||||
attributes = state.attributes
|
||||
min_humidity = attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY)
|
||||
min_humidity, _ = get_min_max(
|
||||
attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY),
|
||||
attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY),
|
||||
)
|
||||
features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||
@ -839,6 +844,9 @@ def _get_temperature_range_from_state(
|
||||
else:
|
||||
max_temp = default_max
|
||||
|
||||
# Handle reversed temperature range
|
||||
min_temp, max_temp = get_min_max(min_temp, max_temp)
|
||||
|
||||
# Homekit only supports 10-38, overwriting
|
||||
# the max to appears to work, but less than 0 causes
|
||||
# a crash on the home app
|
||||
|
@ -656,3 +656,14 @@ def state_changed_event_is_same_state(event: Event[EventStateChangedData]) -> bo
|
||||
old_state = event_data["old_state"]
|
||||
new_state = event_data["new_state"]
|
||||
return bool(new_state and old_state and new_state.state == old_state.state)
|
||||
|
||||
|
||||
def get_min_max(value1: float, value2: float) -> tuple[float, float]:
|
||||
"""Return the minimum and maximum of two values.
|
||||
|
||||
HomeKit will go unavailable if the min and max are reversed
|
||||
so we make sure the min is always the min and the max is always the max
|
||||
as any mistakes made in integrations will cause the entire
|
||||
bridge to go unavailable.
|
||||
"""
|
||||
return min(value1, value2), max(value1, value2)
|
||||
|
@ -807,6 +807,156 @@ async def test_light_invalid_values(
|
||||
assert acc.char_saturation.value == 95
|
||||
|
||||
|
||||
async def test_light_out_of_range_color_temp(hass: HomeAssistant, hk_driver) -> None:
|
||||
"""Test light with an out of range color temp."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||
ATTR_COLOR_MODE: "hs",
|
||||
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||
ATTR_HS_COLOR: (-1, -1),
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||
hk_driver.add_accessory(acc)
|
||||
|
||||
assert acc.char_color_temp.value == 333
|
||||
assert acc.char_color_temp.properties[PROP_MAX_VALUE] == 333
|
||||
assert acc.char_color_temp.properties[PROP_MIN_VALUE] == 250
|
||||
assert acc.char_hue.value == 31
|
||||
assert acc.char_saturation.value == 95
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||
ATTR_COLOR_MODE: "color_temp",
|
||||
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||
ATTR_COLOR_TEMP_KELVIN: -1,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc.run()
|
||||
|
||||
assert acc.char_color_temp.value == 250
|
||||
assert acc.char_hue.value == 16
|
||||
assert acc.char_saturation.value == 100
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||
ATTR_COLOR_MODE: "color_temp",
|
||||
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||
ATTR_COLOR_TEMP_KELVIN: sys.maxsize,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert acc.char_color_temp.value == 250
|
||||
assert acc.char_hue.value == 220
|
||||
assert acc.char_saturation.value == 41
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||
ATTR_COLOR_MODE: "color_temp",
|
||||
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert acc.char_color_temp.value == 250
|
||||
assert acc.char_hue.value == 220
|
||||
assert acc.char_saturation.value == 41
|
||||
|
||||
|
||||
async def test_reversed_color_temp_min_max(hass: HomeAssistant, hk_driver) -> None:
|
||||
"""Test light with a reversed color temp min max."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||
ATTR_COLOR_MODE: "hs",
|
||||
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||
ATTR_MAX_COLOR_TEMP_KELVIN: 3000,
|
||||
ATTR_MIN_COLOR_TEMP_KELVIN: 4000,
|
||||
ATTR_HS_COLOR: (-1, -1),
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||
hk_driver.add_accessory(acc)
|
||||
|
||||
assert acc.char_color_temp.value == 333
|
||||
assert acc.char_color_temp.properties[PROP_MAX_VALUE] == 333
|
||||
assert acc.char_color_temp.properties[PROP_MIN_VALUE] == 250
|
||||
assert acc.char_hue.value == 31
|
||||
assert acc.char_saturation.value == 95
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||
ATTR_COLOR_MODE: "color_temp",
|
||||
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||
ATTR_COLOR_TEMP_KELVIN: -1,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc.run()
|
||||
|
||||
assert acc.char_color_temp.value == 250
|
||||
assert acc.char_hue.value == 16
|
||||
assert acc.char_saturation.value == 100
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||
ATTR_COLOR_MODE: "color_temp",
|
||||
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||
ATTR_COLOR_TEMP_KELVIN: sys.maxsize,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert acc.char_color_temp.value == 250
|
||||
assert acc.char_hue.value == 220
|
||||
assert acc.char_saturation.value == 41
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||
ATTR_COLOR_MODE: "color_temp",
|
||||
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert acc.char_color_temp.value == 250
|
||||
assert acc.char_hue.value == 220
|
||||
assert acc.char_saturation.value == 41
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"supported_color_modes", [[ColorMode.HS], [ColorMode.RGB], [ColorMode.XY]]
|
||||
)
|
||||
|
@ -26,6 +26,7 @@ from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_STEP,
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DEFAULT_MIN_TEMP,
|
||||
DOMAIN as DOMAIN_CLIMATE,
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
@ -2009,8 +2010,8 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
||||
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
|
||||
ATTR_MAX_TEMP: 50,
|
||||
ATTR_MIN_TEMP: 100,
|
||||
ATTR_MAX_TEMP: 100,
|
||||
ATTR_MIN_TEMP: 50,
|
||||
}
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
@ -2024,14 +2025,14 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
||||
acc.run()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert acc.char_cooling_thresh_temp.value == 100
|
||||
assert acc.char_heating_thresh_temp.value == 100
|
||||
assert acc.char_cooling_thresh_temp.value == 50
|
||||
assert acc.char_heating_thresh_temp.value == 50
|
||||
|
||||
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == 100
|
||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 100
|
||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 50
|
||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == 100
|
||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 100
|
||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 50
|
||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||
|
||||
assert acc.char_target_heat_cool.value == 3
|
||||
@ -2048,7 +2049,7 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_heating_thresh_temp.value == 100.0
|
||||
assert acc.char_heating_thresh_temp.value == 50.0
|
||||
assert acc.char_cooling_thresh_temp.value == 100.0
|
||||
assert acc.char_current_heat_cool.value == 1
|
||||
assert acc.char_target_heat_cool.value == 3
|
||||
@ -2633,3 +2634,44 @@ async def test_thermostat_handles_unknown_state(hass: HomeAssistant, hk_driver)
|
||||
assert call_set_hvac_mode
|
||||
assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVACMode.HEAT
|
||||
|
||||
|
||||
async def test_thermostat_reversed_min_max(hass: HomeAssistant, hk_driver) -> None:
|
||||
"""Test reversed min/max temperatures."""
|
||||
entity_id = "climate.test"
|
||||
base_attrs = {
|
||||
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||
ATTR_HVAC_MODES: [
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT_COOL,
|
||||
HVACMode.FAN_ONLY,
|
||||
HVACMode.COOL,
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
],
|
||||
ATTR_MAX_TEMP: DEFAULT_MAX_TEMP,
|
||||
ATTR_MIN_TEMP: DEFAULT_MIN_TEMP,
|
||||
}
|
||||
# support_auto = True
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
HVACMode.OFF,
|
||||
base_attrs,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
|
||||
hk_driver.add_accessory(acc)
|
||||
|
||||
acc.run()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert acc.char_cooling_thresh_temp.value == 23.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] == 7.0
|
||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||
|
Loading…
x
Reference in New Issue
Block a user