Refactor HomeKit to allow supported features/device class to change (#101719)

This commit is contained in:
J. Nick Koston 2023-10-10 06:20:25 -10:00 committed by GitHub
parent f166e1cc1a
commit 7b4b8e7516
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 662 additions and 452 deletions

View File

@ -47,7 +47,14 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
)
from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State, callback
from homeassistant.core import (
CALLBACK_TYPE,
CoreState,
HomeAssistant,
ServiceCall,
State,
callback,
)
from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.helpers import (
config_validation as cv,
@ -55,6 +62,7 @@ from homeassistant.helpers import (
entity_registry as er,
instance_id,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entityfilter import (
BASE_FILTER_SCHEMA,
FILTER_SCHEMA,
@ -534,6 +542,7 @@ class HomeKit:
self.driver: HomeDriver | None = None
self.bridge: HomeBridge | None = None
self._reset_lock = asyncio.Lock()
self._cancel_reload_dispatcher: CALLBACK_TYPE | None = None
def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None:
"""Set up bridge and accessory driver."""
@ -563,16 +572,28 @@ class HomeKit:
async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None:
"""Reset the accessory to load the latest configuration."""
_LOGGER.debug("Resetting accessories: %s", entity_ids)
async with self._reset_lock:
if not self.bridge:
await self.async_reset_accessories_in_accessory_mode(entity_ids)
# For accessory mode reset and reload are the same
await self._async_reload_accessories_in_accessory_mode(entity_ids)
return
await self.async_reset_accessories_in_bridge_mode(entity_ids)
await self._async_reset_accessories_in_bridge_mode(entity_ids)
async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
async def async_reload_accessories(self, entity_ids: Iterable[str]) -> None:
"""Reload the accessory to load the latest configuration."""
_LOGGER.debug("Reloading accessories: %s", entity_ids)
async with self._reset_lock:
if not self.bridge:
await self._async_reload_accessories_in_accessory_mode(entity_ids)
return
await self._async_reload_accessories_in_bridge_mode(entity_ids)
@callback
def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
"""Shutdown an accessory."""
assert self.driver is not None
await accessory.stop()
accessory.async_stop()
# Deallocate the IIDs for the accessory
iid_manager = accessory.iid_manager
services: list[Service] = accessory.services
@ -582,7 +603,7 @@ class HomeKit:
for char in characteristics:
iid_manager.remove_obj(char)
async def async_reset_accessories_in_accessory_mode(
async def _async_reload_accessories_in_accessory_mode(
self, entity_ids: Iterable[str]
) -> None:
"""Reset accessories in accessory mode."""
@ -593,63 +614,88 @@ class HomeKit:
return
if not (state := self.hass.states.get(acc.entity_id)):
_LOGGER.warning(
"The underlying entity %s disappeared during reset", acc.entity_id
"The underlying entity %s disappeared during reload", acc.entity_id
)
return
await self._async_shutdown_accessory(acc)
self._async_shutdown_accessory(acc)
if new_acc := self._async_create_single_accessory([state]):
self.driver.accessory = new_acc
self.hass.async_create_task(
new_acc.run(), f"HomeKit Bridge Accessory: {new_acc.entity_id}"
)
await self.async_config_changed()
# Run must be awaited here since it may change
# the accessories hash
await new_acc.run()
self._async_update_accessories_hash()
async def async_reset_accessories_in_bridge_mode(
def _async_remove_accessories_by_entity_id(
self, entity_ids: Iterable[str]
) -> None:
"""Reset accessories in bridge mode."""
) -> list[str]:
"""Remove accessories by entity id."""
assert self.aid_storage is not None
assert self.bridge is not None
assert self.driver is not None
new = []
removed: list[str] = []
acc: HomeAccessory | None
for entity_id in entity_ids:
aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
if aid not in self.bridge.accessories:
continue
_LOGGER.info(
"HomeKit Bridge %s will reset accessory with linked entity_id %s",
self._name,
entity_id,
)
acc = await self.async_remove_bridge_accessory(aid)
if acc:
await self._async_shutdown_accessory(acc)
if acc and (state := self.hass.states.get(acc.entity_id)):
new.append(state)
else:
_LOGGER.warning(
"The underlying entity %s disappeared during reset", entity_id
)
if acc := self.async_remove_bridge_accessory(aid):
self._async_shutdown_accessory(acc)
removed.append(entity_id)
return removed
if not new:
# No matched accessories, probably on another bridge
async def _async_reset_accessories_in_bridge_mode(
self, entity_ids: Iterable[str]
) -> None:
"""Reset accessories in bridge mode."""
if not (removed := self._async_remove_accessories_by_entity_id(entity_ids)):
_LOGGER.debug("No accessories to reset in bridge mode for: %s", entity_ids)
return
await self.async_config_changed()
await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME)
for state in new:
if acc := self.add_bridge_accessory(state):
self.hass.async_create_task(
acc.run(), f"HomeKit Bridge Accessory: {acc.entity_id}"
)
await self.async_config_changed()
async def async_config_changed(self) -> None:
"""Call config changed which writes out the new config to disk."""
# With a reset, we need to remove the accessories,
# and force config change so iCloud deletes them from
# the database.
assert self.driver is not None
await self.hass.async_add_executor_job(self.driver.config_changed)
self._async_update_accessories_hash()
await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME)
await self._async_recreate_removed_accessories_in_bridge_mode(removed)
async def _async_reload_accessories_in_bridge_mode(
self, entity_ids: Iterable[str]
) -> None:
"""Reload accessories in bridge mode."""
removed = self._async_remove_accessories_by_entity_id(entity_ids)
await self._async_recreate_removed_accessories_in_bridge_mode(removed)
async def _async_recreate_removed_accessories_in_bridge_mode(
self, removed: list[str]
) -> None:
"""Recreate removed accessories in bridge mode."""
for entity_id in removed:
if not (state := self.hass.states.get(entity_id)):
_LOGGER.warning(
"The underlying entity %s disappeared during reload", entity_id
)
continue
if acc := self.add_bridge_accessory(state):
# Run must be awaited here since it may change
# the accessories hash
await acc.run()
self._async_update_accessories_hash()
@callback
def _async_update_accessories_hash(self) -> bool:
"""Update the accessories hash."""
assert self.driver is not None
driver = self.driver
old_hash = driver.state.accessories_hash
new_hash = driver.accessories_hash
if driver.state.set_accessories_hash(new_hash):
_LOGGER.debug(
"Updating HomeKit accessories hash from %s -> %s", old_hash, new_hash
)
driver.async_persist()
driver.async_update_advertisement()
return True
_LOGGER.debug("HomeKit accessories hash is unchanged: %s", new_hash)
return False
def add_bridge_accessory(self, state: State) -> HomeAccessory | None:
"""Try adding accessory to bridge if configured beforehand."""
@ -734,7 +780,8 @@ class HomeKit:
)
)
async def async_remove_bridge_accessory(self, aid: int) -> HomeAccessory | None:
@callback
def async_remove_bridge_accessory(self, aid: int) -> HomeAccessory | None:
"""Try adding accessory to bridge if configured beforehand."""
assert self.bridge is not None
if acc := self.bridge.accessories.pop(aid, None):
@ -782,6 +829,11 @@ class HomeKit:
if self.status != STATUS_READY:
return
self.status = STATUS_WAIT
self._cancel_reload_dispatcher = async_dispatcher_connect(
self.hass,
f"homekit_reload_entities_{self._entry_id}",
self.async_reload_accessories,
)
async_zc_instance = await zeroconf.async_get_async_instance(self.hass)
uuid = await instance_id.async_get(self.hass)
self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id)
@ -989,10 +1041,13 @@ class HomeKit:
"""Stop the accessory driver."""
if self.status != STATUS_RUNNING:
return
self.status = STATUS_STOPPED
_LOGGER.debug("Driver stop for %s", self._name)
if self.driver:
await self.driver.async_stop()
async with self._reset_lock:
self.status = STATUS_STOPPED
assert self._cancel_reload_dispatcher is not None
self._cancel_reload_dispatcher()
_LOGGER.debug("Driver stop for %s", self._name)
if self.driver:
await self.driver.async_stop()
@callback
def _async_configure_linked_sensors(

View File

@ -47,6 +47,7 @@ from homeassistant.core import (
callback as ha_callback,
split_entity_id,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import (
EventStateChangedData,
async_track_state_change_event,
@ -69,7 +70,6 @@ from .const import (
CONF_LINKED_BATTERY_SENSOR,
CONF_LOW_BATTERY_THRESHOLD,
DEFAULT_LOW_BATTERY_THRESHOLD,
DOMAIN,
EVENT_HOMEKIT_CHANGED,
HK_CHARGING,
HK_NOT_CHARGABLE,
@ -81,7 +81,6 @@ from .const import (
MAX_VERSION_LENGTH,
SERV_ACCESSORY_INFO,
SERV_BATTERY_SERVICE,
SERVICE_HOMEKIT_RESET_ACCESSORY,
TYPE_FAUCET,
TYPE_OUTLET,
TYPE_SHOWER,
@ -111,6 +110,12 @@ SWITCH_TYPES = {
}
TYPES: Registry[str, type[HomeAccessory]] = Registry()
RELOAD_ON_CHANGE_ATTRS = (
ATTR_SUPPORTED_FEATURES,
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
)
def get_accessory( # noqa: C901
hass: HomeAssistant, driver: HomeDriver, state: State, aid: int | None, config: dict
@ -272,6 +277,8 @@ def get_accessory( # noqa: C901
class HomeAccessory(Accessory): # type: ignore[misc]
"""Adapter class for Accessory."""
driver: HomeDriver
def __init__(
self,
hass: HomeAssistant,
@ -294,6 +301,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
*args, # noqa: B026
**kwargs,
)
self._reload_on_change_attrs = list(RELOAD_ON_CHANGE_ATTRS)
self.config = config or {}
if device_id:
self.device_id: str | None = device_id
@ -464,7 +472,27 @@ class HomeAccessory(Accessory): # type: ignore[misc]
self, event: EventType[EventStateChangedData]
) -> None:
"""Handle state change event listener callback."""
self.async_update_state_callback(event.data["new_state"])
new_state = event.data["new_state"]
old_state = event.data["old_state"]
if (
new_state
and old_state
and STATE_UNAVAILABLE not in (old_state.state, new_state.state)
):
old_attributes = old_state.attributes
new_attributes = new_state.attributes
for attr in self._reload_on_change_attrs:
if old_attributes.get(attr) != new_attributes.get(attr):
_LOGGER.debug(
"%s: Reloading HomeKit accessory since %s has changed from %s -> %s",
self.entity_id,
attr,
old_attributes.get(attr),
new_attributes.get(attr),
)
self.async_reload()
return
self.async_update_state_callback(new_state)
@ha_callback
def async_update_state_callback(self, new_state: State | None) -> None:
@ -577,21 +605,30 @@ class HomeAccessory(Accessory): # type: ignore[misc]
)
@ha_callback
def async_reset(self) -> None:
"""Reset and recreate an accessory."""
self.hass.async_create_task(
self.hass.services.async_call(
DOMAIN,
SERVICE_HOMEKIT_RESET_ACCESSORY,
{ATTR_ENTITY_ID: self.entity_id},
)
def async_reload(self) -> None:
"""Reload and recreate an accessory and update the c# value in the mDNS record."""
async_dispatcher_send(
self.hass,
f"homekit_reload_entities_{self.driver.entry_id}",
(self.entity_id,),
)
async def stop(self) -> None:
@ha_callback
def async_stop(self) -> None:
"""Cancel any subscriptions when the bridge is stopped."""
while self._subscriptions:
self._subscriptions.pop(0)()
async def stop(self) -> None:
"""Stop the accessory.
This is overrides the parent class to call async_stop
since pyhap will call this function to stop the accessory
but we want to use our async_stop method since we need
it to be a callback to avoid races in reloading accessories.
"""
self.async_stop()
class HomeBridge(Bridge): # type: ignore[misc]
"""Adapter class for Bridge."""
@ -637,7 +674,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
"""Initialize a AccessoryDriver object."""
super().__init__(**kwargs)
self.hass = hass
self._entry_id = entry_id
self.entry_id = entry_id
self._bridge_name = bridge_name
self._entry_title = entry_title
self.iid_storage = iid_storage
@ -649,7 +686,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
"""Override super function to dismiss setup message if paired."""
success = super().pair(client_username_bytes, client_public, client_permissions)
if success:
async_dismiss_setup_message(self.hass, self._entry_id)
async_dismiss_setup_message(self.hass, self.entry_id)
return cast(bool, success)
@pyhap_callback # type: ignore[misc]
@ -662,7 +699,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
async_show_setup_message(
self.hass,
self._entry_id,
self.entry_id,
accessory_friendly_name(self._entry_title, self.accessory),
self.state.pincode,
self.accessory.xhm_uri(),

View File

@ -447,13 +447,14 @@ class Camera(HomeAccessory, PyhapCamera):
self.sessions[session_id].pop(FFMPEG_WATCHER)()
self.sessions[session_id].pop(FFMPEG_LOGGER).cancel()
async def stop(self):
@callback
def async_stop(self):
"""Stop any streams when the accessory is stopped."""
for session_info in self.sessions.values():
self.hass.async_create_background_task(
self.stop_stream(session_info), "homekit.camera-stop-stream"
)
await super().stop()
super().async_stop()
async def stop_stream(self, session_info):
"""Stop the stream for the given ``session_id``."""

View File

@ -60,6 +60,13 @@ class Fan(HomeAccessory):
self.chars = []
state = self.hass.states.get(self.entity_id)
self._reload_on_change_attrs.extend(
(
ATTR_PERCENTAGE_STEP,
ATTR_PRESET_MODES,
)
)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
percentage_step = state.attributes.get(ATTR_PERCENTAGE_STEP, 1)
self.preset_modes = state.attributes.get(ATTR_PRESET_MODES)

View File

@ -76,6 +76,13 @@ class HumidifierDehumidifier(HomeAccessory):
def __init__(self, *args):
"""Initialize a HumidifierDehumidifier accessory object."""
super().__init__(*args, category=CATEGORY_HUMIDIFIER)
self._reload_on_change_attrs.extend(
(
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
)
)
self.chars = []
state = self.hass.states.get(self.entity_id)
device_class = state.attributes.get(

View File

@ -71,7 +71,13 @@ class Light(HomeAccessory):
def __init__(self, *args):
"""Initialize a new Light accessory object."""
super().__init__(*args, category=CATEGORY_LIGHTBULB)
self._reload_on_change_attrs.extend(
(
ATTR_SUPPORTED_COLOR_MODES,
ATTR_MAX_COLOR_TEMP_KELVIN,
ATTR_MIN_COLOR_TEMP_KELVIN,
)
)
self.chars = []
self._event_timer = None
self._pending_events = {}

View File

@ -93,7 +93,7 @@ class RemoteInputSelectAccessory(HomeAccessory, ABC):
state = self.hass.states.get(self.entity_id)
assert state
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self._reload_on_change_attrs.extend((source_list_key,))
self._mapped_sources_list: list[str] = []
self._mapped_sources: dict[str, str] = {}
self.source_key = source_key
@ -204,8 +204,6 @@ class RemoteInputSelectAccessory(HomeAccessory, ABC):
"%s: Sources out of sync. Rebuilding Accessory",
self.entity_id,
)
# Sources are out of sync, recreate the accessory
self.async_reset()
return
_LOGGER.debug(

View File

@ -174,6 +174,15 @@ class Thermostat(HomeAccessory):
self.hc_homekit_to_hass = None
self.hc_hass_to_homekit = None
hc_min_temp, hc_max_temp = self.get_temperature_range()
self._reload_on_change_attrs.extend(
(
ATTR_MIN_HUMIDITY,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_FAN_MODES,
ATTR_HVAC_MODES,
)
)
# Add additional characteristics if auto mode is supported
self.chars = []
@ -345,7 +354,7 @@ class Thermostat(HomeAccessory):
)
self.char_target_fan_state.display_name = "Fan Auto"
self._async_update_state(state)
self.async_update_state(state)
serv_thermostat.setter_callback = self._set_chars
@ -577,29 +586,6 @@ class Thermostat(HomeAccessory):
@callback
def async_update_state(self, new_state):
"""Update thermostat state after state changed."""
# We always recheck valid hvac modes as the entity
# may not have been fully setup when we saw it last
original_hc_hass_to_homekit = self.hc_hass_to_homekit
self._configure_hvac_modes(new_state)
if self.hc_hass_to_homekit != original_hc_hass_to_homekit:
if self.char_target_heat_cool.value not in self.hc_homekit_to_hass:
# We must make sure the char value is
# in the new valid values before
# setting the new valid values or
# changing them with throw
self.char_target_heat_cool.set_value(
list(self.hc_homekit_to_hass)[0], should_notify=False
)
self.char_target_heat_cool.override_properties(
valid_values=self.hc_hass_to_homekit
)
self._async_update_state(new_state)
@callback
def _async_update_state(self, new_state):
"""Update state without rechecking the device features."""
attributes = new_state.attributes
features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
@ -727,6 +713,12 @@ class WaterHeater(HomeAccessory):
def __init__(self, *args):
"""Initialize a WaterHeater accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._reload_on_change_attrs.extend(
(
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
)
)
self._unit = self.hass.config.units.temperature_unit
min_temp, max_temp = self.get_temperature_range()

View File

@ -6,7 +6,7 @@ from typing import Any
from pyhap.const import CATEGORY_SENSOR
from homeassistant.core import CALLBACK_TYPE, Context
from homeassistant.core import CALLBACK_TYPE, Context, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.trigger import async_initialize_triggers
@ -112,10 +112,12 @@ class DeviceTriggerAccessory(HomeAccessory):
_LOGGER.log,
)
async def stop(self) -> None:
@callback
def async_stop(self) -> None:
"""Handle accessory driver stop event."""
if self._remove_triggers:
self._remove_triggers()
super().async_stop()
@property
def available(self) -> bool:

View File

@ -36,7 +36,13 @@ from homeassistant.components.homekit.const import (
)
from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory
from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id
from homeassistant.components.light import (
ATTR_COLOR_MODE,
ATTR_SUPPORTED_COLOR_MODES,
ColorMode,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_ZEROCONF
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@ -532,7 +538,7 @@ async def test_homekit_remove_accessory(
acc_mock.stop = AsyncMock()
homekit.bridge.accessories = {6: acc_mock}
acc = await homekit.async_remove_bridge_accessory(6)
acc = homekit.async_remove_bridge_accessory(6)
assert acc is acc_mock
assert len(homekit.bridge.accessories) == 0
@ -876,6 +882,7 @@ async def test_homekit_stop(hass: HomeAssistant) -> None:
# Test if driver is started
homekit.status = STATUS_RUNNING
homekit._cancel_reload_dispatcher = lambda: None
await homekit.async_stop()
await hass.async_block_till_done()
assert homekit.driver.async_stop.called is True
@ -919,6 +926,120 @@ async def test_homekit_reset_accessories(
await homekit.async_stop()
async def test_homekit_reload_accessory_can_change_class(
hass: HomeAssistant, mock_async_zeroconf: None, mock_hap
) -> None:
"""Test reloading a HomeKit Accessory in brdige mode.
This test ensure when device class changes the HomeKit class changes.
"""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_id = "switch.outlet"
hass.states.async_set(entity_id, "on", {ATTR_DEVICE_CLASS: None})
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit):
await async_init_entry(hass, entry)
bridge: HomeBridge = homekit.driver.accessory
await bridge.run()
switch_accessory = next(iter(bridge.accessories.values()))
assert type(switch_accessory).__name__ == "Switch"
await hass.async_block_till_done()
assert homekit.status == STATUS_RUNNING
homekit.driver.aio_stop_event = MagicMock()
hass.states.async_set(
entity_id, "off", {ATTR_DEVICE_CLASS: SwitchDeviceClass.OUTLET}
)
await hass.async_block_till_done()
await hass.async_block_till_done()
outlet_accessory = next(iter(bridge.accessories.values()))
assert type(outlet_accessory).__name__ == "Outlet"
await homekit.async_stop()
async def test_homekit_reload_accessory_in_accessory_mode(
hass: HomeAssistant, mock_async_zeroconf: None, mock_hap
) -> None:
"""Test reloading a HomeKit Accessory in accessory mode.
This test ensure a device class changes can change the class of
the accessory.
"""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_id = "switch.outlet"
hass.states.async_set(entity_id, "on", {ATTR_DEVICE_CLASS: None})
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit):
await async_init_entry(hass, entry)
primary_accessory = homekit.driver.accessory
await primary_accessory.run()
assert type(primary_accessory).__name__ == "Switch"
await hass.async_block_till_done()
assert homekit.status == STATUS_RUNNING
homekit.driver.aio_stop_event = MagicMock()
hass.states.async_set(
entity_id, "off", {ATTR_DEVICE_CLASS: SwitchDeviceClass.OUTLET}
)
await hass.async_block_till_done()
await hass.async_block_till_done()
primary_accessory = homekit.driver.accessory
assert type(primary_accessory).__name__ == "Outlet"
await homekit.async_stop()
async def test_homekit_reload_accessory_same_class(
hass: HomeAssistant, mock_async_zeroconf: None, mock_hap
) -> None:
"""Test reloading a HomeKit Accessory in bridge mode.
The class of the accessory remains the same.
"""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_id = "light.color"
hass.states.async_set(
entity_id,
"on",
{ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_COLOR_MODE: ColorMode.HS},
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit):
await async_init_entry(hass, entry)
bridge: HomeBridge = homekit.driver.accessory
await bridge.run()
light_accessory_color = next(iter(bridge.accessories.values()))
assert not hasattr(light_accessory_color, "char_color_temp")
await hass.async_block_till_done()
assert homekit.status == STATUS_RUNNING
homekit.driver.aio_stop_event = MagicMock()
hass.states.async_set(
entity_id,
"on",
{
ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS, ColorMode.COLOR_TEMP],
ATTR_COLOR_MODE: ColorMode.COLOR_TEMP,
},
)
await hass.async_block_till_done()
await hass.async_block_till_done()
light_accessory_color_and_temp = next(iter(bridge.accessories.values()))
assert hasattr(light_accessory_color_and_temp, "char_color_temp")
await homekit.async_stop()
async def test_homekit_unpair(
hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_async_zeroconf: None
) -> None:
@ -1076,8 +1197,8 @@ async def test_homekit_reset_accessories_not_supported(
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
"pyhap.accessory.Bridge.add_accessory"
) as mock_add_accessory, patch(
"pyhap.accessory_driver.AccessoryDriver.config_changed"
) as hk_driver_config_changed, patch(
"pyhap.accessory_driver.AccessoryDriver.async_update_advertisement"
) as hk_driver_async_update_advertisement, patch(
"pyhap.accessory_driver.AccessoryDriver.async_start"
), patch.object(
homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0
@ -1101,7 +1222,7 @@ async def test_homekit_reset_accessories_not_supported(
)
await hass.async_block_till_done()
assert hk_driver_config_changed.call_count == 2
assert hk_driver_async_update_advertisement.call_count == 1
assert not mock_add_accessory.called
assert len(homekit.bridge.accessories) == 0
homekit.status = STATUS_STOPPED
@ -1165,22 +1286,25 @@ async def test_homekit_reset_accessories_not_bridged(
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
"pyhap.accessory.Bridge.add_accessory"
) as mock_add_accessory, patch(
"pyhap.accessory_driver.AccessoryDriver.config_changed"
) as hk_driver_config_changed, patch(
"pyhap.accessory_driver.AccessoryDriver.async_update_advertisement"
) as hk_driver_async_update_advertisement, patch(
"pyhap.accessory_driver.AccessoryDriver.async_start"
), patch.object(
homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0
):
await async_init_entry(hass, entry)
assert hk_driver_async_update_advertisement.call_count == 0
acc_mock = MagicMock()
acc_mock.entity_id = entity_id
acc_mock.stop = AsyncMock()
acc_mock.to_HAP = lambda: {}
aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
homekit.bridge.accessories = {aid: acc_mock}
homekit.status = STATUS_RUNNING
homekit.driver.aio_stop_event = MagicMock()
assert hk_driver_async_update_advertisement.call_count == 0
await hass.services.async_call(
DOMAIN,
@ -1190,7 +1314,7 @@ async def test_homekit_reset_accessories_not_bridged(
)
await hass.async_block_till_done()
assert hk_driver_config_changed.call_count == 0
assert hk_driver_async_update_advertisement.call_count == 0
assert not mock_add_accessory.called
homekit.status = STATUS_STOPPED
@ -1208,8 +1332,8 @@ async def test_homekit_reset_single_accessory(
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
"pyhap.accessory_driver.AccessoryDriver.config_changed"
) as hk_driver_config_changed, patch(
"pyhap.accessory_driver.AccessoryDriver.async_update_advertisement"
) as hk_driver_async_update_advertisement, patch(
"pyhap.accessory_driver.AccessoryDriver.async_start"
), patch(
f"{PATH_HOMEKIT}.accessories.HomeAccessory.run"
@ -1226,7 +1350,7 @@ async def test_homekit_reset_single_accessory(
)
await hass.async_block_till_done()
assert mock_run.called
assert hk_driver_config_changed.call_count == 1
assert hk_driver_async_update_advertisement.call_count == 1
homekit.status = STATUS_READY
await homekit.async_stop()

View File

@ -129,7 +129,14 @@ async def test_fan_direction(hass: HomeAssistant, hk_driver, events) -> None:
await hass.async_block_till_done()
assert acc.char_direction.value == 0
hass.states.async_set(entity_id, STATE_ON, {ATTR_DIRECTION: DIRECTION_REVERSE})
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_FEATURES: FanEntityFeature.DIRECTION,
ATTR_DIRECTION: DIRECTION_REVERSE,
},
)
await hass.async_block_till_done()
assert acc.char_direction.value == 1
@ -197,7 +204,11 @@ async def test_fan_oscillate(hass: HomeAssistant, hk_driver, events) -> None:
await hass.async_block_till_done()
assert acc.char_swing.value == 0
hass.states.async_set(entity_id, STATE_ON, {ATTR_OSCILLATING: True})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_FEATURES: FanEntityFeature.OSCILLATE, ATTR_OSCILLATING: True},
)
await hass.async_block_till_done()
assert acc.char_swing.value == 1
@ -272,7 +283,15 @@ async def test_fan_speed(hass: HomeAssistant, hk_driver, events) -> None:
await acc.run()
await hass.async_block_till_done()
hass.states.async_set(entity_id, STATE_ON, {ATTR_PERCENTAGE: 100})
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_PERCENTAGE_STEP: 25,
ATTR_SUPPORTED_FEATURES: FanEntityFeature.SET_SPEED,
ATTR_PERCENTAGE: 100,
},
)
await hass.async_block_till_done()
assert acc.char_speed.value == 100
@ -306,7 +325,15 @@ async def test_fan_speed(hass: HomeAssistant, hk_driver, events) -> None:
assert events[-1].data[ATTR_VALUE] == 42
# Verify speed is preserved from off to on
hass.states.async_set(entity_id, STATE_OFF, {ATTR_PERCENTAGE: 42})
hass.states.async_set(
entity_id,
STATE_OFF,
{
ATTR_PERCENTAGE_STEP: 25,
ATTR_SUPPORTED_FEATURES: FanEntityFeature.SET_SPEED,
ATTR_PERCENTAGE: 42,
},
)
await hass.async_block_till_done()
assert acc.char_speed.value == 50
assert acc.char_active.value == 0

View File

@ -48,7 +48,9 @@ async def test_humidifier(hass: HomeAssistant, hk_driver, events) -> None:
"""Test if humidifier accessory and HA are updated accordingly."""
entity_id = "humidifier.test"
hass.states.async_set(entity_id, STATE_OFF)
hass.states.async_set(
entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: HumidifierDeviceClass.HUMIDIFIER}
)
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass, hk_driver, "HumidifierDehumidifier", entity_id, 1, None
@ -77,7 +79,7 @@ async def test_humidifier(hass: HomeAssistant, hk_driver, events) -> None:
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_HUMIDITY: 47},
{ATTR_HUMIDITY: 47, ATTR_DEVICE_CLASS: HumidifierDeviceClass.HUMIDIFIER},
)
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 47.0
@ -158,7 +160,7 @@ async def test_dehumidifier(hass: HomeAssistant, hk_driver, events) -> None:
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_HUMIDITY: 30},
{ATTR_HUMIDITY: 30, ATTR_DEVICE_CLASS: HumidifierDeviceClass.DEHUMIDIFIER},
)
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 30.0
@ -169,7 +171,7 @@ async def test_dehumidifier(hass: HomeAssistant, hk_driver, events) -> None:
hass.states.async_set(
entity_id,
STATE_OFF,
{ATTR_HUMIDITY: 42},
{ATTR_HUMIDITY: 42, ATTR_DEVICE_CLASS: HumidifierDeviceClass.DEHUMIDIFIER},
)
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 42.0

View File

@ -122,7 +122,8 @@ async def test_light_basic(hass: HomeAssistant, hk_driver, events) -> None:
@pytest.mark.parametrize(
"supported_color_modes", [["brightness"], ["hs"], ["color_temp"]]
"supported_color_modes",
[[ColorMode.BRIGHTNESS], [ColorMode.HS], [ColorMode.COLOR_TEMP]],
)
async def test_light_brightness(
hass: HomeAssistant, hk_driver, events, supported_color_modes
@ -149,7 +150,11 @@ async def test_light_brightness(
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_BRIGHTNESS: 102},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 40
@ -222,24 +227,48 @@ async def test_light_brightness(
# 0 is a special case for homekit, see "Handle Brightness"
# in update_state
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_BRIGHTNESS: 0},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 1
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 255})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_BRIGHTNESS: 255},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_BRIGHTNESS: 0},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 1
# Ensure floats are handled
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 55.66})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_BRIGHTNESS: 55.66},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 22
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 108.4})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_BRIGHTNESS: 108.4},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 43
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0.0})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_BRIGHTNESS: 0.0},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 1
@ -490,7 +519,9 @@ async def test_light_color_temperature_and_rgb_color(
assert acc.char_saturation.value == 100
@pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]])
@pytest.mark.parametrize(
"supported_color_modes", [[ColorMode.HS], [ColorMode.RGB], [ColorMode.XY]]
)
async def test_light_rgb_color(
hass: HomeAssistant, hk_driver, events, supported_color_modes
) -> None:
@ -1221,7 +1252,7 @@ async def test_light_set_brightness_and_color(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: ["hs"],
ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS],
ATTR_BRIGHTNESS: 255,
},
)
@ -1241,11 +1272,19 @@ async def test_light_set_brightness_and_color(
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_BRIGHTNESS: 102},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 40
hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (4.5, 9.2)})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_HS_COLOR: (4.5, 9.2)},
)
await hass.async_block_till_done()
assert acc.char_hue.value == 4
assert acc.char_saturation.value == 9
@ -1297,7 +1336,7 @@ async def test_light_min_max_mireds(hass: HomeAssistant, hk_driver, events) -> N
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: ["color_temp"],
ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP],
ATTR_BRIGHTNESS: 255,
ATTR_MAX_MIREDS: 500.5,
ATTR_MIN_MIREDS: 100.5,
@ -1319,7 +1358,7 @@ async def test_light_set_brightness_and_color_temp(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: ["color_temp"],
ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP],
ATTR_BRIGHTNESS: 255,
},
)
@ -1338,11 +1377,22 @@ async def test_light_set_brightness_and_color_temp(
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102})
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP], ATTR_BRIGHTNESS: 102},
)
await hass.async_block_till_done()
assert acc.char_brightness.value == 40
hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP_KELVIN: (4461)})
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP],
ATTR_COLOR_TEMP_KELVIN: (4461),
},
)
await hass.async_block_till_done()
assert acc.char_color_temp.value == 224

View File

@ -56,11 +56,12 @@ async def test_media_player_set_state(hass: HomeAssistant, hk_driver, events) ->
}
}
entity_id = "media_player.test"
base_attrs = {ATTR_SUPPORTED_FEATURES: 20873, ATTR_MEDIA_VOLUME_MUTED: False}
hass.states.async_set(
entity_id,
None,
{ATTR_SUPPORTED_FEATURES: 20873, ATTR_MEDIA_VOLUME_MUTED: False},
base_attrs,
)
await hass.async_block_till_done()
acc = MediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, config)
@ -75,33 +76,35 @@ async def test_media_player_set_state(hass: HomeAssistant, hk_driver, events) ->
assert acc.chars[FEATURE_PLAY_STOP].value is False
assert acc.chars[FEATURE_TOGGLE_MUTE].value is False
hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True})
hass.states.async_set(
entity_id, STATE_ON, {**base_attrs, ATTR_MEDIA_VOLUME_MUTED: True}
)
await hass.async_block_till_done()
assert acc.chars[FEATURE_ON_OFF].value is True
assert acc.chars[FEATURE_TOGGLE_MUTE].value is True
hass.states.async_set(entity_id, STATE_OFF)
hass.states.async_set(entity_id, STATE_OFF, base_attrs)
await hass.async_block_till_done()
assert acc.chars[FEATURE_ON_OFF].value is False
hass.states.async_set(entity_id, STATE_ON)
hass.states.async_set(entity_id, STATE_ON, base_attrs)
await hass.async_block_till_done()
assert acc.chars[FEATURE_ON_OFF].value is True
hass.states.async_set(entity_id, STATE_STANDBY)
hass.states.async_set(entity_id, STATE_STANDBY, base_attrs)
await hass.async_block_till_done()
assert acc.chars[FEATURE_ON_OFF].value is False
hass.states.async_set(entity_id, STATE_PLAYING)
hass.states.async_set(entity_id, STATE_PLAYING, base_attrs)
await hass.async_block_till_done()
assert acc.chars[FEATURE_PLAY_PAUSE].value is True
assert acc.chars[FEATURE_PLAY_STOP].value is True
hass.states.async_set(entity_id, STATE_PAUSED)
hass.states.async_set(entity_id, STATE_PAUSED, base_attrs)
await hass.async_block_till_done()
assert acc.chars[FEATURE_PLAY_PAUSE].value is False
hass.states.async_set(entity_id, STATE_IDLE)
hass.states.async_set(entity_id, STATE_IDLE, base_attrs)
await hass.async_block_till_done()
assert acc.chars[FEATURE_PLAY_STOP].value is False
@ -180,15 +183,16 @@ async def test_media_player_television(
# Supports 'select_source', 'volume_step', 'turn_on', 'turn_off',
# 'volume_mute', 'volume_set', 'pause'
base_attrs = {
ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV,
ATTR_SUPPORTED_FEATURES: 3469,
ATTR_MEDIA_VOLUME_MUTED: False,
ATTR_INPUT_SOURCE_LIST: ["HDMI 1", "HDMI 2", "HDMI 3", "HDMI 4"],
}
hass.states.async_set(
entity_id,
None,
{
ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV,
ATTR_SUPPORTED_FEATURES: 3469,
ATTR_MEDIA_VOLUME_MUTED: False,
ATTR_INPUT_SOURCE_LIST: ["HDMI 1", "HDMI 2", "HDMI 3", "HDMI 4"],
},
base_attrs,
)
await hass.async_block_till_done()
acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None)
@ -203,32 +207,40 @@ async def test_media_player_television(
assert acc.char_input_source.value == 0
assert acc.char_mute.value is False
hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True})
hass.states.async_set(
entity_id, STATE_ON, {**base_attrs, ATTR_MEDIA_VOLUME_MUTED: True}
)
await hass.async_block_till_done()
assert acc.char_active.value == 1
assert acc.char_mute.value is True
hass.states.async_set(entity_id, STATE_OFF)
hass.states.async_set(entity_id, STATE_OFF, base_attrs)
await hass.async_block_till_done()
assert acc.char_active.value == 0
hass.states.async_set(entity_id, STATE_ON)
hass.states.async_set(entity_id, STATE_ON, base_attrs)
await hass.async_block_till_done()
assert acc.char_active.value == 1
hass.states.async_set(entity_id, STATE_STANDBY)
hass.states.async_set(entity_id, STATE_STANDBY, base_attrs)
await hass.async_block_till_done()
assert acc.char_active.value == 0
hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: "HDMI 2"})
hass.states.async_set(
entity_id, STATE_ON, {**base_attrs, ATTR_INPUT_SOURCE: "HDMI 2"}
)
await hass.async_block_till_done()
assert acc.char_input_source.value == 1
hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: "HDMI 3"})
hass.states.async_set(
entity_id, STATE_ON, {**base_attrs, ATTR_INPUT_SOURCE: "HDMI 3"}
)
await hass.async_block_till_done()
assert acc.char_input_source.value == 2
hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: "HDMI 5"})
hass.states.async_set(
entity_id, STATE_ON, {**base_attrs, ATTR_INPUT_SOURCE: "HDMI 5"}
)
await hass.async_block_till_done()
assert acc.char_input_source.value == 0
assert caplog.records[-2].levelname == "DEBUG"
@ -358,12 +370,15 @@ async def test_media_player_television_basic(
) -> None:
"""Test if basic television accessory and HA are updated accordingly."""
entity_id = "media_player.television"
base_attrs = {
ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV,
ATTR_SUPPORTED_FEATURES: 384,
}
# Supports turn_on', 'turn_off'
hass.states.async_set(
entity_id,
None,
{ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, ATTR_SUPPORTED_FEATURES: 384},
base_attrs,
)
await hass.async_block_till_done()
acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None)
@ -374,15 +389,19 @@ async def test_media_player_television_basic(
assert acc.chars_speaker == []
assert acc.support_select_source is False
hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True})
hass.states.async_set(
entity_id, STATE_ON, {**base_attrs, ATTR_MEDIA_VOLUME_MUTED: True}
)
await hass.async_block_till_done()
assert acc.char_active.value == 1
hass.states.async_set(entity_id, STATE_OFF)
hass.states.async_set(entity_id, STATE_OFF, base_attrs)
await hass.async_block_till_done()
assert acc.char_active.value == 0
hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: "HDMI 3"})
hass.states.async_set(
entity_id, STATE_ON, {**base_attrs, ATTR_INPUT_SOURCE: "HDMI 3"}
)
await hass.async_block_till_done()
assert acc.char_active.value == 1

View File

@ -1,13 +1,14 @@
"""Test different accessory types: Remotes."""
from unittest.mock import patch
import pytest
from homeassistant.components.homekit.accessories import HomeDriver
from homeassistant.components.homekit.const import (
ATTR_KEY_NAME,
ATTR_VALUE,
DOMAIN as HOMEKIT_DOMAIN,
EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
KEY_ARROW_RIGHT,
SERVICE_HOMEKIT_RESET_ACCESSORY,
)
from homeassistant.components.homekit.type_remotes import ActivityRemote
from homeassistant.components.remote import (
@ -30,18 +31,19 @@ from tests.common import async_mock_service
async def test_activity_remote(
hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture
hass: HomeAssistant, hk_driver: HomeDriver, events, caplog: pytest.LogCaptureFixture
) -> None:
"""Test if remote accessory and HA are updated accordingly."""
entity_id = "remote.harmony"
base_attrs = {
ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY,
ATTR_CURRENT_ACTIVITY: "Apple TV",
ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
}
hass.states.async_set(
entity_id,
None,
{
ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY,
ATTR_CURRENT_ACTIVITY: "Apple TV",
ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
},
base_attrs,
)
await hass.async_block_till_done()
acc = ActivityRemote(hass, hk_driver, "ActivityRemote", entity_id, 2, None)
@ -58,47 +60,31 @@ async def test_activity_remote(
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY,
ATTR_CURRENT_ACTIVITY: "Apple TV",
ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
},
base_attrs,
)
await hass.async_block_till_done()
assert acc.char_active.value == 1
hass.states.async_set(entity_id, STATE_OFF)
hass.states.async_set(entity_id, STATE_OFF, base_attrs)
await hass.async_block_till_done()
assert acc.char_active.value == 0
hass.states.async_set(entity_id, STATE_ON)
hass.states.async_set(entity_id, STATE_ON, base_attrs)
await hass.async_block_till_done()
assert acc.char_active.value == 1
hass.states.async_set(entity_id, STATE_STANDBY)
hass.states.async_set(entity_id, STATE_STANDBY, base_attrs)
await hass.async_block_till_done()
assert acc.char_active.value == 0
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY,
ATTR_CURRENT_ACTIVITY: "TV",
ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
},
entity_id, STATE_ON, {**base_attrs, ATTR_CURRENT_ACTIVITY: "TV"}
)
await hass.async_block_till_done()
assert acc.char_input_source.value == 0
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY,
ATTR_CURRENT_ACTIVITY: "Apple TV",
ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
},
entity_id, STATE_ON, {**base_attrs, ATTR_CURRENT_ACTIVITY: "Apple TV"}
)
await hass.async_block_till_done()
assert acc.char_input_source.value == 1
@ -154,21 +140,19 @@ async def test_activity_remote(
assert len(events) == 1
assert events[0].data[ATTR_KEY_NAME] == KEY_ARROW_RIGHT
call_reset_accessory = async_mock_service(
hass, HOMEKIT_DOMAIN, SERVICE_HOMEKIT_RESET_ACCESSORY
)
# A wild source appears - The accessory should rebuild itself
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY,
ATTR_CURRENT_ACTIVITY: "Amazon TV",
ATTR_ACTIVITY_LIST: ["TV", "Apple TV", "Amazon TV"],
},
)
await hass.async_block_till_done()
assert call_reset_accessory[0].data[ATTR_ENTITY_ID] == entity_id
# A wild source appears - The accessory should reload itself
with patch.object(acc, "async_reload") as mock_reload:
hass.states.async_set(
entity_id,
STATE_ON,
{
**base_attrs,
ATTR_CURRENT_ACTIVITY: "Amazon TV",
ATTR_ACTIVITY_LIST: ["TV", "Apple TV", "Amazon TV"],
},
)
await hass.async_block_till_done()
assert mock_reload.called
async def test_activity_remote_bad_names(

View File

@ -1,4 +1,6 @@
"""Test different accessory types: Sensors."""
from unittest.mock import patch
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.homekit import get_accessory
from homeassistant.components.homekit.const import (
@ -71,11 +73,13 @@ async def test_temperature(hass: HomeAssistant, hk_driver) -> None:
await hass.async_block_till_done()
assert acc.char_temp.value == 0
hass.states.async_set(
entity_id, "75.2", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}
)
await hass.async_block_till_done()
assert acc.char_temp.value == 24
# The UOM changes, the accessory should reload itself
with patch.object(acc, "async_reload") as mock_reload:
hass.states.async_set(
entity_id, "75.2", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}
)
await hass.async_block_till_done()
assert mock_reload.called
async def test_humidity(hass: HomeAssistant, hk_driver) -> None:

View File

@ -79,21 +79,22 @@ from tests.common import async_mock_service
async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
"""Test if accessory and HA are updated accordingly."""
entity_id = "climate.test"
base_attrs = {
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
}
hass.states.async_set(
entity_id,
HVACMode.OFF,
{
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
base_attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
@ -124,17 +125,10 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.HEAT,
{
**base_attrs,
ATTR_TEMPERATURE: 22.2,
ATTR_CURRENT_TEMPERATURE: 17.8,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -148,17 +142,10 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.HEAT,
{
**base_attrs,
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 23.0,
ATTR_HVAC_ACTION: HVACAction.IDLE,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -172,17 +159,10 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.FAN_ONLY,
{
**base_attrs,
ATTR_TEMPERATURE: 20.0,
ATTR_CURRENT_TEMPERATURE: 25.0,
ATTR_HVAC_ACTION: HVACAction.COOLING,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -196,6 +176,7 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.COOL,
{
**base_attrs,
ATTR_TEMPERATURE: 20.0,
ATTR_CURRENT_TEMPERATURE: 19.0,
ATTR_HVAC_ACTION: HVACAction.IDLE,
@ -211,7 +192,7 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
hass.states.async_set(
entity_id,
HVACMode.OFF,
{ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0},
{**base_attrs, ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0},
)
await hass.async_block_till_done()
assert acc.char_target_temp.value == 22.0
@ -224,17 +205,10 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.AUTO,
{
**base_attrs,
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -248,17 +222,10 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.HEAT_COOL,
{
**base_attrs,
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 25.0,
ATTR_HVAC_ACTION: HVACAction.COOLING,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -272,17 +239,10 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.AUTO,
{
**base_attrs,
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 22.0,
ATTR_HVAC_ACTION: HVACAction.IDLE,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -296,17 +256,10 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.FAN_ONLY,
{
**base_attrs,
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 22.0,
ATTR_HVAC_ACTION: HVACAction.FAN,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -320,7 +273,7 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.DRY,
{
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE,
**base_attrs,
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 22.0,
ATTR_HVAC_ACTION: HVACAction.DRYING,
@ -419,23 +372,23 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None:
async def test_thermostat_auto(hass: HomeAssistant, hk_driver, events) -> None:
"""Test if accessory and HA are updated accordingly."""
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,
],
}
# support_auto = True
hass.states.async_set(
entity_id,
HVACMode.OFF,
{
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,
],
},
base_attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
@ -458,18 +411,11 @@ async def test_thermostat_auto(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.HEAT_COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 22.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -484,18 +430,11 @@ async def test_thermostat_auto(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 24.0,
ATTR_HVAC_ACTION: HVACAction.COOLING,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -510,18 +449,11 @@ async def test_thermostat_auto(hass: HomeAssistant, hk_driver, events) -> None:
entity_id,
HVACMode.AUTO,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 21.0,
ATTR_HVAC_ACTION: HVACAction.IDLE,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -575,23 +507,23 @@ async def test_thermostat_mode_and_temp_change(
) -> None:
"""Test if accessory where the mode and temp change in the same call."""
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,
],
}
# support_auto = True
hass.states.async_set(
entity_id,
HVACMode.OFF,
{
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,
],
},
base_attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
@ -614,18 +546,11 @@ async def test_thermostat_mode_and_temp_change(
entity_id,
HVACMode.COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 21.0,
ATTR_HVAC_ACTION: HVACAction.COOLING,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -688,9 +613,9 @@ async def test_thermostat_mode_and_temp_change(
async def test_thermostat_humidity(hass: HomeAssistant, hk_driver, events) -> None:
"""Test if accessory and HA are updated accordingly with humidity."""
entity_id = "climate.test"
base_attrs = {ATTR_SUPPORTED_FEATURES: 4}
# support_auto = True
hass.states.async_set(entity_id, HVACMode.OFF, {ATTR_SUPPORTED_FEATURES: 4})
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)
@ -704,14 +629,18 @@ async def test_thermostat_humidity(hass: HomeAssistant, hk_driver, events) -> No
assert acc.char_target_humidity.properties[PROP_MIN_VALUE] == DEFAULT_MIN_HUMIDITY
hass.states.async_set(
entity_id, HVACMode.HEAT_COOL, {ATTR_HUMIDITY: 65, ATTR_CURRENT_HUMIDITY: 40}
entity_id,
HVACMode.HEAT_COOL,
{**base_attrs, ATTR_HUMIDITY: 65, ATTR_CURRENT_HUMIDITY: 40},
)
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 40
assert acc.char_target_humidity.value == 65
hass.states.async_set(
entity_id, HVACMode.COOL, {ATTR_HUMIDITY: 35, ATTR_CURRENT_HUMIDITY: 70}
entity_id,
HVACMode.COOL,
{**base_attrs, ATTR_HUMIDITY: 35, ATTR_CURRENT_HUMIDITY: 70},
)
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 70
@ -772,24 +701,24 @@ async def test_thermostat_humidity_with_target_humidity(
async def test_thermostat_power_state(hass: HomeAssistant, hk_driver, events) -> None:
"""Test if accessory and HA are updated accordingly."""
entity_id = "climate.test"
base_attrs = {
ATTR_SUPPORTED_FEATURES: 4096,
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [
HVACMode.HEAT_COOL,
HVACMode.COOL,
HVACMode.AUTO,
HVACMode.HEAT,
HVACMode.OFF,
],
}
# SUPPORT_ON_OFF = True
hass.states.async_set(
entity_id,
HVACMode.HEAT,
{
ATTR_SUPPORTED_FEATURES: 4096,
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [
HVACMode.HEAT_COOL,
HVACMode.COOL,
HVACMode.AUTO,
HVACMode.HEAT,
HVACMode.OFF,
],
},
base_attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
@ -805,16 +734,10 @@ async def test_thermostat_power_state(hass: HomeAssistant, hk_driver, events) ->
entity_id,
HVACMode.OFF,
{
**base_attrs,
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.IDLE,
ATTR_HVAC_MODES: [
HVACMode.HEAT_COOL,
HVACMode.COOL,
HVACMode.AUTO,
HVACMode.HEAT,
HVACMode.OFF,
],
},
)
await hass.async_block_till_done()
@ -825,16 +748,10 @@ async def test_thermostat_power_state(hass: HomeAssistant, hk_driver, events) ->
entity_id,
HVACMode.OFF,
{
**base_attrs,
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.IDLE,
ATTR_HVAC_MODES: [
HVACMode.HEAT_COOL,
HVACMode.COOL,
HVACMode.AUTO,
HVACMode.HEAT,
HVACMode.OFF,
],
},
)
await hass.async_block_till_done()
@ -1566,12 +1483,15 @@ async def test_thermostat_without_target_temp_only_range(
) -> None:
"""Test a thermostat that only supports a range."""
entity_id = "climate.test"
base_attrs = {
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
}
# support_auto = True
hass.states.async_set(
entity_id,
HVACMode.OFF,
{ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE},
base_attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
@ -1594,19 +1514,11 @@ async def test_thermostat_without_target_temp_only_range(
entity_id,
HVACMode.HEAT_COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 22.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -1621,19 +1533,11 @@ async def test_thermostat_without_target_temp_only_range(
entity_id,
HVACMode.COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 24.0,
ATTR_HVAC_ACTION: HVACAction.COOLING,
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -1648,19 +1552,11 @@ async def test_thermostat_without_target_temp_only_range(
entity_id,
HVACMode.COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 21.0,
ATTR_HVAC_ACTION: HVACAction.IDLE,
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.HEAT_COOL,
HVACMode.FAN_ONLY,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
@ -1925,16 +1821,17 @@ async def test_thermostat_with_no_modes_when_we_first_see(
) -> None:
"""Test if a thermostat that is not ready when we first see it."""
entity_id = "climate.test"
base_attrs = {
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
ATTR_HVAC_MODES: [],
}
# support_auto = True
hass.states.async_set(
entity_id,
HVACMode.OFF,
{
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
ATTR_HVAC_MODES: [],
},
base_attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
@ -1955,24 +1852,22 @@ async def test_thermostat_with_no_modes_when_we_first_see(
assert acc.char_target_heat_cool.value == 0
hass.states.async_set(
entity_id,
HVACMode.HEAT_COOL,
{
ATTR_TARGET_TEMP_HIGH: 22.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.OFF, HVACMode.AUTO],
},
)
await hass.async_block_till_done()
assert acc.char_heating_thresh_temp.value == 20.0
assert acc.char_cooling_thresh_temp.value == 22.0
assert acc.char_current_heat_cool.value == 1
assert acc.char_target_heat_cool.value == 3
assert acc.char_current_temp.value == 18.0
assert acc.char_display_units.value == 0
# Verify reload on modes changed out from under us
with patch.object(acc, "async_reload") as mock_reload:
hass.states.async_set(
entity_id,
HVACMode.HEAT_COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 22.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.OFF, HVACMode.AUTO],
},
)
await hass.async_block_till_done()
assert mock_reload.called
async def test_thermostat_with_no_off_after_recheck(
@ -1981,15 +1876,16 @@ async def test_thermostat_with_no_off_after_recheck(
"""Test if a thermostat that is not ready when we first see it that actually does not have off."""
entity_id = "climate.test"
base_attrs = {
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
ATTR_HVAC_MODES: [],
}
# support_auto = True
hass.states.async_set(
entity_id,
HVACMode.COOL,
{
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
ATTR_HVAC_MODES: [],
},
base_attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
@ -2010,24 +1906,22 @@ async def test_thermostat_with_no_off_after_recheck(
assert acc.char_target_heat_cool.value == 2
hass.states.async_set(
entity_id,
HVACMode.HEAT_COOL,
{
ATTR_TARGET_TEMP_HIGH: 22.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
},
)
await hass.async_block_till_done()
assert acc.char_heating_thresh_temp.value == 20.0
assert acc.char_cooling_thresh_temp.value == 22.0
assert acc.char_current_heat_cool.value == 1
assert acc.char_target_heat_cool.value == 3
assert acc.char_current_temp.value == 18.0
assert acc.char_display_units.value == 0
# Verify reload when modes change out from under us
with patch.object(acc, "async_reload") as mock_reload:
hass.states.async_set(
entity_id,
HVACMode.HEAT_COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 22.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
},
)
await hass.async_block_till_done()
assert mock_reload.called
async def test_thermostat_with_temp_clamps(
@ -2035,17 +1929,17 @@ async def test_thermostat_with_temp_clamps(
) -> None:
"""Test that tempatures are clamped to valid values to prevent homekit crash."""
entity_id = "climate.test"
base_attrs = {
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,
}
hass.states.async_set(
entity_id,
HVACMode.COOL,
{
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
ATTR_HVAC_MODES: [],
ATTR_MAX_TEMP: 50,
ATTR_MIN_TEMP: 100,
},
base_attrs,
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
@ -2064,17 +1958,17 @@ async def test_thermostat_with_temp_clamps(
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 100
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
assert acc.char_target_heat_cool.value == 2
assert acc.char_target_heat_cool.value == 3
hass.states.async_set(
entity_id,
HVACMode.HEAT_COOL,
{
**base_attrs,
ATTR_TARGET_TEMP_HIGH: 822.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 9918.0,
ATTR_HVAC_ACTION: HVACAction.HEATING,
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
},
)
await hass.async_block_till_done()

View File

@ -71,3 +71,4 @@ async def test_programmable_switch_button_fires_on_trigger(
char = acc.get_characteristic(call.args[0]["aid"], call.args[0]["iid"])
assert char.display_name == CHAR_PROGRAMMABLE_SWITCH_EVENT
await acc.stop()
await hass.async_block_till_done()