mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +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,
|
PROP_MIN_VALUE,
|
||||||
SERV_LIGHTBULB,
|
SERV_LIGHTBULB,
|
||||||
)
|
)
|
||||||
|
from .util import get_min_max
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -120,12 +121,14 @@ class Light(HomeAccessory):
|
|||||||
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
|
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
|
||||||
|
|
||||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
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)
|
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)
|
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:
|
if not self.color_temp_supported and not self.rgbww_supported:
|
||||||
self.max_mireds = self.min_mireds
|
self.max_mireds = self.min_mireds
|
||||||
self.char_color_temp = serv_light.configure_char(
|
self.char_color_temp = serv_light.configure_char(
|
||||||
|
@ -14,6 +14,7 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_HVAC_ACTION,
|
ATTR_HVAC_ACTION,
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
ATTR_HVAC_MODES,
|
ATTR_HVAC_MODES,
|
||||||
|
ATTR_MAX_HUMIDITY,
|
||||||
ATTR_MAX_TEMP,
|
ATTR_MAX_TEMP,
|
||||||
ATTR_MIN_HUMIDITY,
|
ATTR_MIN_HUMIDITY,
|
||||||
ATTR_MIN_TEMP,
|
ATTR_MIN_TEMP,
|
||||||
@ -21,6 +22,7 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_SWING_MODES,
|
ATTR_SWING_MODES,
|
||||||
ATTR_TARGET_TEMP_HIGH,
|
ATTR_TARGET_TEMP_HIGH,
|
||||||
ATTR_TARGET_TEMP_LOW,
|
ATTR_TARGET_TEMP_LOW,
|
||||||
|
DEFAULT_MAX_HUMIDITY,
|
||||||
DEFAULT_MAX_TEMP,
|
DEFAULT_MAX_TEMP,
|
||||||
DEFAULT_MIN_HUMIDITY,
|
DEFAULT_MIN_HUMIDITY,
|
||||||
DEFAULT_MIN_TEMP,
|
DEFAULT_MIN_TEMP,
|
||||||
@ -90,7 +92,7 @@ from .const import (
|
|||||||
SERV_FANV2,
|
SERV_FANV2,
|
||||||
SERV_THERMOSTAT,
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -208,7 +210,10 @@ class Thermostat(HomeAccessory):
|
|||||||
self.fan_chars: list[str] = []
|
self.fan_chars: list[str] = []
|
||||||
|
|
||||||
attributes = state.attributes
|
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)
|
features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
if features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
if features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||||
@ -839,6 +844,9 @@ def _get_temperature_range_from_state(
|
|||||||
else:
|
else:
|
||||||
max_temp = default_max
|
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
|
# Homekit only supports 10-38, overwriting
|
||||||
# the max to appears to work, but less than 0 causes
|
# the max to appears to work, but less than 0 causes
|
||||||
# a crash on the home app
|
# 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"]
|
old_state = event_data["old_state"]
|
||||||
new_state = event_data["new_state"]
|
new_state = event_data["new_state"]
|
||||||
return bool(new_state and old_state and new_state.state == old_state.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
|
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(
|
@pytest.mark.parametrize(
|
||||||
"supported_color_modes", [[ColorMode.HS], [ColorMode.RGB], [ColorMode.XY]]
|
"supported_color_modes", [[ColorMode.HS], [ColorMode.RGB], [ColorMode.XY]]
|
||||||
)
|
)
|
||||||
|
@ -26,6 +26,7 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_TARGET_TEMP_STEP,
|
ATTR_TARGET_TEMP_STEP,
|
||||||
DEFAULT_MAX_TEMP,
|
DEFAULT_MAX_TEMP,
|
||||||
DEFAULT_MIN_HUMIDITY,
|
DEFAULT_MIN_HUMIDITY,
|
||||||
|
DEFAULT_MIN_TEMP,
|
||||||
DOMAIN as DOMAIN_CLIMATE,
|
DOMAIN as DOMAIN_CLIMATE,
|
||||||
FAN_AUTO,
|
FAN_AUTO,
|
||||||
FAN_HIGH,
|
FAN_HIGH,
|
||||||
@ -2009,8 +2010,8 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
|||||||
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||||
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
|
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
|
||||||
ATTR_MAX_TEMP: 50,
|
ATTR_MAX_TEMP: 100,
|
||||||
ATTR_MIN_TEMP: 100,
|
ATTR_MIN_TEMP: 50,
|
||||||
}
|
}
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
entity_id,
|
entity_id,
|
||||||
@ -2024,14 +2025,14 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
|||||||
acc.run()
|
acc.run()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert acc.char_cooling_thresh_temp.value == 100
|
assert acc.char_cooling_thresh_temp.value == 50
|
||||||
assert acc.char_heating_thresh_temp.value == 100
|
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_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_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_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_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
|
||||||
assert acc.char_target_heat_cool.value == 3
|
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()
|
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_cooling_thresh_temp.value == 100.0
|
||||||
assert acc.char_current_heat_cool.value == 1
|
assert acc.char_current_heat_cool.value == 1
|
||||||
assert acc.char_target_heat_cool.value == 3
|
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
|
||||||
assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id
|
assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id
|
||||||
assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVACMode.HEAT
|
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