Compare commits

...

32 Commits

Author SHA1 Message Date
Franck Nijhof 5af5f3694e Bump version to 2024.4.0b5 2024-04-02 12:28:20 +02:00
Bram Kragten b539b25682 Update frontend to 20240402.0 (#114627) 2024-04-02 12:28:07 +02:00
Fexiven ca31479d29 Fix Starlink integration startup issue (#114615) 2024-04-02 12:28:04 +02:00
Franck Nijhof 92dfec3c98 Add floor selector (#114614) 2024-04-02 12:28:00 +02:00
max2697 230c29edbe Bump opower to 0.4.2 (#114608) 2024-04-02 12:27:57 +02:00
Jack Boswell 559fe65471 Catch potential ValueError when getting or setting Starlink sleep values (#114607) 2024-04-02 12:27:54 +02:00
mkmer 384d10a51d Add diagnostic platform to Whirlpool (#114578)
* Add diagnostic platform and tests

* lowercase variable

* Correc doc string
2024-04-02 12:27:50 +02:00
Brett Adams e5a620545c Fix battery heater in Tessie (#114568) 2024-04-02 12:27:47 +02:00
Maciej Bieniek 7b84e86f89 Improve Shelly RPC device update progress (#114566)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-04-02 12:27:44 +02:00
Joost Lekkerkerker 18b6de567d Bump roombapy to 1.8.1 (#114478)
* Bump roombapy to 1.7.0

* Bump

* Bump

* Fix
2024-04-02 12:27:40 +02:00
Pete Sage a6076a0d33 Display sonos album title with URL encoding (#113693)
* unescape the title

When extracting the title from the item_id, it needs to be unescaped.

* sort imports
2024-04-02 12:27:36 +02:00
Paulus Schoutsen 7164993562 Bump version to 2024.4.0b4 2024-04-02 01:51:42 +00:00
mkmer bc21836e7e Bump whirlpool-sixth-sense to 0.18.7 (#114606)
Bump sixth-sense to 0.18.7
2024-04-02 01:51:35 +00:00
J. Nick Koston 52612b10fd Avoid storing raw extracted traceback in system_log (#114603)
This is never actually used and takes up quite a bit of ram
2024-04-02 01:51:35 +00:00
J. Nick Koston 623d85ecaa Fix memory leak when importing a platform fails (#114602)
* Fix memory leak when importing a platform fails

re-raising ImportError would trigger a memory leak

* fixes, coverage

* Apply suggestions from code review
2024-04-02 01:51:33 +00:00
J. Nick Koston 43631d5944 Add missing platforms_exist guard to check_config (#114600)
* Add missing platforms_exist guard to check_config

related issue #112811

When the exception hits, the config will end up being saved in the traceback
so the memory is never released.

This matches the check_config code to homeassistant.config to avoid having
the exception thrown.

* patch

* merge branch
2024-04-02 01:51:33 +00:00
J. Nick Koston 112aab47fb Bump zeroconf to 0.132.0 (#114596)
changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.131.0...0.132.0
2024-04-02 01:51:32 +00:00
Martin Hjelmare ea13f102e0 Fix reolink media source data access (#114593)
* Add test

* Fix reolink media source data access
2024-04-02 01:51:31 +00:00
jjlawren bb33725e7f Bump plexapi to 4.15.11 (#114581) 2024-04-02 01:51:31 +00:00
Michael bd6890ab83 Filter out ignored entries in ssdp step of AVM Fritz!SmartHome (#114574)
filter out ignored entries in ssdp step
2024-04-02 01:51:30 +00:00
Michael 25c611ffc4 Reduce usage of executer threads in AVM Fritz!Tools (#114570)
* call entity state update calls in one executer task

* remove not needed wrapping

* mark as "non-public" method

* add guard against changes on _entity_update_functions
2024-04-02 01:51:29 +00:00
Maikel Punie fc24b61859 Bump velbusaio to 2024.4.0 (#114569)
Bump valbusaio to 2024.4.0
2024-04-02 01:51:28 +00:00
Joost Lekkerkerker 71588b5c22 Fix wrong icons (#114567)
* Fix wrong icons

* Fix wrong icons
2024-04-02 01:51:27 +00:00
Robert Svensson 14dfb6a255 Bump axis to v60 (#114544)
* Improve Axis MQTT support

* Bump axis to v60
2024-04-02 01:51:27 +00:00
G Johansson ef97255d9c Fix server update from breaking setup in Speedtest.NET (#114524) 2024-04-02 01:51:26 +00:00
J. Nick Koston e8afdd67d0 Fix workday doing blocking I/O in the event loop (#114492) 2024-04-02 01:51:25 +00:00
J. Nick Koston 008e4413b5 Fix late load of anyio doing blocking I/O in the event loop (#114491)
* Fix late load of anyio doing blocking I/O in the event loop

httpx loads anyio which loads the asyncio backend in the event loop
as soon as httpx makes the first request

* tweak
2024-04-02 01:51:24 +00:00
dotvav c373d40e34 Fix Overkiz Hitachi OVP air-to-air heat pump (#114487)
Unpack command parameters instead of passing a list
2024-04-02 01:51:24 +00:00
J. Nick Koston bdf51553ef Improve sonos test synchronization (#114468) 2024-04-02 01:51:23 +00:00
Michael Hansen f2edc15687 Add initial support for floors to intents (#114456)
* Add initial support for floors to intents

* Fix climate intent

* More tests

* No return value

* Add requested changes

* Reuse event handler
2024-04-02 01:51:22 +00:00
J. Nick Koston 286a09d737 Mark executor jobs as background unless created from a tracked task (#114450)
* Mark executor jobs as background unless created from a tracked task

If the current task is not tracked the executor job should not
be a background task to avoid delaying startup and shutdown.

Currently any executor job created in a untracked task or
background task would end up being tracked and delaying
startup/shutdown

* import exec has the same issue

* Avoid tracking import executor jobs

There is no reason to track these jobs as they are always awaited
and we do not want to support fire and forget import executor jobs

* fix xiaomi_miio

* lots of fire time changed without background await

* revert changes moved to other PR

* more

* more

* more

* m

* m

* p

* fix fire and forget tests

* scrape

* sonos

* system

* more

* capture callback before block

* coverage

* more

* more races

* more races

* more

* missed some

* more fixes

* missed some more

* fix

* remove unneeded

* one more race

* two
2024-04-02 01:51:21 +00:00
Shay Levy e8ee2fd25c Cleanup Shelly RGBW light entities (#114410) 2024-04-02 01:51:21 +00:00
109 changed files with 1385 additions and 374 deletions
+5
View File
@@ -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
+3 -2
View File
@@ -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)
+1 -1
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"services": {
"restart": "mdi:restart",
"start": "mdi:start",
"start": "mdi:play",
"stop": "mdi:stop"
}
}
+14 -9
View File
@@ -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"]
}
@@ -357,5 +357,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
]
await self.executor.async_execute_command(
OverkizCommand.GLOBAL_CONTROL, command_data
OverkizCommand.GLOBAL_CONTROL, *command_data
)
+1 -1
View File
@@ -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.",
+2
View File
@@ -234,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
)
CONF_GEN = "gen"
SHELLY_PLUS_RGBW_CHANNELS = 4
+17
View File
@@ -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)
+10 -6
View File
@@ -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(
+12
View File
@@ -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:
+13 -4
View File
@@ -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,
),
+1 -1
View File
@@ -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 -1
View File
@@ -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"]
}
+11 -5
View File
@@ -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"]
}
+1 -1
View File
@@ -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)
+5 -2
View File
@@ -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
+10 -9
View File
@@ -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"
+89 -21
View File
@@ -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,
)
+42
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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"
+9 -9
View File
@@ -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
+9 -9
View File
@@ -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
+1
View File
@@ -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}",
},
},
}
+2 -2
View File
@@ -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"}}}'
+2 -2
View File
@@ -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()
+4 -4
View File
@@ -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
}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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
+2 -2
View File
@@ -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"
+5 -5
View File
@@ -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)
+20 -20
View File
@@ -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,
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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")
+6 -6
View File
@@ -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
+12
View File
@@ -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)
+6
View File
@@ -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},
+85 -3
View File
@@ -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
+11 -8
View File
@@ -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
+34 -4
View File
@@ -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"
)
+7 -6
View File
@@ -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)
+18 -11
View File
@@ -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)
+14 -2
View File
@@ -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
+11 -11
View File
@@ -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(
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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