Handle unknown state in HomeKit (#107039)

This commit is contained in:
J. Nick Koston 2024-01-07 22:42:28 -10:00 committed by GitHub
parent 40e1bab0ac
commit ea4143154b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 157 additions and 8 deletions

View File

@ -36,6 +36,7 @@ from homeassistant.const import (
PERCENTAGE,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
__version__,
)
@ -506,7 +507,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
_LOGGER.debug("New_state: %s", new_state)
# HomeKit handles unavailable state via the available property
# so we should not propagate it here
if new_state is None or new_state.state == STATE_UNAVAILABLE:
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return
battery_state = None
battery_charging_state = None

View File

@ -54,6 +54,8 @@ from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.core import State, callback
@ -167,7 +169,9 @@ HEAT_COOL_DEADBAND = 5
def _hk_hvac_mode_from_state(state: State) -> int | None:
"""Return the equivalent HomeKit HVAC mode for a given state."""
if not (hvac_mode := try_parse_enum(HVACMode, state.state)):
if (current_state := state.state) in (STATE_UNKNOWN, STATE_UNAVAILABLE):
return None
if not (hvac_mode := try_parse_enum(HVACMode, current_state)):
_LOGGER.error(
"%s: Received invalid HVAC mode: %s", state.entity_id, state.state
)

View File

@ -368,22 +368,22 @@ async def test_windowcovering_cover_set_tilt(
assert acc.char_current_tilt.value == 0
assert acc.char_target_tilt.value == 0
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: None})
hass.states.async_set(entity_id, STATE_CLOSING, {ATTR_CURRENT_TILT_POSITION: None})
await hass.async_block_till_done()
assert acc.char_current_tilt.value == 0
assert acc.char_target_tilt.value == 0
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 100})
hass.states.async_set(entity_id, STATE_CLOSING, {ATTR_CURRENT_TILT_POSITION: 100})
await hass.async_block_till_done()
assert acc.char_current_tilt.value == 90
assert acc.char_target_tilt.value == 90
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 50})
hass.states.async_set(entity_id, STATE_CLOSING, {ATTR_CURRENT_TILT_POSITION: 50})
await hass.async_block_till_done()
assert acc.char_current_tilt.value == 0
assert acc.char_target_tilt.value == 0
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 0})
hass.states.async_set(entity_id, STATE_CLOSING, {ATTR_CURRENT_TILT_POSITION: 0})
await hass.async_block_till_done()
assert acc.char_current_tilt.value == -90
assert acc.char_target_tilt.value == -90

View File

@ -66,14 +66,14 @@ async def test_lock_unlock(hass: HomeAssistant, hk_driver, events) -> None:
hass.states.async_set(entity_id, STATE_UNKNOWN)
await hass.async_block_till_done()
assert acc.char_current_state.value == 3
assert acc.char_current_state.value == 2
assert acc.char_target_state.value == 0
# Unavailable should keep last state
# but set the accessory to not available
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
await hass.async_block_till_done()
assert acc.char_current_state.value == 3
assert acc.char_current_state.value == 2
assert acc.char_target_state.value == 0
assert acc.available is False

View File

@ -1,6 +1,7 @@
"""Test different accessory types: Thermostats."""
from unittest.mock import patch
from pyhap.characteristic import Characteristic
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
import pytest
@ -68,6 +69,8 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_TEMPERATURE_UNIT,
EVENT_HOMEASSISTANT_START,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.core import CoreState, HomeAssistant
@ -2446,3 +2449,144 @@ async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set(
assert acc.ordered_fan_speeds == []
assert not acc.fan_chars
async def test_thermostat_handles_unknown_state(
hass: HomeAssistant, hk_driver, events
) -> None:
"""Test a thermostat can handle unknown state."""
entity_id = "climate.test"
attrs = {
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE,
ATTR_MIN_TEMP: 44.6,
ATTR_MAX_TEMP: 95,
ATTR_PRESET_MODES: ["home", "away"],
ATTR_TEMPERATURE: 67,
ATTR_TARGET_TEMP_HIGH: None,
ATTR_TARGET_TEMP_LOW: None,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_FAN_MODES: None,
ATTR_HVAC_ACTION: HVACAction.IDLE,
ATTR_PRESET_MODE: "home",
ATTR_FRIENDLY_NAME: "Rec Room",
ATTR_HVAC_MODES: [
HVACMode.OFF,
HVACMode.HEAT,
],
}
call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode")
hass.states.async_set(
entity_id,
HVACMode.OFF,
attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
heat_cool_char: Characteristic = acc.char_target_heat_cool
assert heat_cool_char.value == HC_HEAT_COOL_OFF
assert acc.available is True
hass.states.async_set(
entity_id,
STATE_UNKNOWN,
attrs,
)
await hass.async_block_till_done()
assert heat_cool_char.value == HC_HEAT_COOL_OFF
assert acc.available is True
hass.states.async_set(
entity_id,
HVACMode.OFF,
attrs,
)
await hass.async_block_till_done()
assert heat_cool_char.value == HC_HEAT_COOL_OFF
assert acc.available is True
hass.states.async_set(
entity_id,
STATE_UNAVAILABLE,
attrs,
)
await hass.async_block_till_done()
assert heat_cool_char.value == HC_HEAT_COOL_OFF
assert acc.available is False
hass.states.async_set(
entity_id,
HVACMode.OFF,
attrs,
)
await hass.async_block_till_done()
assert heat_cool_char.value == HC_HEAT_COOL_OFF
assert acc.available is True
hass.states.async_set(
entity_id,
STATE_UNAVAILABLE,
attrs,
)
await hass.async_block_till_done()
assert heat_cool_char.value == HC_HEAT_COOL_OFF
assert acc.available is False
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: heat_cool_char.to_HAP()[HAP_REPR_IID],
HAP_REPR_VALUE: HC_HEAT_COOL_HEAT,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert heat_cool_char.value == HC_HEAT_COOL_HEAT
assert acc.available is False
assert call_set_hvac_mode
assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.HEAT
hass.states.async_set(
entity_id,
STATE_UNKNOWN,
attrs,
)
await hass.async_block_till_done()
assert heat_cool_char.value == HC_HEAT_COOL_HEAT
assert acc.available is True
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: heat_cool_char.to_HAP()[HAP_REPR_IID],
HAP_REPR_VALUE: HC_HEAT_COOL_HEAT,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert heat_cool_char.value == HC_HEAT_COOL_HEAT
assert acc.available is True
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