diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index a14e0add488..470bb78874c 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -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 diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 1fc8b3f2430..4dcc6fb8f65 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -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 ) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index a44db05a37b..989e4dd01d3 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -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 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index dc614ee54c4..7bdfd6c5803 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -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 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 5bfbe0b1627..0bea0144506 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -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