forked from home-assistant/core
Compare commits
32 Commits
2024.4.0b3
...
2024.4.0b5
| Author | SHA1 | Date | |
|---|---|---|---|
| 5af5f3694e | |||
| b539b25682 | |||
| ca31479d29 | |||
| 92dfec3c98 | |||
| 230c29edbe | |||
| 559fe65471 | |||
| 384d10a51d | |||
| e5a620545c | |||
| 7b84e86f89 | |||
| 18b6de567d | |||
| a6076a0d33 | |||
| 7164993562 | |||
| bc21836e7e | |||
| 52612b10fd | |||
| 623d85ecaa | |||
| 43631d5944 | |||
| 112aab47fb | |||
| ea13f102e0 | |||
| bb33725e7f | |||
| bd6890ab83 | |||
| 25c611ffc4 | |||
| fc24b61859 | |||
| 71588b5c22 | |||
| 14dfb6a255 | |||
| ef97255d9c | |||
| e8afdd67d0 | |||
| 008e4413b5 | |||
| c373d40e34 | |||
| bdf51553ef | |||
| f2edc15687 | |||
| 286a09d737 | |||
| e8ee2fd25c |
@@ -93,6 +93,11 @@ from .util.async_ import create_eager_task
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import async_get_user_site, is_virtual_env
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
||||
from anyio._backends import _asyncio # noqa: F401
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .runner import RuntimeConfig
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class AxisHub:
|
||||
if status.status.state == ClientState.ACTIVE:
|
||||
self.config.entry.async_on_unload(
|
||||
await mqtt.async_subscribe(
|
||||
hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message
|
||||
hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message
|
||||
)
|
||||
)
|
||||
|
||||
@@ -124,7 +124,8 @@ class AxisHub:
|
||||
def mqtt_message(self, message: ReceiveMessage) -> None:
|
||||
"""Receive Axis MQTT message."""
|
||||
self.disconnect_from_stream()
|
||||
|
||||
if message.topic.endswith("event/connection"):
|
||||
return
|
||||
event = mqtt_json_to_event(message.payload)
|
||||
self.api.event.handler(event)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==59"],
|
||||
"requirements": ["axis==60"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -58,6 +58,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
@@ -75,6 +76,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_name,
|
||||
area=None,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
start,
|
||||
template,
|
||||
@@ -163,7 +164,12 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
self.hass.bus.async_listen(
|
||||
ar.EVENT_AREA_REGISTRY_UPDATED,
|
||||
self._async_handle_area_registry_changed,
|
||||
self._async_handle_area_floor_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
fr.EVENT_FLOOR_REGISTRY_UPDATED,
|
||||
self._async_handle_area_floor_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
@@ -696,10 +702,13 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
return lang_intents
|
||||
|
||||
@core.callback
|
||||
def _async_handle_area_registry_changed(
|
||||
self, event: core.Event[ar.EventAreaRegistryUpdatedData]
|
||||
def _async_handle_area_floor_registry_changed(
|
||||
self,
|
||||
event: core.Event[
|
||||
ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData
|
||||
],
|
||||
) -> None:
|
||||
"""Clear area area cache when the area registry has changed."""
|
||||
"""Clear area/floor list cache when the area registry has changed."""
|
||||
self._slot_lists = None
|
||||
|
||||
@core.callback
|
||||
@@ -773,6 +782,8 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
# Default name
|
||||
entity_names.append((state.name, state.name, context))
|
||||
|
||||
_LOGGER.debug("Exposed entities: %s", entity_names)
|
||||
|
||||
# Expose all areas.
|
||||
#
|
||||
# We pass in area id here with the expectation that no two areas will
|
||||
@@ -788,11 +799,25 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
area_names.append((alias, area.id))
|
||||
|
||||
_LOGGER.debug("Exposed entities: %s", entity_names)
|
||||
# Expose all floors.
|
||||
#
|
||||
# We pass in floor id here with the expectation that no two floors will
|
||||
# share the same name or alias.
|
||||
floors = fr.async_get(self.hass)
|
||||
floor_names = []
|
||||
for floor in floors.async_list_floors():
|
||||
floor_names.append((floor.name, floor.floor_id))
|
||||
if floor.aliases:
|
||||
for alias in floor.aliases:
|
||||
if not alias.strip():
|
||||
continue
|
||||
|
||||
floor_names.append((alias, floor.floor_id))
|
||||
|
||||
self._slot_lists = {
|
||||
"area": TextSlotList.from_tuples(area_names, allow_template=False),
|
||||
"name": TextSlotList.from_tuples(entity_names, allow_template=False),
|
||||
"floor": TextSlotList.from_tuples(floor_names, allow_template=False),
|
||||
}
|
||||
|
||||
return self._slot_lists
|
||||
@@ -953,6 +978,10 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
|
||||
# area only
|
||||
return ErrorKey.NO_AREA, {"area": unmatched_area}
|
||||
|
||||
if unmatched_floor := unmatched_text.get("floor"):
|
||||
# floor only
|
||||
return ErrorKey.NO_FLOOR, {"floor": unmatched_floor}
|
||||
|
||||
# Area may still have matched
|
||||
matched_area: str | None = None
|
||||
if matched_area_entity := result.entities.get("area"):
|
||||
@@ -1000,6 +1029,13 @@ def _get_no_states_matched_response(
|
||||
"area": no_states_error.area,
|
||||
}
|
||||
|
||||
if no_states_error.floor:
|
||||
# domain in floor
|
||||
return ErrorKey.NO_DOMAIN_IN_FLOOR, {
|
||||
"domain": domain,
|
||||
"floor": no_states_error.floor,
|
||||
}
|
||||
|
||||
# domain only
|
||||
return ErrorKey.NO_DOMAIN, {"domain": domain}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.27"]
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.29"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"services": {
|
||||
"restart": "mdi:restart",
|
||||
"start": "mdi:start",
|
||||
"start": "mdi:play",
|
||||
"stop": "mdi:stop"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +311,17 @@ class FritzBoxTools(
|
||||
)
|
||||
return unregister_entity_updates
|
||||
|
||||
def _entity_states_update(self) -> dict:
|
||||
"""Run registered entity update calls."""
|
||||
entity_states = {}
|
||||
for key in list(self._entity_update_functions):
|
||||
if (update_fn := self._entity_update_functions.get(key)) is not None:
|
||||
_LOGGER.debug("update entity %s", key)
|
||||
entity_states[key] = update_fn(
|
||||
self.fritz_status, self.data["entity_states"].get(key)
|
||||
)
|
||||
return entity_states
|
||||
|
||||
async def _async_update_data(self) -> UpdateCoordinatorDataType:
|
||||
"""Update FritzboxTools data."""
|
||||
entity_data: UpdateCoordinatorDataType = {
|
||||
@@ -319,15 +330,9 @@ class FritzBoxTools(
|
||||
}
|
||||
try:
|
||||
await self.async_scan_devices()
|
||||
for key in list(self._entity_update_functions):
|
||||
_LOGGER.debug("update entity %s", key)
|
||||
entity_data["entity_states"][
|
||||
key
|
||||
] = await self.hass.async_add_executor_job(
|
||||
self._entity_update_functions[key],
|
||||
self.fritz_status,
|
||||
self.data["entity_states"].get(key),
|
||||
)
|
||||
entity_data["entity_states"] = await self.hass.async_add_executor_job(
|
||||
self._entity_states_update
|
||||
)
|
||||
if self.has_call_deflections:
|
||||
entity_data[
|
||||
"call_deflections"
|
||||
|
||||
@@ -141,7 +141,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
|
||||
# update old and user-configured config entries
|
||||
for entry in self._async_current_entries():
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if entry.data[CONF_HOST] == host:
|
||||
if uuid and not entry.unique_id:
|
||||
self.hass.config_entries.async_update_entry(entry, unique_id=uuid)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240329.1"]
|
||||
"requirements": ["home-assistant-frontend==20240402.0"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"services": {
|
||||
"select_next": "mdi:skip",
|
||||
"select_next": "mdi:skip-next",
|
||||
"select_option": "mdi:check",
|
||||
"select_previous": "mdi:skip-previous",
|
||||
"select_first": "mdi:skip-backward",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"unjoin": "mdi:ungroup",
|
||||
"volume_down": "mdi:volume-minus",
|
||||
"volume_mute": "mdi:volume-mute",
|
||||
"volume_set": "mdi:volume",
|
||||
"volume_set": "mdi:volume-medium",
|
||||
"volume_up": "mdi:volume-plus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.4.1"]
|
||||
"requirements": ["opower==0.4.2"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -357,5 +357,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
|
||||
]
|
||||
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.GLOBAL_CONTROL, command_data
|
||||
OverkizCommand.GLOBAL_CONTROL, *command_data
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["plexapi", "plexwebsocket"],
|
||||
"requirements": [
|
||||
"PlexAPI==4.15.10",
|
||||
"PlexAPI==4.15.11",
|
||||
"plexauth==0.0.6",
|
||||
"plexwebsocket==0.0.14"
|
||||
],
|
||||
|
||||
@@ -46,7 +46,6 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
"""Initialize ReolinkVODMediaSource."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
self.data: dict[str, ReolinkData] = hass.data[DOMAIN]
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
@@ -57,7 +56,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
_, config_entry_id, channel_str, stream_res, filename = identifier
|
||||
channel = int(channel_str)
|
||||
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
vod_type = VodRequestType.RTMP
|
||||
if host.api.is_nvr:
|
||||
@@ -130,7 +130,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
continue
|
||||
channels: list[str] = []
|
||||
host = self.data[config_entry.entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry.entry_id].host
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_reg, config_entry.entry_id
|
||||
)
|
||||
@@ -187,7 +188,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
self, config_entry_id: str, channel: int
|
||||
) -> BrowseMediaSource:
|
||||
"""Allow the user to select the high or low playback resolution, (low loads faster)."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
main_enc = await host.api.get_encoding(channel, "main")
|
||||
if main_enc == "h265":
|
||||
@@ -236,7 +238,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
self, config_entry_id: str, channel: int, stream: str
|
||||
) -> BrowseMediaSource:
|
||||
"""Return all days on which recordings are available for a reolink camera."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
# We want today of the camera, not necessarily today of the server
|
||||
now = host.api.time() or await host.api.async_get_time()
|
||||
@@ -288,7 +291,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
day: int,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return all recording files on a specific day of a Reolink camera."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
start = dt.datetime(year, month, day, hour=0, minute=0, second=0)
|
||||
end = dt.datetime(year, month, day, hour=23, minute=59, second=59)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/roomba",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["paho_mqtt", "roombapy"],
|
||||
"requirements": ["roombapy==1.6.13"],
|
||||
"requirements": ["roombapy==1.8.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_amzn-alexa._tcp.local.",
|
||||
|
||||
@@ -234,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
|
||||
)
|
||||
|
||||
CONF_GEN = "gen"
|
||||
|
||||
SHELLY_PLUS_RGBW_CHANNELS = 4
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.light import (
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_RGBW_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
@@ -34,12 +35,14 @@ from .const import (
|
||||
RGBW_MODELS,
|
||||
RPC_MIN_TRANSITION_TIME_SEC,
|
||||
SHBLB_1_RGB_EFFECTS,
|
||||
SHELLY_PLUS_RGBW_CHANNELS,
|
||||
STANDARD_RGB_EFFECTS,
|
||||
)
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
|
||||
from .entity import ShellyBlockEntity, ShellyRpcEntity
|
||||
from .utils import (
|
||||
async_remove_shelly_entity,
|
||||
async_remove_shelly_rpc_entities,
|
||||
brightness_to_percentage,
|
||||
get_device_entry_gen,
|
||||
get_rpc_key_ids,
|
||||
@@ -118,14 +121,28 @@ def async_setup_rpc_entry(
|
||||
return
|
||||
|
||||
if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"):
|
||||
# Light mode remove RGB & RGBW entities, add light entities
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, ["rgb:0", "rgbw:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids)
|
||||
return
|
||||
|
||||
light_keys = [f"light:{i}" for i in range(SHELLY_PLUS_RGBW_CHANNELS)]
|
||||
|
||||
if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"):
|
||||
# RGB mode remove light & RGBW entities, add RGB entity
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgbw:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids)
|
||||
return
|
||||
|
||||
if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"):
|
||||
# RGBW mode remove light & RGB entities, add RGBW entity
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgb:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids)
|
||||
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
) -> None:
|
||||
"""Initialize update entity."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
self._ota_in_progress: bool = False
|
||||
self._ota_in_progress: bool | int = False
|
||||
self._attr_release_url = get_release_url(
|
||||
coordinator.device.gen, coordinator.model, description.beta
|
||||
)
|
||||
@@ -237,14 +237,13 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
@callback
|
||||
def _ota_progress_callback(self, event: dict[str, Any]) -> None:
|
||||
"""Handle device OTA progress."""
|
||||
if self._ota_in_progress:
|
||||
if self.in_progress is not False:
|
||||
event_type = event["event"]
|
||||
if event_type == OTA_BEGIN:
|
||||
self._attr_in_progress = 0
|
||||
self._ota_in_progress = 0
|
||||
elif event_type == OTA_PROGRESS:
|
||||
self._attr_in_progress = event["progress_percent"]
|
||||
self._ota_in_progress = event["progress_percent"]
|
||||
elif event_type in (OTA_ERROR, OTA_SUCCESS):
|
||||
self._attr_in_progress = False
|
||||
self._ota_in_progress = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -262,6 +261,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
|
||||
return self.installed_version
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool | int:
|
||||
"""Update installation in progress."""
|
||||
return self._ota_in_progress
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
@@ -292,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
await self.coordinator.async_shutdown_device_and_start_reauth()
|
||||
else:
|
||||
self._ota_in_progress = True
|
||||
LOGGER.debug("OTA update call successful")
|
||||
LOGGER.info("OTA update call for %s successful", self.coordinator.name)
|
||||
|
||||
|
||||
class RpcSleepingUpdateEntity(
|
||||
|
||||
@@ -488,3 +488,15 @@ async def async_shutdown_device(device: BlockDevice | RpcDevice) -> None:
|
||||
await device.shutdown()
|
||||
if isinstance(device, BlockDevice):
|
||||
device.shutdown()
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove_shelly_rpc_entities(
|
||||
hass: HomeAssistant, domain: str, mac: str, keys: list[str]
|
||||
) -> None:
|
||||
"""Remove RPC based Shelly entity."""
|
||||
entity_reg = er_async_get(hass)
|
||||
for key in keys:
|
||||
if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"):
|
||||
LOGGER.debug("Removing entity: %s", entity_id)
|
||||
entity_reg.async_remove(entity_id)
|
||||
|
||||
@@ -201,7 +201,7 @@ def build_item_response(
|
||||
|
||||
if not title:
|
||||
try:
|
||||
title = payload["idstring"].split("/")[1]
|
||||
title = urllib.parse.unquote(payload["idstring"].split("/")[1])
|
||||
except IndexError:
|
||||
title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
|
||||
|
||||
|
||||
@@ -25,10 +25,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
partial(speedtest.Speedtest, secure=True)
|
||||
)
|
||||
coordinator = SpeedTestDataCoordinator(hass, config_entry, api)
|
||||
await hass.async_add_executor_job(coordinator.update_servers)
|
||||
except speedtest.SpeedtestException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data[DOMAIN] = coordinator
|
||||
|
||||
async def _async_finish_startup(hass: HomeAssistant) -> None:
|
||||
"""Run this only when HA has finished its startup."""
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@@ -36,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
# Don't start a speedtest during startup
|
||||
async_at_started(hass, _async_finish_startup)
|
||||
|
||||
hass.data[DOMAIN] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -58,14 +58,14 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
|
||||
async def _async_update_data(self) -> StarlinkData:
|
||||
async with asyncio.timeout(4):
|
||||
try:
|
||||
status, location, sleep = await asyncio.gather(
|
||||
self.hass.async_add_executor_job(status_data, self.channel_context),
|
||||
self.hass.async_add_executor_job(
|
||||
location_data, self.channel_context
|
||||
),
|
||||
self.hass.async_add_executor_job(
|
||||
get_sleep_config, self.channel_context
|
||||
),
|
||||
status = await self.hass.async_add_executor_job(
|
||||
status_data, self.channel_context
|
||||
)
|
||||
location = await self.hass.async_add_executor_job(
|
||||
location_data, self.channel_context
|
||||
)
|
||||
sleep = await self.hass.async_add_executor_job(
|
||||
get_sleep_config, self.channel_context
|
||||
)
|
||||
return StarlinkData(location, sleep, *status)
|
||||
except GrpcError as exc:
|
||||
|
||||
@@ -10,6 +10,7 @@ import math
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -62,14 +63,22 @@ class StarlinkTimeEntity(StarlinkEntity, TimeEntity):
|
||||
def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time:
|
||||
hour = math.floor(utc_minutes / 60)
|
||||
minute = utc_minutes % 60
|
||||
utc = datetime.now(UTC).replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
try:
|
||||
utc = datetime.now(UTC).replace(
|
||||
hour=hour, minute=minute, second=0, microsecond=0
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
return utc.astimezone(timezone).time()
|
||||
|
||||
|
||||
def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
try:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
utc_time = zoned_time.astimezone(UTC).time()
|
||||
return (utc_time.hour * 60) + utc_time.minute
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reboot": "mdi:reboot",
|
||||
"reboot": "mdi:restart",
|
||||
"shutdown": "mdi:power"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,6 @@ class LogEntry:
|
||||
"level",
|
||||
"message",
|
||||
"exception",
|
||||
"extracted_tb",
|
||||
"root_cause",
|
||||
"source",
|
||||
"count",
|
||||
@@ -200,7 +199,6 @@ class LogEntry:
|
||||
else:
|
||||
self.source = (record.pathname, record.lineno)
|
||||
self.count = 1
|
||||
self.extracted_tb = extracted_tb
|
||||
self.key = (self.name, self.source, self.root_cause)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
|
||||
@@ -34,7 +34,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = (
|
||||
is_on=lambda x: x == TessieState.ONLINE,
|
||||
),
|
||||
TessieBinarySensorEntityDescription(
|
||||
key="charge_state_battery_heater_on",
|
||||
key="climate_state_battery_heater",
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
"state": {
|
||||
"name": "Status"
|
||||
},
|
||||
"charge_state_battery_heater_on": {
|
||||
"climate_state_battery_heater": {
|
||||
"name": "Battery heater"
|
||||
},
|
||||
"charge_state_charge_enable_request": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"services": {
|
||||
"start": "mdi:start",
|
||||
"start": "mdi:play",
|
||||
"pause": "mdi:pause",
|
||||
"cancel": "mdi:cancel",
|
||||
"finish": "mdi:check",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"velbus-packet",
|
||||
"velbus-protocol"
|
||||
],
|
||||
"requirements": ["velbus-aio==2023.12.0"],
|
||||
"requirements": ["velbus-aio==2024.4.0"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10CF",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Diagnostics support for Whirlpool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import WhirlpoolData
|
||||
from .const import DOMAIN
|
||||
|
||||
TO_REDACT = {
|
||||
"SERIAL_NUMBER",
|
||||
"macaddress",
|
||||
"username",
|
||||
"password",
|
||||
"token",
|
||||
"unique_id",
|
||||
"SAID",
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
whirlpool: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
diagnostics_data = {
|
||||
"Washer_dryers": {
|
||||
wd["NAME"]: dict(wd.items())
|
||||
for wd in whirlpool.appliances_manager.washer_dryers
|
||||
},
|
||||
"aircons": {
|
||||
ac["NAME"]: dict(ac.items()) for ac in whirlpool.appliances_manager.aircons
|
||||
},
|
||||
"ovens": {
|
||||
oven["NAME"]: dict(oven.items())
|
||||
for oven in whirlpool.appliances_manager.ovens
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
||||
"appliances": async_redact_data(diagnostics_data, TO_REDACT),
|
||||
}
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["whirlpool"],
|
||||
"requirements": ["whirlpool-sixth-sense==0.18.6"]
|
||||
"requirements": ["whirlpool-sixth-sense==0.18.7"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from holidays import HolidayBase, country_holidays
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -13,7 +15,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
|
||||
from .const import CONF_PROVINCE, DOMAIN, PLATFORMS
|
||||
|
||||
|
||||
def _validate_country_and_province(
|
||||
async def _async_validate_country_and_province(
|
||||
hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None
|
||||
) -> None:
|
||||
"""Validate country and province."""
|
||||
@@ -21,7 +23,7 @@ def _validate_country_and_province(
|
||||
if not country:
|
||||
return
|
||||
try:
|
||||
country_holidays(country)
|
||||
await hass.async_add_executor_job(country_holidays, country)
|
||||
except NotImplementedError as ex:
|
||||
async_create_issue(
|
||||
hass,
|
||||
@@ -39,7 +41,9 @@ def _validate_country_and_province(
|
||||
if not province:
|
||||
return
|
||||
try:
|
||||
country_holidays(country, subdiv=province)
|
||||
await hass.async_add_executor_job(
|
||||
partial(country_holidays, country, subdiv=province)
|
||||
)
|
||||
except NotImplementedError as ex:
|
||||
async_create_issue(
|
||||
hass,
|
||||
@@ -66,10 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
country: str | None = entry.options.get(CONF_COUNTRY)
|
||||
province: str | None = entry.options.get(CONF_PROVINCE)
|
||||
|
||||
_validate_country_and_province(hass, entry, country, province)
|
||||
await _async_validate_country_and_province(hass, entry, country, province)
|
||||
|
||||
if country and CONF_LANGUAGE not in entry.options:
|
||||
cls: HolidayBase = country_holidays(country, subdiv=province)
|
||||
cls: HolidayBase = await hass.async_add_executor_job(
|
||||
partial(country_holidays, country, subdiv=province)
|
||||
)
|
||||
default_language = cls.default_language
|
||||
new_options = entry.options.copy()
|
||||
new_options[CONF_LANGUAGE] = default_language
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"switch_set_wifi_led_off": "mdi:wifi-off",
|
||||
"switch_set_power_price": "mdi:currency-usd",
|
||||
"switch_set_power_mode": "mdi:power",
|
||||
"vacuum_remote_control_start": "mdi:start",
|
||||
"vacuum_remote_control_start": "mdi:play",
|
||||
"vacuum_remote_control_stop": "mdi:stop",
|
||||
"vacuum_remote_control_move": "mdi:remote",
|
||||
"vacuum_remote_control_move_step": "mdi:remote",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zeroconf"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["zeroconf==0.131.0"]
|
||||
"requirements": ["zeroconf==0.132.0"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ from .util.signal_type import SignalType
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 4
|
||||
PATCH_VERSION: Final = "0b3"
|
||||
PATCH_VERSION: Final = "0b5"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
||||
@@ -774,8 +774,11 @@ class HomeAssistant:
|
||||
) -> asyncio.Future[_T]:
|
||||
"""Add an executor job from within the event loop."""
|
||||
task = self.loop.run_in_executor(None, target, *args)
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(self._tasks.remove)
|
||||
|
||||
tracked = asyncio.current_task() in self._tasks
|
||||
task_bucket = self._tasks if tracked else self._background_tasks
|
||||
task_bucket.add(task)
|
||||
task.add_done_callback(task_bucket.remove)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
@@ -198,15 +198,16 @@ async def async_check_ha_config_file( # noqa: C901
|
||||
|
||||
# Check if the integration has a custom config validator
|
||||
config_validator = None
|
||||
try:
|
||||
config_validator = await integration.async_get_platform("config")
|
||||
except ImportError as err:
|
||||
# Filter out import error of the config platform.
|
||||
# If the config platform contains bad imports, make sure
|
||||
# that still fails.
|
||||
if err.name != f"{integration.pkg_path}.config":
|
||||
result.add_error(f"Error importing config platform {domain}: {err}")
|
||||
continue
|
||||
if integration.platforms_exists(("config",)):
|
||||
try:
|
||||
config_validator = await integration.async_get_platform("config")
|
||||
except ImportError as err:
|
||||
# Filter out import error of the config platform.
|
||||
# If the config platform contains bad imports, make sure
|
||||
# that still fails.
|
||||
if err.name != f"{integration.pkg_path}.config":
|
||||
result.add_error(f"Error importing config platform {domain}: {err}")
|
||||
continue
|
||||
|
||||
if config_validator is not None and hasattr(
|
||||
config_validator, "async_validate_config"
|
||||
|
||||
@@ -24,7 +24,13 @@ from homeassistant.core import Context, HomeAssistant, State, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from . import area_registry, config_validation as cv, device_registry, entity_registry
|
||||
from . import (
|
||||
area_registry,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity_registry,
|
||||
floor_registry,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SlotsType = dict[str, Any]
|
||||
@@ -144,16 +150,18 @@ class NoStatesMatchedError(IntentError):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None,
|
||||
area: str | None,
|
||||
domains: set[str] | None,
|
||||
device_classes: set[str] | None,
|
||||
name: str | None = None,
|
||||
area: str | None = None,
|
||||
floor: str | None = None,
|
||||
domains: set[str] | None = None,
|
||||
device_classes: set[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize error."""
|
||||
super().__init__()
|
||||
|
||||
self.name = name
|
||||
self.area = area
|
||||
self.floor = floor
|
||||
self.domains = domains
|
||||
self.device_classes = device_classes
|
||||
|
||||
@@ -220,12 +228,35 @@ def _find_area(
|
||||
return None
|
||||
|
||||
|
||||
def _filter_by_area(
|
||||
def _find_floor(
|
||||
id_or_name: str, floors: floor_registry.FloorRegistry
|
||||
) -> floor_registry.FloorEntry | None:
|
||||
"""Find an floor by id or name, checking aliases too."""
|
||||
floor = floors.async_get_floor(id_or_name) or floors.async_get_floor_by_name(
|
||||
id_or_name
|
||||
)
|
||||
if floor is not None:
|
||||
return floor
|
||||
|
||||
# Check floor aliases
|
||||
for maybe_floor in floors.floors.values():
|
||||
if not maybe_floor.aliases:
|
||||
continue
|
||||
|
||||
for floor_alias in maybe_floor.aliases:
|
||||
if id_or_name == floor_alias.casefold():
|
||||
return maybe_floor
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _filter_by_areas(
|
||||
states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]],
|
||||
area: area_registry.AreaEntry,
|
||||
areas: Iterable[area_registry.AreaEntry],
|
||||
devices: device_registry.DeviceRegistry,
|
||||
) -> Iterable[tuple[State, entity_registry.RegistryEntry | None]]:
|
||||
"""Filter state/entity pairs by an area."""
|
||||
filter_area_ids: set[str | None] = {a.id for a in areas}
|
||||
entity_area_ids: dict[str, str | None] = {}
|
||||
for _state, entity in states_and_entities:
|
||||
if entity is None:
|
||||
@@ -241,7 +272,7 @@ def _filter_by_area(
|
||||
entity_area_ids[entity.id] = device.area_id
|
||||
|
||||
for state, entity in states_and_entities:
|
||||
if (entity is not None) and (entity_area_ids.get(entity.id) == area.id):
|
||||
if (entity is not None) and (entity_area_ids.get(entity.id) in filter_area_ids):
|
||||
yield (state, entity)
|
||||
|
||||
|
||||
@@ -252,11 +283,14 @@ def async_match_states(
|
||||
name: str | None = None,
|
||||
area_name: str | None = None,
|
||||
area: area_registry.AreaEntry | None = None,
|
||||
floor_name: str | None = None,
|
||||
floor: floor_registry.FloorEntry | None = None,
|
||||
domains: Collection[str] | None = None,
|
||||
device_classes: Collection[str] | None = None,
|
||||
states: Iterable[State] | None = None,
|
||||
entities: entity_registry.EntityRegistry | None = None,
|
||||
areas: area_registry.AreaRegistry | None = None,
|
||||
floors: floor_registry.FloorRegistry | None = None,
|
||||
devices: device_registry.DeviceRegistry | None = None,
|
||||
assistant: str | None = None,
|
||||
) -> Iterable[State]:
|
||||
@@ -268,6 +302,15 @@ def async_match_states(
|
||||
if entities is None:
|
||||
entities = entity_registry.async_get(hass)
|
||||
|
||||
if devices is None:
|
||||
devices = device_registry.async_get(hass)
|
||||
|
||||
if areas is None:
|
||||
areas = area_registry.async_get(hass)
|
||||
|
||||
if floors is None:
|
||||
floors = floor_registry.async_get(hass)
|
||||
|
||||
# Gather entities
|
||||
states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]] = []
|
||||
for state in states:
|
||||
@@ -294,20 +337,35 @@ def async_match_states(
|
||||
if _is_device_class(state, entity, device_classes)
|
||||
]
|
||||
|
||||
filter_areas: list[area_registry.AreaEntry] = []
|
||||
|
||||
if (floor is None) and (floor_name is not None):
|
||||
# Look up floor by name
|
||||
floor = _find_floor(floor_name, floors)
|
||||
if floor is None:
|
||||
_LOGGER.warning("Floor not found: %s", floor_name)
|
||||
return
|
||||
|
||||
if floor is not None:
|
||||
filter_areas = [
|
||||
a for a in areas.async_list_areas() if a.floor_id == floor.floor_id
|
||||
]
|
||||
|
||||
if (area is None) and (area_name is not None):
|
||||
# Look up area by name
|
||||
if areas is None:
|
||||
areas = area_registry.async_get(hass)
|
||||
|
||||
area = _find_area(area_name, areas)
|
||||
assert area is not None, f"No area named {area_name}"
|
||||
if area is None:
|
||||
_LOGGER.warning("Area not found: %s", area_name)
|
||||
return
|
||||
|
||||
if area is not None:
|
||||
# Filter by states/entities by area
|
||||
if devices is None:
|
||||
devices = device_registry.async_get(hass)
|
||||
filter_areas = [area]
|
||||
|
||||
states_and_entities = list(_filter_by_area(states_and_entities, area, devices))
|
||||
if filter_areas:
|
||||
# Filter by states/entities by area
|
||||
states_and_entities = list(
|
||||
_filter_by_areas(states_and_entities, filter_areas, devices)
|
||||
)
|
||||
|
||||
if assistant is not None:
|
||||
# Filter by exposure
|
||||
@@ -318,9 +376,6 @@ def async_match_states(
|
||||
]
|
||||
|
||||
if name is not None:
|
||||
if devices is None:
|
||||
devices = device_registry.async_get(hass)
|
||||
|
||||
# Filter by name
|
||||
name = name.casefold()
|
||||
|
||||
@@ -389,7 +444,7 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
"""
|
||||
|
||||
slot_schema = {
|
||||
vol.Any("name", "area"): cv.string,
|
||||
vol.Any("name", "area", "floor"): cv.string,
|
||||
vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
@@ -453,7 +508,7 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
# Don't match on name if targeting all entities
|
||||
entity_name = None
|
||||
|
||||
# Look up area first to fail early
|
||||
# Look up area to fail early
|
||||
area_slot = slots.get("area", {})
|
||||
area_id = area_slot.get("value")
|
||||
area_name = area_slot.get("text")
|
||||
@@ -464,6 +519,17 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
if area is None:
|
||||
raise IntentHandleError(f"No area named {area_name}")
|
||||
|
||||
# Look up floor to fail early
|
||||
floor_slot = slots.get("floor", {})
|
||||
floor_id = floor_slot.get("value")
|
||||
floor_name = floor_slot.get("text")
|
||||
floor: floor_registry.FloorEntry | None = None
|
||||
if floor_id is not None:
|
||||
floors = floor_registry.async_get(hass)
|
||||
floor = floors.async_get_floor(floor_id)
|
||||
if floor is None:
|
||||
raise IntentHandleError(f"No floor named {floor_name}")
|
||||
|
||||
# Optional domain/device class filters.
|
||||
# Convert to sets for speed.
|
||||
domains: set[str] | None = None
|
||||
@@ -480,6 +546,7 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
hass,
|
||||
name=entity_name,
|
||||
area=area,
|
||||
floor=floor,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
assistant=intent_obj.assistant,
|
||||
@@ -491,6 +558,7 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
raise NoStatesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
floor=floor_name or floor_id,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
)
|
||||
|
||||
@@ -844,6 +844,48 @@ class EntitySelector(Selector[EntitySelectorConfig]):
|
||||
return cast(list, vol.Schema([validate])(data)) # Output is a list
|
||||
|
||||
|
||||
class FloorSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent an floor selector config."""
|
||||
|
||||
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
||||
multiple: bool
|
||||
|
||||
|
||||
@SELECTORS.register("floor")
|
||||
class FloorSelector(Selector[AreaSelectorConfig]):
|
||||
"""Selector of a single or list of floors."""
|
||||
|
||||
selector_type = "floor"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("entity"): vol.All(
|
||||
cv.ensure_list,
|
||||
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("device"): vol.All(
|
||||
cv.ensure_list,
|
||||
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, config: FloorSelectorConfig | None = None) -> None:
|
||||
"""Instantiate a selector."""
|
||||
super().__init__(config)
|
||||
|
||||
def __call__(self, data: Any) -> str | list[str]:
|
||||
"""Validate the passed selection."""
|
||||
if not self.config["multiple"]:
|
||||
floor_id: str = vol.Schema(str)(data)
|
||||
return floor_id
|
||||
if not isinstance(data, list):
|
||||
raise vol.Invalid("Value should be a list")
|
||||
return [vol.Schema(str)(val) for val in data]
|
||||
|
||||
|
||||
class IconSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent an icon selector config."""
|
||||
|
||||
|
||||
+15
-16
@@ -750,9 +750,7 @@ class Integration:
|
||||
self._import_futures: dict[str, asyncio.Future[ModuleType]] = {}
|
||||
cache: dict[str, ModuleType | ComponentProtocol] = hass.data[DATA_COMPONENTS]
|
||||
self._cache = cache
|
||||
missing_platforms_cache: dict[str, ImportError] = hass.data[
|
||||
DATA_MISSING_PLATFORMS
|
||||
]
|
||||
missing_platforms_cache: dict[str, bool] = hass.data[DATA_MISSING_PLATFORMS]
|
||||
self._missing_platforms_cache = missing_platforms_cache
|
||||
self._top_level_files = top_level_files or set()
|
||||
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
||||
@@ -1085,8 +1083,7 @@ class Integration:
|
||||
import_futures: list[tuple[str, asyncio.Future[ModuleType]]] = []
|
||||
|
||||
for platform_name in platform_names:
|
||||
full_name = f"{domain}.{platform_name}"
|
||||
if platform := self._get_platform_cached_or_raise(full_name):
|
||||
if platform := self._get_platform_cached_or_raise(platform_name):
|
||||
platforms[platform_name] = platform
|
||||
continue
|
||||
|
||||
@@ -1095,6 +1092,7 @@ class Integration:
|
||||
in_progress_imports[platform_name] = future
|
||||
continue
|
||||
|
||||
full_name = f"{domain}.{platform_name}"
|
||||
if (
|
||||
self.import_executor
|
||||
and full_name not in self.hass.config.components
|
||||
@@ -1166,14 +1164,18 @@ class Integration:
|
||||
|
||||
return platforms
|
||||
|
||||
def _get_platform_cached_or_raise(self, full_name: str) -> ModuleType | None:
|
||||
def _get_platform_cached_or_raise(self, platform_name: str) -> ModuleType | None:
|
||||
"""Return a platform for an integration from cache."""
|
||||
full_name = f"{self.domain}.{platform_name}"
|
||||
if full_name in self._cache:
|
||||
# the cache is either a ModuleType or a ComponentProtocol
|
||||
# but we only care about the ModuleType here
|
||||
return self._cache[full_name] # type: ignore[return-value]
|
||||
if full_name in self._missing_platforms_cache:
|
||||
raise self._missing_platforms_cache[full_name]
|
||||
raise ModuleNotFoundError(
|
||||
f"Platform {full_name} not found",
|
||||
name=f"{self.pkg_path}.{platform_name}",
|
||||
)
|
||||
return None
|
||||
|
||||
def platforms_are_loaded(self, platform_names: Iterable[str]) -> bool:
|
||||
@@ -1189,9 +1191,7 @@ class Integration:
|
||||
|
||||
def get_platform(self, platform_name: str) -> ModuleType:
|
||||
"""Return a platform for an integration."""
|
||||
if platform := self._get_platform_cached_or_raise(
|
||||
f"{self.domain}.{platform_name}"
|
||||
):
|
||||
if platform := self._get_platform_cached_or_raise(platform_name):
|
||||
return platform
|
||||
return self._load_platform(platform_name)
|
||||
|
||||
@@ -1212,10 +1212,7 @@ class Integration:
|
||||
):
|
||||
existing_platforms.append(platform_name)
|
||||
continue
|
||||
missing_platforms[full_name] = ModuleNotFoundError(
|
||||
f"Platform {full_name} not found",
|
||||
name=f"{self.pkg_path}.{platform_name}",
|
||||
)
|
||||
missing_platforms[full_name] = True
|
||||
|
||||
return existing_platforms
|
||||
|
||||
@@ -1233,11 +1230,13 @@ class Integration:
|
||||
cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS]
|
||||
try:
|
||||
cache[full_name] = self._import_platform(platform_name)
|
||||
except ImportError as ex:
|
||||
except ModuleNotFoundError:
|
||||
if self.domain in cache:
|
||||
# If the domain is loaded, cache that the platform
|
||||
# does not exist so we do not try to load it again
|
||||
self._missing_platforms_cache[full_name] = ex
|
||||
self._missing_platforms_cache[full_name] = True
|
||||
raise
|
||||
except ImportError:
|
||||
raise
|
||||
except RuntimeError as err:
|
||||
# _DeadlockError inherits from RuntimeError
|
||||
|
||||
@@ -30,8 +30,8 @@ habluetooth==2.4.2
|
||||
hass-nabucasa==0.79.0
|
||||
hassil==1.6.1
|
||||
home-assistant-bluetooth==1.12.0
|
||||
home-assistant-frontend==20240329.1
|
||||
home-assistant-intents==2024.3.27
|
||||
home-assistant-frontend==20240402.0
|
||||
home-assistant-intents==2024.3.29
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.3
|
||||
@@ -60,7 +60,7 @@ voluptuous-serialize==2.6.0
|
||||
voluptuous==0.13.1
|
||||
webrtc-noise-gain==1.2.3
|
||||
yarl==1.9.4
|
||||
zeroconf==0.131.0
|
||||
zeroconf==0.132.0
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
# see https://github.com/home-assistant/core/pull/16238
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.4.0b3"
|
||||
version = "2024.4.0b5"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -45,7 +45,7 @@ Mastodon.py==1.8.1
|
||||
Pillow==10.2.0
|
||||
|
||||
# homeassistant.components.plex
|
||||
PlexAPI==4.15.10
|
||||
PlexAPI==4.15.11
|
||||
|
||||
# homeassistant.components.progettihwsw
|
||||
ProgettiHWSW==0.1.3
|
||||
@@ -514,7 +514,7 @@ aurorapy==0.2.7
|
||||
# avion==0.10
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==59
|
||||
axis==60
|
||||
|
||||
# homeassistant.components.azure_event_hub
|
||||
azure-eventhub==5.11.1
|
||||
@@ -1077,10 +1077,10 @@ hole==0.8.0
|
||||
holidays==0.45
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240329.1
|
||||
home-assistant-frontend==20240402.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.3.27
|
||||
home-assistant-intents==2024.3.29
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -1482,7 +1482,7 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.4.1
|
||||
opower==0.4.2
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -2462,7 +2462,7 @@ rokuecp==0.19.2
|
||||
romy==0.0.7
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.13
|
||||
roombapy==1.8.1
|
||||
|
||||
# homeassistant.components.roon
|
||||
roonapi==0.1.6
|
||||
@@ -2795,7 +2795,7 @@ vallox-websocket-api==5.1.1
|
||||
vehicle==2.2.1
|
||||
|
||||
# homeassistant.components.velbus
|
||||
velbus-aio==2023.12.0
|
||||
velbus-aio==2024.4.0
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.19
|
||||
@@ -2847,7 +2847,7 @@ webmin-xmlrpc==0.0.2
|
||||
webrtc-noise-gain==1.2.3
|
||||
|
||||
# homeassistant.components.whirlpool
|
||||
whirlpool-sixth-sense==0.18.6
|
||||
whirlpool-sixth-sense==0.18.7
|
||||
|
||||
# homeassistant.components.whois
|
||||
whois==0.9.27
|
||||
@@ -2925,7 +2925,7 @@ zamg==0.3.6
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.131.0
|
||||
zeroconf==0.132.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.1
|
||||
|
||||
@@ -39,7 +39,7 @@ HATasmota==0.8.0
|
||||
Pillow==10.2.0
|
||||
|
||||
# homeassistant.components.plex
|
||||
PlexAPI==4.15.10
|
||||
PlexAPI==4.15.11
|
||||
|
||||
# homeassistant.components.progettihwsw
|
||||
ProgettiHWSW==0.1.3
|
||||
@@ -454,7 +454,7 @@ auroranoaa==0.0.3
|
||||
aurorapy==0.2.7
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==59
|
||||
axis==60
|
||||
|
||||
# homeassistant.components.azure_event_hub
|
||||
azure-eventhub==5.11.1
|
||||
@@ -876,10 +876,10 @@ hole==0.8.0
|
||||
holidays==0.45
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240329.1
|
||||
home-assistant-frontend==20240402.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.3.27
|
||||
home-assistant-intents==2024.3.29
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -1176,7 +1176,7 @@ openerz-api==0.3.0
|
||||
openhomedevice==2.2.0
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.4.1
|
||||
opower==0.4.2
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -1896,7 +1896,7 @@ rokuecp==0.19.2
|
||||
romy==0.0.7
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.13
|
||||
roombapy==1.8.1
|
||||
|
||||
# homeassistant.components.roon
|
||||
roonapi==0.1.6
|
||||
@@ -2151,7 +2151,7 @@ vallox-websocket-api==5.1.1
|
||||
vehicle==2.2.1
|
||||
|
||||
# homeassistant.components.velbus
|
||||
velbus-aio==2023.12.0
|
||||
velbus-aio==2024.4.0
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.19
|
||||
@@ -2194,7 +2194,7 @@ webmin-xmlrpc==0.0.2
|
||||
webrtc-noise-gain==1.2.3
|
||||
|
||||
# homeassistant.components.whirlpool
|
||||
whirlpool-sixth-sense==0.18.6
|
||||
whirlpool-sixth-sense==0.18.7
|
||||
|
||||
# homeassistant.components.whois
|
||||
whois==0.9.27
|
||||
@@ -2260,7 +2260,7 @@ yt-dlp==2024.03.10
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.131.0
|
||||
zeroconf==0.132.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.1
|
||||
|
||||
@@ -201,7 +201,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory)
|
||||
):
|
||||
freezer.tick(SCAN_INTERVAL * 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
power = hass.states.get("sensor.mydevicename_total_energy")
|
||||
assert power.state == "unknown"
|
||||
# sun rose again
|
||||
@@ -218,7 +218,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory)
|
||||
):
|
||||
freezer.tick(SCAN_INTERVAL * 4)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
power = hass.states.get("sensor.mydevicename_power_output")
|
||||
assert power is not None
|
||||
assert power.state == "45.7"
|
||||
@@ -237,7 +237,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory)
|
||||
):
|
||||
freezer.tick(SCAN_INTERVAL * 6)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
power = hass.states.get("sensor.mydevicename_power_output")
|
||||
assert power.state == "unknown" # should this be 'available'?
|
||||
|
||||
@@ -277,7 +277,7 @@ async def test_sensor_unknown_error(
|
||||
):
|
||||
freezer.tick(SCAN_INTERVAL * 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert (
|
||||
"Exception: AuroraError('another error') occurred, 2 retries remaining"
|
||||
in caplog.text
|
||||
|
||||
@@ -74,6 +74,7 @@ MQTT_CLIENT_RESPONSE = {
|
||||
"status": {"state": "active", "connectionStatus": "Connected"},
|
||||
"config": {
|
||||
"server": {"protocol": "tcp", "host": "192.168.0.90", "port": 1883},
|
||||
"deviceTopicPrefix": f"axis/{MAC}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -91,9 +91,9 @@ async def test_device_support_mqtt(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry
|
||||
) -> None:
|
||||
"""Successful setup."""
|
||||
mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8")
|
||||
mqtt_mock.async_subscribe.assert_called_with(f"axis/{MAC}/#", mock.ANY, 0, "utf-8")
|
||||
|
||||
topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0"
|
||||
topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0"
|
||||
message = (
|
||||
b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR",'
|
||||
b' "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}'
|
||||
|
||||
@@ -278,7 +278,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None:
|
||||
result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
|
||||
)
|
||||
assert result["type"] == "create_entry"
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entry = hass.config_entries.async_entries("cast")[0]
|
||||
|
||||
assert castbrowser_mock.return_value.start_discovery.call_count == 1
|
||||
@@ -291,7 +291,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None:
|
||||
user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
castbrowser_mock.return_value.start_discovery.assert_not_called()
|
||||
castbrowser_mock.assert_not_called()
|
||||
|
||||
@@ -137,8 +137,8 @@ async def async_setup_cast_internal_discovery(hass, config=None):
|
||||
return_value=browser,
|
||||
) as cast_browser:
|
||||
add_entities = await async_setup_cast(hass, config)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert browser.start_discovery.call_count == 1
|
||||
|
||||
@@ -209,8 +209,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf
|
||||
entry = MockConfigEntry(data=data, domain="cast")
|
||||
entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
discovery_callback = cast_browser.call_args[0][0].add_cast
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -480,6 +481,20 @@ async def test_error_no_area(hass: HomeAssistant, init_components) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_error_no_floor(hass: HomeAssistant, init_components) -> None:
|
||||
"""Test error message when floor is missing."""
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all the lights on missing floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Sorry, I am not aware of any floor called missing"
|
||||
)
|
||||
|
||||
|
||||
async def test_error_no_device_in_area(
|
||||
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
|
||||
) -> None:
|
||||
@@ -549,6 +564,48 @@ async def test_error_no_domain_in_area(
|
||||
)
|
||||
|
||||
|
||||
async def test_error_no_domain_in_floor(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
area_registry: ar.AreaRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test error message when no devices/entities for a domain exist on a floor."""
|
||||
floor_ground = floor_registry.async_create("ground")
|
||||
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||
area_kitchen = area_registry.async_update(
|
||||
area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id
|
||||
)
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all lights on the ground floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Sorry, I am not aware of any light on the ground floor"
|
||||
)
|
||||
|
||||
# Add a new floor/area to trigger registry event handlers
|
||||
floor_upstairs = floor_registry.async_create("upstairs")
|
||||
area_bedroom = area_registry.async_get_or_create("bedroom_id")
|
||||
area_bedroom = area_registry.async_update(
|
||||
area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id
|
||||
)
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all lights upstairs", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Sorry, I am not aware of any light on the upstairs floor"
|
||||
)
|
||||
|
||||
|
||||
async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None:
|
||||
"""Test error message when no entities of a device class exist."""
|
||||
|
||||
@@ -736,7 +793,7 @@ async def test_no_states_matched_default_error(
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.conversation.default_agent.intent.async_handle",
|
||||
side_effect=intent.NoStatesMatchedError(None, None, None, None),
|
||||
side_effect=intent.NoStatesMatchedError(),
|
||||
):
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on lights in the kitchen", None, Context(), None
|
||||
@@ -759,11 +816,16 @@ async def test_empty_aliases(
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test that empty aliases are not added to slot lists."""
|
||||
floor_1 = floor_registry.async_create("first floor", aliases={" "})
|
||||
|
||||
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
|
||||
area_kitchen = area_registry.async_update(area_kitchen.id, aliases={" "})
|
||||
area_kitchen = area_registry.async_update(
|
||||
area_kitchen.id, aliases={" "}, floor_id=floor_1
|
||||
)
|
||||
|
||||
entry = MockConfigEntry()
|
||||
entry.add_to_hass(hass)
|
||||
@@ -799,7 +861,7 @@ async def test_empty_aliases(
|
||||
slot_lists = mock_recognize_all.call_args[0][2]
|
||||
|
||||
# Slot lists should only contain non-empty text
|
||||
assert slot_lists.keys() == {"area", "name"}
|
||||
assert slot_lists.keys() == {"area", "name", "floor"}
|
||||
areas = slot_lists["area"]
|
||||
assert len(areas.values) == 1
|
||||
assert areas.values[0].value_out == area_kitchen.id
|
||||
@@ -810,6 +872,11 @@ async def test_empty_aliases(
|
||||
assert names.values[0].value_out == kitchen_light.name
|
||||
assert names.values[0].text_in.text == kitchen_light.name
|
||||
|
||||
floors = slot_lists["floor"]
|
||||
assert len(floors.values) == 1
|
||||
assert floors.values[0].value_out == floor_1.floor_id
|
||||
assert floors.values[0].text_in.text == floor_1.name
|
||||
|
||||
|
||||
async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None:
|
||||
"""Test that sentences for all domains are always loaded."""
|
||||
|
||||
@@ -2,14 +2,26 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import conversation, cover, media_player, vacuum, valve
|
||||
from homeassistant.components import (
|
||||
conversation,
|
||||
cover,
|
||||
light,
|
||||
media_player,
|
||||
vacuum,
|
||||
valve,
|
||||
)
|
||||
from homeassistant.components.cover import intent as cover_intent
|
||||
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
|
||||
from homeassistant.components.media_player import intent as media_player_intent
|
||||
from homeassistant.components.vacuum import intent as vaccum_intent
|
||||
from homeassistant.const import STATE_CLOSED
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_mock_service
|
||||
@@ -244,3 +256,92 @@ async def test_media_player_intents(
|
||||
"entity_id": entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.75,
|
||||
}
|
||||
|
||||
|
||||
async def test_turn_floor_lights_on_off(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
entity_registry: er.EntityRegistry,
|
||||
area_registry: ar.AreaRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test that we can turn lights on/off for an entire floor."""
|
||||
floor_ground = floor_registry.async_create("ground", aliases={"downstairs"})
|
||||
floor_upstairs = floor_registry.async_create("upstairs")
|
||||
|
||||
# Kitchen and living room are on the ground floor
|
||||
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||
area_kitchen = area_registry.async_update(
|
||||
area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id
|
||||
)
|
||||
|
||||
area_living_room = area_registry.async_get_or_create("living_room_id")
|
||||
area_living_room = area_registry.async_update(
|
||||
area_living_room.id, name="living_room", floor_id=floor_ground.floor_id
|
||||
)
|
||||
|
||||
# Bedroom is upstairs
|
||||
area_bedroom = area_registry.async_get_or_create("bedroom_id")
|
||||
area_bedroom = area_registry.async_update(
|
||||
area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id
|
||||
)
|
||||
|
||||
# One light per area
|
||||
kitchen_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "kitchen_light"
|
||||
)
|
||||
kitchen_light = entity_registry.async_update_entity(
|
||||
kitchen_light.entity_id, area_id=area_kitchen.id
|
||||
)
|
||||
hass.states.async_set(kitchen_light.entity_id, "off")
|
||||
|
||||
living_room_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "living_room_light"
|
||||
)
|
||||
living_room_light = entity_registry.async_update_entity(
|
||||
living_room_light.entity_id, area_id=area_living_room.id
|
||||
)
|
||||
hass.states.async_set(living_room_light.entity_id, "off")
|
||||
|
||||
bedroom_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "bedroom_light"
|
||||
)
|
||||
bedroom_light = entity_registry.async_update_entity(
|
||||
bedroom_light.entity_id, area_id=area_bedroom.id
|
||||
)
|
||||
hass.states.async_set(bedroom_light.entity_id, "off")
|
||||
|
||||
# Target by floor
|
||||
on_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all lights downstairs", None, Context(), None
|
||||
)
|
||||
|
||||
assert len(on_calls) == 2
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
kitchen_light.entity_id,
|
||||
living_room_light.entity_id,
|
||||
}
|
||||
|
||||
on_calls.clear()
|
||||
result = await conversation.async_converse(
|
||||
hass, "upstairs lights on", None, Context(), None
|
||||
)
|
||||
|
||||
assert len(on_calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
bedroom_light.entity_id
|
||||
}
|
||||
|
||||
off_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_OFF)
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn upstairs lights off", None, Context(), None
|
||||
)
|
||||
|
||||
assert len(off_calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
bedroom_light.entity_id
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ async def test_image_update_unavailable(
|
||||
# fritzbox becomes unavailable
|
||||
fc_class_mock().call_action_side_effect(ReadTimeout)
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("image.mock_title_guestwifi")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
@@ -207,7 +207,7 @@ async def test_image_update_unavailable(
|
||||
# fritzbox is available again
|
||||
fc_class_mock().call_action_side_effect(None)
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("image.mock_title_guestwifi")
|
||||
assert state.state != STATE_UNKNOWN
|
||||
|
||||
@@ -134,7 +134,7 @@ async def test_sensor_update_fail(
|
||||
|
||||
fc_class_mock().call_action_side_effect(FritzConnectionException)
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
sensors = hass.states.async_all(SENSOR_DOMAIN)
|
||||
for sensor in sensors:
|
||||
|
||||
@@ -104,7 +104,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -123,7 +123,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -146,7 +146,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_device_alarm")
|
||||
assert state
|
||||
|
||||
@@ -65,7 +65,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_template")
|
||||
assert state
|
||||
|
||||
@@ -145,7 +145,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_preset")
|
||||
assert state
|
||||
@@ -203,7 +203,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
@@ -243,7 +243,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 4
|
||||
assert fritz().login.call_count == 4
|
||||
@@ -386,7 +386,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
@@ -397,7 +397,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert fritz().update_devices.call_count == 3
|
||||
@@ -422,7 +422,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_climate")
|
||||
assert state
|
||||
|
||||
@@ -108,7 +108,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_climate")
|
||||
assert state
|
||||
|
||||
@@ -237,7 +237,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -259,7 +259,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 4
|
||||
assert fritz().login.call_count == 4
|
||||
@@ -294,7 +294,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_light")
|
||||
assert state
|
||||
|
||||
@@ -87,7 +87,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -105,7 +105,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 4
|
||||
assert fritz().login.call_count == 4
|
||||
@@ -128,7 +128,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_device_temperature")
|
||||
assert state
|
||||
|
||||
@@ -151,7 +151,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -169,7 +169,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 4
|
||||
assert fritz().login.call_count == 4
|
||||
@@ -207,7 +207,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_switch")
|
||||
assert state
|
||||
|
||||
@@ -99,7 +99,7 @@ async def test_setup(
|
||||
# so no changes to entities.
|
||||
mock_feed.return_value.update.return_value = "OK_NO_DATA", None
|
||||
async_fire_time_changed(hass, utcnow + geo_rss_events.SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
all_states = hass.states.async_all()
|
||||
assert len(all_states) == 1
|
||||
@@ -109,7 +109,7 @@ async def test_setup(
|
||||
# Simulate an update - empty data, removes all entities
|
||||
mock_feed.return_value.update.return_value = "ERROR", None
|
||||
async_fire_time_changed(hass, utcnow + 2 * geo_rss_events.SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
all_states = hass.states.async_all()
|
||||
assert len(all_states) == 1
|
||||
|
||||
@@ -46,7 +46,7 @@ async def test_sensors(
|
||||
):
|
||||
next_update = dt_util.utcnow() + timedelta(minutes=15)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(SENSOR)
|
||||
assert state.state == result
|
||||
@@ -61,7 +61,7 @@ async def test_sensor_reauth_trigger(
|
||||
with patch(TOKEN, side_effect=RefreshError):
|
||||
next_update = dt_util.utcnow() + timedelta(minutes=15)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ async def test_window_shuttler(
|
||||
|
||||
windowshutter.is_open = False
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
@@ -68,12 +68,12 @@ async def test_window_shuttler_battery(
|
||||
|
||||
windowshutter.battery = 1 # maxcube-api MAX_DEVICE_BATTERY_LOW
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(BATTERY_ENTITY_ID)
|
||||
assert state.state == STATE_ON # on means low
|
||||
|
||||
windowshutter.battery = 0 # maxcube-api MAX_DEVICE_BATTERY_OK
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(BATTERY_ENTITY_ID)
|
||||
assert state.state == STATE_OFF # off means normal
|
||||
|
||||
@@ -140,7 +140,7 @@ async def test_thermostat_set_hvac_mode_off(
|
||||
thermostat.valve_position = 0
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.OFF
|
||||
@@ -168,8 +168,8 @@ async def test_thermostat_set_hvac_mode_heat(
|
||||
thermostat.mode = MAX_DEVICE_MODE_MANUAL
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -204,7 +204,7 @@ async def test_thermostat_set_temperature(
|
||||
thermostat.valve_position = 0
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.AUTO
|
||||
@@ -248,7 +248,7 @@ async def test_thermostat_set_preset_on(
|
||||
thermostat.target_temperature = ON_TEMPERATURE
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -273,7 +273,7 @@ async def test_thermostat_set_preset_comfort(
|
||||
thermostat.target_temperature = thermostat.comfort_temperature
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -298,7 +298,7 @@ async def test_thermostat_set_preset_eco(
|
||||
thermostat.target_temperature = thermostat.eco_temperature
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -323,7 +323,7 @@ async def test_thermostat_set_preset_away(
|
||||
thermostat.target_temperature = thermostat.eco_temperature
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -348,7 +348,7 @@ async def test_thermostat_set_preset_boost(
|
||||
thermostat.target_temperature = thermostat.eco_temperature
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.AUTO
|
||||
@@ -401,7 +401,7 @@ async def test_wallthermostat_set_hvac_mode_heat(
|
||||
wallthermostat.target_temperature = MIN_TEMPERATURE
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(WALL_ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -425,7 +425,7 @@ async def test_wallthermostat_set_hvac_mode_auto(
|
||||
wallthermostat.target_temperature = 23.0
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(WALL_ENTITY_ID)
|
||||
assert state.state == HVACMode.AUTO
|
||||
|
||||
@@ -125,7 +125,7 @@ async def test_site_cannot_update(
|
||||
|
||||
future_time = utcnow() + timedelta(minutes=20)
|
||||
async_fire_time_changed(hass, future_time)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
weather = hass.states.get("weather.met_office_wavertree_daily")
|
||||
assert weather.state == STATE_UNAVAILABLE
|
||||
@@ -297,7 +297,7 @@ async def test_forecast_service(
|
||||
# Trigger data refetch
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert wavertree_data["wavertree_daily_mock"].call_count == 2
|
||||
assert wavertree_data["wavertree_hourly_mock"].call_count == 1
|
||||
@@ -324,7 +324,7 @@ async def test_forecast_service(
|
||||
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
@@ -412,7 +412,7 @@ async def test_forecast_subscription(
|
||||
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["id"] == subscription_id
|
||||
@@ -430,6 +430,6 @@ async def test_forecast_subscription(
|
||||
)
|
||||
freezer.tick(timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
@@ -88,7 +88,7 @@ async def test_device_trackers(
|
||||
WIRELESS_DATA.append(DEVICE_2_WIRELESS)
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
device_2 = hass.states.get("device_tracker.device_2")
|
||||
assert device_2
|
||||
@@ -101,7 +101,7 @@ async def test_device_trackers(
|
||||
del WIRELESS_DATA[1] # device 2 is removed from wireless list
|
||||
with freeze_time(utcnow() + timedelta(minutes=4)):
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=4))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
device_2 = hass.states.get("device_tracker.device_2")
|
||||
assert device_2
|
||||
@@ -110,7 +110,7 @@ async def test_device_trackers(
|
||||
# test state changes to away if last_seen past consider_home_interval
|
||||
with freeze_time(utcnow() + timedelta(minutes=6)):
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=6))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
device_2 = hass.states.get("device_tracker.device_2")
|
||||
assert device_2
|
||||
@@ -266,7 +266,7 @@ async def test_update_failed(hass: HomeAssistant, mock_device_registry_devices)
|
||||
mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect
|
||||
):
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
device_1 = hass.states.get("device_tracker.device_1")
|
||||
assert device_1
|
||||
|
||||
@@ -183,7 +183,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None:
|
||||
# Restoring other media player to its previous state
|
||||
# The zone should not be restored
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Checking that values were not (!) restored
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
@@ -193,7 +193,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None:
|
||||
|
||||
# Restoring media player to its previous state
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -226,7 +226,7 @@ async def test_service_calls_with_all_entities(hass: HomeAssistant) -> None:
|
||||
|
||||
# Restoring media player to its previous state
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "all"})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -259,7 +259,7 @@ async def test_service_calls_without_relevant_entities(hass: HomeAssistant) -> N
|
||||
|
||||
# Restoring media player to its previous state
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -273,7 +273,7 @@ async def test_restore_without_snapshort(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch.object(MockMonoprice, "restore_zone") as method_call:
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert not method_call.called
|
||||
|
||||
@@ -295,7 +295,7 @@ async def test_update(hass: HomeAssistant) -> None:
|
||||
monoprice.set_volume(11, 38)
|
||||
|
||||
await async_update_entity(hass, ZONE_1_ID)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -321,7 +321,7 @@ async def test_failed_update(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch.object(MockMonoprice, "zone_status", side_effect=SerialException):
|
||||
await async_update_entity(hass, ZONE_1_ID)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -347,7 +347,7 @@ async def test_empty_update(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch.object(MockMonoprice, "zone_status", return_value=None):
|
||||
await async_update_entity(hass, ZONE_1_ID)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -418,7 +418,7 @@ async def test_unknown_source(hass: HomeAssistant) -> None:
|
||||
monoprice.set_source(11, 5)
|
||||
|
||||
await async_update_entity(hass, ZONE_1_ID)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ async def test_media_player_handle_URLerror(
|
||||
mock_remote.get_mute = Mock(side_effect=URLError(None, None))
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=2))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state_tv = hass.states.get("media_player.panasonic_viera_tv")
|
||||
assert state_tv.state == STATE_UNAVAILABLE
|
||||
@@ -41,7 +41,7 @@ async def test_media_player_handle_HTTPError(
|
||||
mock_remote.get_mute = Mock(side_effect=HTTPError(None, 400, None, None, None))
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=2))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state_tv = hass.states.get("media_player.panasonic_viera_tv")
|
||||
assert state_tv.state == STATE_OFF
|
||||
|
||||
@@ -208,7 +208,7 @@ async def test_update_unavailable(projector_from_address, hass: HomeAssistant) -
|
||||
|
||||
projector_from_address.side_effect = socket.timeout
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("media_player.test")
|
||||
assert state.state == "unavailable"
|
||||
@@ -237,7 +237,7 @@ async def test_unavailable_time(mocked_projector, hass: HomeAssistant) -> None:
|
||||
|
||||
mocked_projector.get_power.side_effect = ProjectorError("unavailable time")
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("media_player.test")
|
||||
assert state.state == "off"
|
||||
|
||||
@@ -332,7 +332,7 @@ async def test_log_object_sources(
|
||||
caplog.clear()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=11))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert "No new object growth found" in caplog.text
|
||||
|
||||
fake_object2 = FakeObject()
|
||||
@@ -344,7 +344,7 @@ async def test_log_object_sources(
|
||||
caplog.clear()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=21))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert "New object FakeObject (1/2)" in caplog.text
|
||||
|
||||
many_objects = [FakeObject() for _ in range(30)]
|
||||
@@ -352,7 +352,7 @@ async def test_log_object_sources(
|
||||
caplog.clear()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert "New object FakeObject (2/30)" in caplog.text
|
||||
assert "New objects overflowed by {'FakeObject': 25}" in caplog.text
|
||||
|
||||
@@ -362,7 +362,7 @@ async def test_log_object_sources(
|
||||
caplog.clear()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=41))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert "FakeObject" not in caplog.text
|
||||
assert "No new object growth found" not in caplog.text
|
||||
|
||||
@@ -370,7 +370,7 @@ async def test_log_object_sources(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=51))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert "FakeObject" not in caplog.text
|
||||
assert "No new object growth found" not in caplog.text
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch(mock_func, return_value=mock_result) as mock_fetch:
|
||||
await mock_ddp_response(hass, MOCK_STATUS_PLAYING)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
mock_state = hass.states.get(mock_entity_id)
|
||||
mock_attrs = dict(mock_state.attributes)
|
||||
@@ -255,6 +256,7 @@ async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch(mock_func, return_value=mock_result) as mock_fetch_app:
|
||||
await mock_ddp_response(hass, MOCK_STATUS_PLAYING)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
mock_state = hass.states.get(mock_entity_id)
|
||||
mock_attrs = dict(mock_state.attributes)
|
||||
|
||||
@@ -78,7 +78,7 @@ hass.states.set('test.entity', data.get('name', 'not set'))
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {"name": "paulus"})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.is_state("test.entity", "paulus")
|
||||
|
||||
@@ -96,7 +96,7 @@ print("This triggers warning.")
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "Don't use print() inside scripts." in caplog.text
|
||||
|
||||
@@ -111,7 +111,7 @@ logger.info('Logging from inside script')
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "Logging from inside script" in caplog.text
|
||||
|
||||
@@ -126,7 +126,7 @@ this is not valid Python
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "Error loading script test.py" in caplog.text
|
||||
|
||||
@@ -140,8 +140,8 @@ async def test_execute_runtime_error(
|
||||
raise Exception('boom')
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "Error executing script: boom" in caplog.text
|
||||
|
||||
@@ -153,7 +153,7 @@ raise Exception('boom')
|
||||
"""
|
||||
|
||||
task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert type(task.exception()) == HomeAssistantError
|
||||
assert "Error executing script (Exception): boom" in str(task.exception())
|
||||
@@ -168,7 +168,7 @@ async def test_accessing_async_methods(
|
||||
hass.async_stop()
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Not allowed to access async methods" in caplog.text
|
||||
@@ -181,7 +181,7 @@ hass.async_stop()
|
||||
"""
|
||||
|
||||
task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert type(task.exception()) == ServiceValidationError
|
||||
assert "Not allowed to access async methods" in str(task.exception())
|
||||
@@ -198,7 +198,7 @@ mylist = [1, 2, 3, 4]
|
||||
logger.info('Logging from inside script: %s %s' % (mydict["a"], mylist[2]))
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Logging from inside script: 1 3" in caplog.text
|
||||
@@ -217,7 +217,7 @@ async def test_accessing_forbidden_methods(
|
||||
"time.tzset()": "TimeWrapper.tzset",
|
||||
}.items():
|
||||
caplog.records.clear()
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
assert f"Not allowed to access {name}" in caplog.text
|
||||
|
||||
@@ -231,7 +231,7 @@ async def test_accessing_forbidden_methods_with_response(hass: HomeAssistant) ->
|
||||
"time.tzset()": "TimeWrapper.tzset",
|
||||
}.items():
|
||||
task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert type(task.exception()) == ServiceValidationError
|
||||
assert f"Not allowed to access {name}" in str(task.exception())
|
||||
@@ -244,7 +244,7 @@ for i in [1, 2]:
|
||||
hass.states.set('hello.{}'.format(i), 'world')
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.is_state("hello.1", "world")
|
||||
@@ -279,7 +279,7 @@ hass.states.set('hello.ab_list', '{}'.format(ab_list))
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.is_state("hello.a", "1")
|
||||
assert hass.states.is_state("hello.b", "2")
|
||||
@@ -302,7 +302,7 @@ hass.states.set('hello.b', a[1])
|
||||
hass.states.set('hello.c', a[2])
|
||||
"""
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.is_state("hello.a", "1")
|
||||
assert hass.states.is_state("hello.b", "2")
|
||||
@@ -325,7 +325,7 @@ hass.states.set('module.datetime',
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.is_state("module.time", "1986")
|
||||
assert hass.states.is_state("module.time_strptime", "12:34")
|
||||
@@ -351,7 +351,7 @@ def b():
|
||||
b()
|
||||
"""
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.is_state("hello.a", "one")
|
||||
assert hass.states.is_state("hello.b", "two")
|
||||
@@ -517,7 +517,7 @@ time.sleep(5)
|
||||
|
||||
with patch("homeassistant.components.python_script.time.sleep"):
|
||||
hass.async_add_executor_job(execute, hass, "test.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert caplog.text.count("time.sleep") == 1
|
||||
|
||||
@@ -664,7 +664,7 @@ hass.states.set('hello.c', c)
|
||||
"""
|
||||
|
||||
hass.async_add_executor_job(execute, hass, "aug_assign.py", source, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.get("hello.a").state == str(((10 + 20) * 5) - 8)
|
||||
assert hass.states.get("hello.b").state == ("foo" + "bar") * 2
|
||||
@@ -686,5 +686,5 @@ async def test_prohibited_augmented_assignment_operations(
|
||||
) -> None:
|
||||
"""Test that prohibited augmented assignment operations raise an error."""
|
||||
hass.async_add_executor_job(execute, hass, "aug_assign_prohibited.py", case, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert error in caplog.text
|
||||
|
||||
@@ -65,6 +65,17 @@ async def setup_component(hass: HomeAssistant) -> None:
|
||||
assert await async_setup_component(hass, MEDIA_STREAM_DOMAIN, {})
|
||||
|
||||
|
||||
async def test_platform_loads_before_config_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the platform can be loaded before the config entry."""
|
||||
# Fake that the config entry is not loaded before the media_source platform
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
assert mock_setup_entry.call_count == 0
|
||||
|
||||
|
||||
async def test_resolve(
|
||||
hass: HomeAssistant,
|
||||
reolink_connect: MagicMock,
|
||||
|
||||
@@ -99,12 +99,12 @@ def _mocked_discovery(*_):
|
||||
|
||||
roomba = RoombaInfo(
|
||||
hostname="irobot-BLID",
|
||||
robotname="robot_name",
|
||||
robot_name="robot_name",
|
||||
ip=MOCK_IP,
|
||||
mac="mac",
|
||||
sw="firmware",
|
||||
firmware="firmware",
|
||||
sku="sku",
|
||||
cap={"cap": 1},
|
||||
capabilities={"cap": 1},
|
||||
)
|
||||
|
||||
roomba_discovery.get_all = MagicMock(return_value=[roomba])
|
||||
|
||||
@@ -200,7 +200,7 @@ async def test_setup_websocket_2(
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
freezer.move_to(next_update)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
@@ -225,7 +225,7 @@ async def test_setup_encrypted_websocket(
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
freezer.move_to(next_update)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state
|
||||
@@ -242,7 +242,7 @@ async def test_update_on(
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
freezer.move_to(next_update)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_ON
|
||||
@@ -262,7 +262,7 @@ async def test_update_off(
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
freezer.move_to(next_update)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
@@ -290,7 +290,7 @@ async def test_update_off_ws_no_power_state(
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
freezer.move_to(next_update)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
@@ -22,7 +22,7 @@ async def test_keypad_disabled_binary_sensor(
|
||||
|
||||
# Make the coordinator refresh data.
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled")
|
||||
assert keypad is not None
|
||||
@@ -43,7 +43,7 @@ async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure(
|
||||
|
||||
# Make the coordinator refresh data.
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled")
|
||||
assert keypad is not None
|
||||
|
||||
@@ -59,7 +59,7 @@ async def test_changed_by(
|
||||
|
||||
# Make the coordinator refresh data.
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
mock_lock.last_changed_by.assert_called_once_with()
|
||||
|
||||
lock_device = hass.states.get("lock.vault_door")
|
||||
|
||||
@@ -261,7 +261,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
|
||||
|
||||
mocker.payload = "test_scrape_sensor_no_data"
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.ha_version")
|
||||
assert state is not None
|
||||
@@ -541,7 +541,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None:
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(minutes=10),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.get_values_with_template")
|
||||
assert state.state == "Current Version: 2021.12.10"
|
||||
@@ -555,7 +555,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None:
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(minutes=20),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.get_values_with_template")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
@@ -568,7 +568,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None:
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(minutes=30),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.get_values_with_template")
|
||||
assert state.state == "Current Version: 2021.12.10"
|
||||
@@ -608,7 +608,7 @@ async def test_availability(
|
||||
hass.states.async_set("sensor.input1", "on")
|
||||
freezer.tick(timedelta(minutes=10))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.current_version")
|
||||
assert state.state == "2021.12.10"
|
||||
@@ -618,7 +618,7 @@ async def test_availability(
|
||||
|
||||
freezer.tick(timedelta(minutes=10))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.current_version")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
@@ -126,6 +126,18 @@ def register_entity(
|
||||
return f"{domain}.{object_id}"
|
||||
|
||||
|
||||
def get_entity(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
unique_id: str,
|
||||
) -> str | None:
|
||||
"""Get Shelly entity."""
|
||||
entity_registry = async_get(hass)
|
||||
return entity_registry.async_get_entity_id(
|
||||
domain, DOMAIN, f"{MOCK_MAC}-{unique_id}"
|
||||
)
|
||||
|
||||
|
||||
def get_entity_state(hass: HomeAssistant, entity_id: str) -> str:
|
||||
"""Return entity state."""
|
||||
entity = hass.states.get(entity_id)
|
||||
|
||||
@@ -169,6 +169,9 @@ MOCK_CONFIG = {
|
||||
"input:1": {"id": 1, "type": "analog", "enable": True},
|
||||
"input:2": {"id": 2, "name": "Gas", "type": "count", "enable": True},
|
||||
"light:0": {"name": "test light_0"},
|
||||
"light:1": {"name": "test light_1"},
|
||||
"light:2": {"name": "test light_2"},
|
||||
"light:3": {"name": "test light_3"},
|
||||
"rgb:0": {"name": "test rgb_0"},
|
||||
"rgbw:0": {"name": "test rgbw_0"},
|
||||
"switch:0": {"name": "test switch_0"},
|
||||
@@ -225,6 +228,9 @@ MOCK_STATUS_RPC = {
|
||||
"input:1": {"id": 1, "percent": 89, "xpercent": 8.9},
|
||||
"input:2": {"id": 2, "counts": {"total": 56174, "xtotal": 561.74}},
|
||||
"light:0": {"output": True, "brightness": 53.0},
|
||||
"light:1": {"output": True, "brightness": 53.0},
|
||||
"light:2": {"output": True, "brightness": 53.0},
|
||||
"light:3": {"output": True, "brightness": 53.0},
|
||||
"rgb:0": {"output": True, "brightness": 53.0, "rgb": [45, 55, 65]},
|
||||
"rgbw:0": {"output": True, "brightness": 53.0, "rgb": [21, 22, 23], "white": 120},
|
||||
"cloud": {"connected": False},
|
||||
|
||||
@@ -29,6 +29,7 @@ from homeassistant.components.light import (
|
||||
ColorMode,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.components.shelly.const import SHELLY_PLUS_RGBW_CHANNELS
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
@@ -38,7 +39,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
|
||||
from . import init_integration, mutate_rpc_device_status
|
||||
from . import get_entity, init_integration, mutate_rpc_device_status, register_entity
|
||||
from .conftest import mock_white_light_set_state
|
||||
|
||||
RELAY_BLOCK_ID = 0
|
||||
@@ -587,7 +588,8 @@ async def test_rpc_device_rgb_profile(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test RPC device in RGB profile."""
|
||||
monkeypatch.delitem(mock_rpc_device.status, "light:0")
|
||||
for i in range(SHELLY_PLUS_RGBW_CHANNELS):
|
||||
monkeypatch.delitem(mock_rpc_device.status, f"light:{i}")
|
||||
monkeypatch.delitem(mock_rpc_device.status, "rgbw:0")
|
||||
entity_id = "light.test_rgb_0"
|
||||
await init_integration(hass, 2)
|
||||
@@ -633,7 +635,8 @@ async def test_rpc_device_rgbw_profile(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test RPC device in RGBW profile."""
|
||||
monkeypatch.delitem(mock_rpc_device.status, "light:0")
|
||||
for i in range(SHELLY_PLUS_RGBW_CHANNELS):
|
||||
monkeypatch.delitem(mock_rpc_device.status, f"light:{i}")
|
||||
monkeypatch.delitem(mock_rpc_device.status, "rgb:0")
|
||||
entity_id = "light.test_rgbw_0"
|
||||
await init_integration(hass, 2)
|
||||
@@ -673,3 +676,82 @@ async def test_rpc_device_rgbw_profile(
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.unique_id == "123456789ABC-rgbw:0"
|
||||
|
||||
|
||||
async def test_rpc_rgbw_device_light_mode_remove_others(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test Shelly RPC RGBW device in light mode removes RGB/RGBW entities."""
|
||||
# register lights
|
||||
monkeypatch.delitem(mock_rpc_device.status, "rgb:0")
|
||||
monkeypatch.delitem(mock_rpc_device.status, "rgbw:0")
|
||||
register_entity(hass, LIGHT_DOMAIN, "test_rgb_0", "rgb:0")
|
||||
register_entity(hass, LIGHT_DOMAIN, "test_rgbw_0", "rgbw:0")
|
||||
|
||||
# verify RGB & RGBW entities created
|
||||
assert get_entity(hass, LIGHT_DOMAIN, "rgb:0") is not None
|
||||
assert get_entity(hass, LIGHT_DOMAIN, "rgbw:0") is not None
|
||||
|
||||
# init to remove RGB & RGBW
|
||||
await init_integration(hass, 2)
|
||||
|
||||
# verify we have 4 lights
|
||||
for i in range(SHELLY_PLUS_RGBW_CHANNELS):
|
||||
entity_id = f"light.test_light_{i}"
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.unique_id == f"123456789ABC-light:{i}"
|
||||
|
||||
# verify RGB & RGBW entities removed
|
||||
assert get_entity(hass, LIGHT_DOMAIN, "rgb:0") is None
|
||||
assert get_entity(hass, LIGHT_DOMAIN, "rgbw:0") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("active_mode", "removed_mode"),
|
||||
[
|
||||
("rgb", "rgbw"),
|
||||
("rgbw", "rgb"),
|
||||
],
|
||||
)
|
||||
async def test_rpc_rgbw_device_rgb_w_modes_remove_others(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
active_mode: str,
|
||||
removed_mode: str,
|
||||
) -> None:
|
||||
"""Test Shelly RPC RGBW device in RGB/W modes other lights."""
|
||||
removed_key = f"{removed_mode}:0"
|
||||
|
||||
# register lights
|
||||
for i in range(SHELLY_PLUS_RGBW_CHANNELS):
|
||||
monkeypatch.delitem(mock_rpc_device.status, f"light:{i}")
|
||||
entity_id = f"light.test_light_{i}"
|
||||
register_entity(hass, LIGHT_DOMAIN, entity_id, f"light:{i}")
|
||||
monkeypatch.delitem(mock_rpc_device.status, f"{removed_mode}:0")
|
||||
register_entity(hass, LIGHT_DOMAIN, f"test_{removed_key}", removed_key)
|
||||
|
||||
# verify lights entities created
|
||||
for i in range(SHELLY_PLUS_RGBW_CHANNELS):
|
||||
assert get_entity(hass, LIGHT_DOMAIN, f"light:{i}") is not None
|
||||
assert get_entity(hass, LIGHT_DOMAIN, removed_key) is not None
|
||||
|
||||
await init_integration(hass, 2)
|
||||
|
||||
# verify we have RGB/w light
|
||||
entity_id = f"light.test_{active_mode}_0"
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.unique_id == f"123456789ABC-{active_mode}:0"
|
||||
|
||||
# verify light & RGB/W entities removed
|
||||
for i in range(SHELLY_PLUS_RGBW_CHANNELS):
|
||||
assert get_entity(hass, LIGHT_DOMAIN, f"light:{i}") is None
|
||||
assert get_entity(hass, LIGHT_DOMAIN, removed_key) is None
|
||||
|
||||
@@ -255,6 +255,16 @@ async def test_rpc_update(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_rpc_device.trigger_ota_update.call_count == 1
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
|
||||
assert state.attributes[ATTR_LATEST_VERSION] == "2"
|
||||
assert state.attributes[ATTR_IN_PROGRESS] is True
|
||||
assert state.attributes[ATTR_RELEASE_URL] == GEN2_RELEASE_URL
|
||||
|
||||
inject_rpc_device_event(
|
||||
monkeypatch,
|
||||
mock_rpc_device,
|
||||
@@ -270,14 +280,7 @@ async def test_rpc_update(
|
||||
},
|
||||
)
|
||||
|
||||
assert mock_rpc_device.trigger_ota_update.call_count == 1
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
|
||||
assert state.attributes[ATTR_LATEST_VERSION] == "2"
|
||||
assert state.attributes[ATTR_IN_PROGRESS] == 0
|
||||
assert state.attributes[ATTR_RELEASE_URL] == GEN2_RELEASE_URL
|
||||
assert hass.states.get(entity_id).attributes[ATTR_IN_PROGRESS] == 0
|
||||
|
||||
inject_rpc_device_event(
|
||||
monkeypatch,
|
||||
|
||||
@@ -53,7 +53,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity(
|
||||
mock_solaredge().get_overview.return_value = mock_overview_data
|
||||
freezer.tick(OVERVIEW_UPDATE_DELAY)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get("sensor.solaredge_lifetime_energy")
|
||||
assert state
|
||||
assert state.state == str(mock_overview_data["overview"]["lifeTimeData"]["energy"])
|
||||
@@ -63,7 +63,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity(
|
||||
mock_solaredge().get_overview.return_value = mock_overview_data
|
||||
freezer.tick(OVERVIEW_UPDATE_DELAY)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.solaredge_lifetime_energy")
|
||||
assert state
|
||||
@@ -74,7 +74,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity(
|
||||
mock_solaredge().get_overview.return_value = mock_overview_data
|
||||
freezer.tick(OVERVIEW_UPDATE_DELAY)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.solaredge_lifetime_energy")
|
||||
assert state
|
||||
@@ -85,7 +85,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity(
|
||||
mock_solaredge().get_overview.return_value = mock_overview_data
|
||||
freezer.tick(OVERVIEW_UPDATE_DELAY)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.solaredge_energy_this_year")
|
||||
assert state
|
||||
@@ -103,7 +103,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity(
|
||||
mock_solaredge().get_overview.return_value = mock_overview_data
|
||||
freezer.tick(OVERVIEW_UPDATE_DELAY)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.solaredge_lifetime_energy")
|
||||
assert state
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
"""Configuration for Sonos tests."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from copy import copy
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from soco import SoCo
|
||||
from soco.events_base import Event as SonosEvent
|
||||
|
||||
from homeassistant.components import ssdp, zeroconf
|
||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.components.sonos import DOMAIN
|
||||
from homeassistant.const import CONF_HOSTS
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
@@ -30,6 +34,31 @@ class SonosMockSubscribe:
|
||||
"""Initialize the mock subscriber."""
|
||||
self.event_listener = SonosMockEventListener(ip_address)
|
||||
self.service = Mock()
|
||||
self.callback_future: asyncio.Future[Callable[[SonosEvent], None]] = None
|
||||
self._callback: Callable[[SonosEvent], None] | None = None
|
||||
|
||||
@property
|
||||
def callback(self) -> Callable[[SonosEvent], None] | None:
|
||||
"""Return the callback."""
|
||||
return self._callback
|
||||
|
||||
@callback.setter
|
||||
def callback(self, callback: Callable[[SonosEvent], None]) -> None:
|
||||
"""Set the callback."""
|
||||
self._callback = callback
|
||||
future = self._get_callback_future()
|
||||
if not future.done():
|
||||
future.set_result(callback)
|
||||
|
||||
def _get_callback_future(self) -> asyncio.Future[Callable[[SonosEvent], None]]:
|
||||
"""Get the callback future."""
|
||||
if not self.callback_future:
|
||||
self.callback_future = asyncio.get_running_loop().create_future()
|
||||
return self.callback_future
|
||||
|
||||
async def wait_for_callback_to_be_set(self) -> Callable[[SonosEvent], None]:
|
||||
"""Wait for the callback to be set."""
|
||||
return await self._get_callback_future()
|
||||
|
||||
async def unsubscribe(self) -> None:
|
||||
"""Unsubscribe mock."""
|
||||
@@ -94,8 +123,9 @@ def async_setup_sonos(hass, config_entry, fire_zgs_event):
|
||||
async def _wrapper():
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await fire_zgs_event()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
return _wrapper
|
||||
|
||||
@@ -455,14 +485,14 @@ def zgs_discovery_fixture():
|
||||
|
||||
|
||||
@pytest.fixture(name="fire_zgs_event")
|
||||
def zgs_event_fixture(hass, soco, zgs_discovery):
|
||||
def zgs_event_fixture(hass: HomeAssistant, soco: SoCo, zgs_discovery: str):
|
||||
"""Create alarm_event fixture."""
|
||||
variables = {"ZoneGroupState": zgs_discovery}
|
||||
|
||||
async def _wrapper():
|
||||
event = SonosMockEvent(soco, soco.zoneGroupTopology, variables)
|
||||
subscription = soco.zoneGroupTopology.subscribe.return_value
|
||||
sub_callback = subscription.callback
|
||||
subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value
|
||||
sub_callback = await subscription.wait_for_callback_to_be_set()
|
||||
sub_callback(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Tests for the Sonos Media Browser."""
|
||||
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.components.media_player.browse_media import BrowseMedia
|
||||
from homeassistant.components.media_player.const import MediaClass, MediaType
|
||||
from homeassistant.components.sonos.media_browser import (
|
||||
build_item_response,
|
||||
get_thumbnail_url_full,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import SoCoMockFactory
|
||||
|
||||
|
||||
class MockMusicServiceItem:
|
||||
"""Mocks a Soco MusicServiceItem."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
item_id: str,
|
||||
parent_id: str,
|
||||
item_class: str,
|
||||
) -> None:
|
||||
"""Initialize the mock item."""
|
||||
self.title = title
|
||||
self.item_id = item_id
|
||||
self.item_class = item_class
|
||||
self.parent_id = parent_id
|
||||
|
||||
def get_uri(self) -> str:
|
||||
"""Return URI."""
|
||||
return self.item_id.replace("S://", "x-file-cifs://")
|
||||
|
||||
|
||||
def mock_browse_by_idstring(
|
||||
search_type: str, idstring: str, start=0, max_items=100, full_album_art_uri=False
|
||||
) -> list[MockMusicServiceItem]:
|
||||
"""Mock the call to browse_by_id_string."""
|
||||
if search_type == "albums" and (
|
||||
idstring == "A:ALBUM/Abbey%20Road" or idstring == "A:ALBUM/Abbey Road"
|
||||
):
|
||||
return [
|
||||
MockMusicServiceItem(
|
||||
"Come Together",
|
||||
"S://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3",
|
||||
"A:ALBUM/Abbey%20Road",
|
||||
"object.item.audioItem.musicTrack",
|
||||
),
|
||||
MockMusicServiceItem(
|
||||
"Something",
|
||||
"S://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3",
|
||||
"A:ALBUM/Abbey%20Road",
|
||||
"object.item.audioItem.musicTrack",
|
||||
),
|
||||
]
|
||||
return None
|
||||
|
||||
|
||||
async def test_build_item_response(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
async_autosetup_sonos,
|
||||
soco,
|
||||
discover,
|
||||
) -> None:
|
||||
"""Test building a browse item response."""
|
||||
soco_mock = soco_factory.mock_list.get("192.168.42.2")
|
||||
soco_mock.music_library.browse_by_idstring = mock_browse_by_idstring
|
||||
browse_item: BrowseMedia = build_item_response(
|
||||
soco_mock.music_library,
|
||||
{"search_type": MediaType.ALBUM, "idstring": "A:ALBUM/Abbey%20Road"},
|
||||
partial(
|
||||
get_thumbnail_url_full,
|
||||
soco_mock.music_library,
|
||||
True,
|
||||
None,
|
||||
),
|
||||
)
|
||||
assert browse_item.title == "Abbey Road"
|
||||
assert browse_item.media_class == MediaClass.ALBUM
|
||||
assert browse_item.media_content_id == "A:ALBUM/Abbey%20Road"
|
||||
assert len(browse_item.children) == 2
|
||||
assert browse_item.children[0].media_class == MediaClass.TRACK
|
||||
assert browse_item.children[0].title == "Come Together"
|
||||
assert (
|
||||
browse_item.children[0].media_content_id
|
||||
== "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3"
|
||||
)
|
||||
assert browse_item.children[1].media_class == MediaClass.TRACK
|
||||
assert browse_item.children[1].title == "Something"
|
||||
assert (
|
||||
browse_item.children[1].media_content_id
|
||||
== "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3"
|
||||
)
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from soco import SoCo
|
||||
|
||||
from homeassistant.components.sonos.const import (
|
||||
DOMAIN,
|
||||
SCAN_INTERVAL,
|
||||
@@ -11,27 +13,27 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import SonosMockEvent
|
||||
from .conftest import SonosMockEvent, SonosMockSubscribe
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_subscription_repair_issues(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, soco, zgs_discovery
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, soco: SoCo, zgs_discovery
|
||||
) -> None:
|
||||
"""Test repair issues handling for failed subscriptions."""
|
||||
issue_registry = async_get_issue_registry(hass)
|
||||
|
||||
subscription = soco.zoneGroupTopology.subscribe.return_value
|
||||
subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value
|
||||
subscription.event_listener = Mock(address=("192.168.4.2", 1400))
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Ensure an issue is registered on subscription failure
|
||||
sub_callback = await subscription.wait_for_callback_to_be_set()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID)
|
||||
|
||||
# Ensure the issue still exists after reload
|
||||
@@ -42,7 +44,6 @@ async def test_subscription_repair_issues(
|
||||
# Ensure the issue has been removed after a successful subscription callback
|
||||
variables = {"ZoneGroupState": zgs_discovery}
|
||||
event = SonosMockEvent(soco, soco.zoneGroupTopology, variables)
|
||||
sub_callback = subscription.callback
|
||||
sub_callback(event)
|
||||
await hass.async_block_till_done()
|
||||
assert not issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID)
|
||||
|
||||
@@ -26,6 +26,7 @@ async def test_entity_registry_unsupported(
|
||||
soco.get_battery_info.side_effect = NotSupportedException
|
||||
|
||||
await async_setup_sonos()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "media_player.zone_a" in entity_registry.entities
|
||||
assert "sensor.zone_a_battery" not in entity_registry.entities
|
||||
@@ -36,6 +37,8 @@ async def test_entity_registry_supported(
|
||||
hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test sonos device with battery registered in the device registry."""
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "media_player.zone_a" in entity_registry.entities
|
||||
assert "sensor.zone_a_battery" in entity_registry.entities
|
||||
assert "binary_sensor.zone_a_charging" in entity_registry.entities
|
||||
@@ -69,6 +72,7 @@ async def test_battery_on_s1(
|
||||
soco.get_battery_info.return_value = {}
|
||||
|
||||
await async_setup_sonos()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
subscription = soco.deviceProperties.subscribe.return_value
|
||||
sub_callback = subscription.callback
|
||||
@@ -78,7 +82,7 @@ async def test_battery_on_s1(
|
||||
|
||||
# Update the speaker with a callback event
|
||||
sub_callback(device_properties_event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
battery = entity_registry.entities["sensor.zone_a_battery"]
|
||||
battery_state = hass.states.get(battery.entity_id)
|
||||
@@ -101,6 +105,7 @@ async def test_device_payload_without_battery(
|
||||
soco.get_battery_info.return_value = None
|
||||
|
||||
await async_setup_sonos()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
subscription = soco.deviceProperties.subscribe.return_value
|
||||
sub_callback = subscription.callback
|
||||
@@ -109,7 +114,7 @@ async def test_device_payload_without_battery(
|
||||
device_properties_event.variables["more_info"] = bad_payload
|
||||
|
||||
sub_callback(device_properties_event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert bad_payload in caplog.text
|
||||
|
||||
@@ -125,6 +130,7 @@ async def test_device_payload_without_battery_and_ignored_keys(
|
||||
soco.get_battery_info.return_value = None
|
||||
|
||||
await async_setup_sonos()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
subscription = soco.deviceProperties.subscribe.return_value
|
||||
sub_callback = subscription.callback
|
||||
@@ -133,7 +139,7 @@ async def test_device_payload_without_battery_and_ignored_keys(
|
||||
device_properties_event.variables["more_info"] = ignored_payload
|
||||
|
||||
sub_callback(device_properties_event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert ignored_payload not in caplog.text
|
||||
|
||||
@@ -150,7 +156,7 @@ async def test_audio_input_sensor(
|
||||
subscription = soco.avTransport.subscribe.return_value
|
||||
sub_callback = subscription.callback
|
||||
sub_callback(tv_event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"]
|
||||
audio_input_state = hass.states.get(audio_input_sensor.entity_id)
|
||||
@@ -161,7 +167,7 @@ async def test_audio_input_sensor(
|
||||
type(soco).soundbar_audio_input_format = no_input_mock
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
no_input_mock.assert_called_once()
|
||||
audio_input_state = hass.states.get(audio_input_sensor.entity_id)
|
||||
@@ -169,13 +175,13 @@ async def test_audio_input_sensor(
|
||||
|
||||
# Ensure state is not polled when source is not TV and state is already "No input"
|
||||
sub_callback(no_media_event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
unpolled_mock = PropertyMock(return_value="Will not be polled")
|
||||
type(soco).soundbar_audio_input_format = unpolled_mock
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
unpolled_mock.assert_not_called()
|
||||
audio_input_state = hass.states.get(audio_input_sensor.entity_id)
|
||||
@@ -199,7 +205,7 @@ async def test_microphone_binary_sensor(
|
||||
# Update the speaker with a callback event
|
||||
subscription = soco.deviceProperties.subscribe.return_value
|
||||
subscription.callback(device_properties_event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id)
|
||||
assert mic_binary_sensor_state.state == STATE_ON
|
||||
@@ -225,17 +231,18 @@ async def test_favorites_sensor(
|
||||
empty_event = SonosMockEvent(soco, service, {})
|
||||
subscription = service.subscribe.return_value
|
||||
subscription.callback(event=empty_event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Reload the integration to enable the sensor
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Trigger subscription callback for speaker discovery
|
||||
await fire_zgs_event()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
favorites_updated_event = SonosMockEvent(
|
||||
soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"}
|
||||
@@ -245,4 +252,4 @@ async def test_favorites_sensor(
|
||||
return_value=True,
|
||||
):
|
||||
subscription.callback(event=favorites_updated_event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
@@ -12,9 +12,20 @@ from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def test_fallback_to_polling(
|
||||
hass: HomeAssistant, async_autosetup_sonos, soco, caplog: pytest.LogCaptureFixture
|
||||
hass: HomeAssistant,
|
||||
config_entry,
|
||||
soco,
|
||||
fire_zgs_event,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that polling fallback works."""
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
# Do not wait on background tasks here because the
|
||||
# subscription callback will fire an unsub the polling check
|
||||
await hass.async_block_till_done()
|
||||
await fire_zgs_event()
|
||||
|
||||
speaker = list(hass.data[DATA_SONOS].discovered.values())[0]
|
||||
assert speaker.soco is soco
|
||||
assert speaker._subscriptions
|
||||
@@ -30,7 +41,7 @@ async def test_fallback_to_polling(
|
||||
),
|
||||
):
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert not speaker._subscriptions
|
||||
assert speaker.subscriptions_failed
|
||||
@@ -46,6 +57,7 @@ async def test_subscription_creation_fails(
|
||||
side_effect=ConnectionError("Took too long"),
|
||||
):
|
||||
await async_setup_sonos()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
speaker = list(hass.data[DATA_SONOS].discovered.values())[0]
|
||||
assert not speaker._subscriptions
|
||||
|
||||
@@ -665,7 +665,7 @@ async def test_zone_attributes(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID)
|
||||
assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"]
|
||||
|
||||
@@ -74,7 +74,7 @@ async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> Non
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(minutes=61),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get("sensor.speedtest_ping")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
@@ -97,7 +97,7 @@ async def test_sensor_process_fails(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3")
|
||||
assert process_sensor is not None
|
||||
|
||||
@@ -232,7 +232,7 @@ async def test_sensor_updating(
|
||||
mock_psutil.virtual_memory.side_effect = Exception("Failed to update")
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
memory_sensor = hass.states.get("sensor.system_monitor_memory_free")
|
||||
assert memory_sensor is not None
|
||||
@@ -248,7 +248,7 @@ async def test_sensor_updating(
|
||||
)
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
memory_sensor = hass.states.get("sensor.system_monitor_memory_free")
|
||||
assert memory_sensor is not None
|
||||
@@ -293,7 +293,7 @@ async def test_sensor_process_fails(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
process_sensor = hass.states.get("sensor.system_monitor_process_python3")
|
||||
assert process_sensor is not None
|
||||
@@ -330,7 +330,7 @@ async def test_sensor_network_sensors(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1")
|
||||
packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1")
|
||||
@@ -362,7 +362,7 @@ async def test_sensor_network_sensors(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1")
|
||||
packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1")
|
||||
@@ -470,7 +470,7 @@ async def test_exception_handling_disk_sensor(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "OS error for /" in caplog.text
|
||||
|
||||
@@ -483,7 +483,7 @@ async def test_exception_handling_disk_sensor(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "OS error for /" in caplog.text
|
||||
|
||||
@@ -498,7 +498,7 @@ async def test_exception_handling_disk_sensor(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
disk_sensor = hass.states.get("sensor.system_monitor_disk_free")
|
||||
assert disk_sensor is not None
|
||||
@@ -528,7 +528,7 @@ async def test_cpu_percentage_is_zero_returns_unknown(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
cpu_sensor = hass.states.get("sensor.system_monitor_processor_use")
|
||||
assert cpu_sensor is not None
|
||||
@@ -538,7 +538,7 @@ async def test_cpu_percentage_is_zero_returns_unknown(
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
cpu_sensor = hass.states.get("sensor.system_monitor_processor_use")
|
||||
assert cpu_sensor is not None
|
||||
@@ -573,7 +573,7 @@ async def test_remove_obsolete_entities(
|
||||
)
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Fake an entity which should be removed as not supported and disabled
|
||||
entity_registry.async_get_or_create(
|
||||
|
||||
@@ -79,7 +79,7 @@ async def test_state(hass: HomeAssistant, mock_socket, now) -> None:
|
||||
mock_socket.recv.return_value = b"on"
|
||||
|
||||
async_fire_time_changed(hass, now + timedelta(seconds=45))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ async def test_temperature_readback(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(hass, utcnow + timedelta(seconds=70))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
temperature = hass.states.get("sensor.mydevicename")
|
||||
assert temperature
|
||||
|
||||
@@ -165,8 +165,8 @@
|
||||
'platform': 'tessie',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'charge_state_battery_heater_on',
|
||||
'unique_id': 'VINVINVIN-charge_state_battery_heater_on',
|
||||
'translation_key': 'climate_state_battery_heater',
|
||||
'unique_id': 'VINVINVIN-climate_state_battery_heater',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -548,30 +548,30 @@ async def test_other_update_failures(hass: HomeAssistant) -> None:
|
||||
|
||||
# then an error: ServiceUnavailable --> UpdateFailed
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
assert mock_request.call_count == 2
|
||||
|
||||
# works again
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 2)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
|
||||
assert mock_request.call_count == 3
|
||||
|
||||
# then an error: TotalConnectError --> UpdateFailed
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 3)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
assert mock_request.call_count == 4
|
||||
|
||||
# works again
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 4)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
|
||||
assert mock_request.call_count == 5
|
||||
|
||||
# unknown TotalConnect status via ValueError
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 5)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
assert mock_request.call_count == 6
|
||||
|
||||
@@ -278,7 +278,7 @@ async def test_setup_nvr_errors_during_indexing(
|
||||
mock_remote.return_value.index.side_effect = None
|
||||
|
||||
async_fire_time_changed(hass, now + timedelta(seconds=31))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
camera_states = hass.states.async_all("camera")
|
||||
|
||||
@@ -313,7 +313,7 @@ async def test_setup_nvr_errors_during_initialization(
|
||||
mock_remote.side_effect = None
|
||||
|
||||
async_fire_time_changed(hass, now + timedelta(seconds=31))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
camera_states = hass.states.async_all("camera")
|
||||
|
||||
@@ -362,7 +362,7 @@ async def test_motion_recording_mode_properties(
|
||||
] = True
|
||||
|
||||
async_fire_time_changed(hass, now + timedelta(seconds=31))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("camera.front")
|
||||
|
||||
@@ -375,7 +375,7 @@ async def test_motion_recording_mode_properties(
|
||||
mock_remote.return_value.get_camera.return_value["recordingIndicator"] = "DISABLED"
|
||||
|
||||
async_fire_time_changed(hass, now + timedelta(seconds=61))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("camera.front")
|
||||
|
||||
@@ -387,7 +387,7 @@ async def test_motion_recording_mode_properties(
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, now + timedelta(seconds=91))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("camera.front")
|
||||
|
||||
@@ -399,7 +399,7 @@ async def test_motion_recording_mode_properties(
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, now + timedelta(seconds=121))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("camera.front")
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# serializer version: 1
|
||||
# name: test_entry_diagnostics
|
||||
dict({
|
||||
'appliances': dict({
|
||||
'Washer_dryers': dict({
|
||||
'dryer': dict({
|
||||
'NAME': 'dryer',
|
||||
'SAID': '**REDACTED**',
|
||||
}),
|
||||
'washer': dict({
|
||||
'NAME': 'washer',
|
||||
'SAID': '**REDACTED**',
|
||||
}),
|
||||
}),
|
||||
'aircons': dict({
|
||||
'TestZone': dict({
|
||||
'NAME': 'TestZone',
|
||||
'SAID': '**REDACTED**',
|
||||
}),
|
||||
}),
|
||||
'ovens': dict({
|
||||
}),
|
||||
}),
|
||||
'config_entry': dict({
|
||||
'data': dict({
|
||||
'brand': 'Whirlpool',
|
||||
'password': '**REDACTED**',
|
||||
'region': 'EU',
|
||||
'username': '**REDACTED**',
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'domain': 'whirlpool',
|
||||
'minor_version': 1,
|
||||
'options': dict({
|
||||
}),
|
||||
'pref_disable_new_entities': False,
|
||||
'pref_disable_polling': False,
|
||||
'source': 'user',
|
||||
'title': 'Mock Title',
|
||||
'unique_id': None,
|
||||
'version': 1,
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user