mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Refactor HomeKit to allow supported features/device class to change (#101719)
This commit is contained in:
parent
f166e1cc1a
commit
7b4b8e7516
@ -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(
|
||||
|
@ -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(),
|
||||
|
@ -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``."""
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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 = {}
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user