forked from home-assistant/core
Compare commits
63 Commits
2024.4.0b2
...
2024.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b61397656c | ||
|
|
590546a9a5 | ||
|
|
9ba4d26abd | ||
|
|
aa33da546d | ||
|
|
3845523a27 | ||
|
|
6a7fad0228 | ||
|
|
33f07ce035 | ||
|
|
4302c5c273 | ||
|
|
b2df1b1c03 | ||
|
|
0aa134459b | ||
|
|
0ca3700c16 | ||
|
|
35ff633d99 | ||
|
|
7a2f6ce430 | ||
|
|
7cb603a226 | ||
|
|
43562289e4 | ||
|
|
79fa7caa41 | ||
|
|
8bdb27c88b | ||
|
|
f676448f27 | ||
|
|
639c4a843b | ||
|
|
02dee34338 | ||
|
|
4e0290ce0e | ||
|
|
fa2f49693c | ||
|
|
2ce784105d | ||
|
|
85fb4a27a3 | ||
|
|
8cbedbe26b | ||
|
|
5bd52da13a | ||
|
|
d53848aae4 | ||
|
|
4e0d6f287e | ||
|
|
5af5f3694e | ||
|
|
b539b25682 | ||
|
|
ca31479d29 | ||
|
|
92dfec3c98 | ||
|
|
230c29edbe | ||
|
|
559fe65471 | ||
|
|
384d10a51d | ||
|
|
e5a620545c | ||
|
|
7b84e86f89 | ||
|
|
18b6de567d | ||
|
|
a6076a0d33 | ||
|
|
7164993562 | ||
|
|
bc21836e7e | ||
|
|
52612b10fd | ||
|
|
623d85ecaa | ||
|
|
43631d5944 | ||
|
|
112aab47fb | ||
|
|
ea13f102e0 | ||
|
|
bb33725e7f | ||
|
|
bd6890ab83 | ||
|
|
25c611ffc4 | ||
|
|
fc24b61859 | ||
|
|
71588b5c22 | ||
|
|
14dfb6a255 | ||
|
|
ef97255d9c | ||
|
|
e8afdd67d0 | ||
|
|
008e4413b5 | ||
|
|
c373d40e34 | ||
|
|
bdf51553ef | ||
|
|
f2edc15687 | ||
|
|
286a09d737 | ||
|
|
e8ee2fd25c | ||
|
|
11b8b01cde | ||
|
|
4f761c25d8 | ||
|
|
953ceb0d8d |
@@ -93,6 +93,11 @@ from .util.async_ import create_eager_task
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import async_get_user_site, is_virtual_env
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
||||
from anyio._backends import _asyncio # noqa: F401
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .runner import RuntimeConfig
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class AxisHub:
|
||||
if status.status.state == ClientState.ACTIVE:
|
||||
self.config.entry.async_on_unload(
|
||||
await mqtt.async_subscribe(
|
||||
hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message
|
||||
hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message
|
||||
)
|
||||
)
|
||||
|
||||
@@ -124,7 +124,8 @@ class AxisHub:
|
||||
def mqtt_message(self, message: ReceiveMessage) -> None:
|
||||
"""Receive Axis MQTT message."""
|
||||
self.disconnect_from_stream()
|
||||
|
||||
if message.topic.endswith("event/connection"):
|
||||
return
|
||||
event = mqtt_json_to_event(message.payload)
|
||||
self.api.event.handler(event)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==59"],
|
||||
"requirements": ["axis==60"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -58,6 +58,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
@@ -75,6 +76,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_name,
|
||||
area=None,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
start,
|
||||
template,
|
||||
@@ -163,7 +164,12 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
self.hass.bus.async_listen(
|
||||
ar.EVENT_AREA_REGISTRY_UPDATED,
|
||||
self._async_handle_area_registry_changed,
|
||||
self._async_handle_area_floor_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
fr.EVENT_FLOOR_REGISTRY_UPDATED,
|
||||
self._async_handle_area_floor_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
@@ -696,10 +702,13 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
return lang_intents
|
||||
|
||||
@core.callback
|
||||
def _async_handle_area_registry_changed(
|
||||
self, event: core.Event[ar.EventAreaRegistryUpdatedData]
|
||||
def _async_handle_area_floor_registry_changed(
|
||||
self,
|
||||
event: core.Event[
|
||||
ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData
|
||||
],
|
||||
) -> None:
|
||||
"""Clear area area cache when the area registry has changed."""
|
||||
"""Clear area/floor list cache when the area registry has changed."""
|
||||
self._slot_lists = None
|
||||
|
||||
@core.callback
|
||||
@@ -773,6 +782,8 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
# Default name
|
||||
entity_names.append((state.name, state.name, context))
|
||||
|
||||
_LOGGER.debug("Exposed entities: %s", entity_names)
|
||||
|
||||
# Expose all areas.
|
||||
#
|
||||
# We pass in area id here with the expectation that no two areas will
|
||||
@@ -788,11 +799,25 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
area_names.append((alias, area.id))
|
||||
|
||||
_LOGGER.debug("Exposed entities: %s", entity_names)
|
||||
# Expose all floors.
|
||||
#
|
||||
# We pass in floor id here with the expectation that no two floors will
|
||||
# share the same name or alias.
|
||||
floors = fr.async_get(self.hass)
|
||||
floor_names = []
|
||||
for floor in floors.async_list_floors():
|
||||
floor_names.append((floor.name, floor.floor_id))
|
||||
if floor.aliases:
|
||||
for alias in floor.aliases:
|
||||
if not alias.strip():
|
||||
continue
|
||||
|
||||
floor_names.append((alias, floor.floor_id))
|
||||
|
||||
self._slot_lists = {
|
||||
"area": TextSlotList.from_tuples(area_names, allow_template=False),
|
||||
"name": TextSlotList.from_tuples(entity_names, allow_template=False),
|
||||
"floor": TextSlotList.from_tuples(floor_names, allow_template=False),
|
||||
}
|
||||
|
||||
return self._slot_lists
|
||||
@@ -953,6 +978,10 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
|
||||
# area only
|
||||
return ErrorKey.NO_AREA, {"area": unmatched_area}
|
||||
|
||||
if unmatched_floor := unmatched_text.get("floor"):
|
||||
# floor only
|
||||
return ErrorKey.NO_FLOOR, {"floor": unmatched_floor}
|
||||
|
||||
# Area may still have matched
|
||||
matched_area: str | None = None
|
||||
if matched_area_entity := result.entities.get("area"):
|
||||
@@ -1000,6 +1029,13 @@ def _get_no_states_matched_response(
|
||||
"area": no_states_error.area,
|
||||
}
|
||||
|
||||
if no_states_error.floor:
|
||||
# domain in floor
|
||||
return ErrorKey.NO_DOMAIN_IN_FLOOR, {
|
||||
"domain": domain,
|
||||
"floor": no_states_error.floor,
|
||||
}
|
||||
|
||||
# domain only
|
||||
return ErrorKey.NO_DOMAIN, {"domain": domain}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.27"]
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"]
|
||||
}
|
||||
|
||||
@@ -55,10 +55,11 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def _validate_input(self, user_input: dict[str, Any]) -> None:
|
||||
"""Validate the user input if the directory exists."""
|
||||
if not os.path.isabs(user_input[CONF_DOWNLOAD_DIR]):
|
||||
download_path = self.hass.config.path(user_input[CONF_DOWNLOAD_DIR])
|
||||
download_path = user_input[CONF_DOWNLOAD_DIR]
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = self.hass.config.path(download_path)
|
||||
|
||||
if not os.path.isdir(download_path):
|
||||
if not await self.hass.async_add_executor_job(os.path.isdir, download_path):
|
||||
_LOGGER.error(
|
||||
"Download path %s does not exist. File Downloader not active",
|
||||
download_path,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"services": {
|
||||
"restart": "mdi:restart",
|
||||
"start": "mdi:start",
|
||||
"start": "mdi:play",
|
||||
"stop": "mdi:stop"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["flexit_bacnet==2.1.0"]
|
||||
"requirements": ["flexit_bacnet==2.2.1"]
|
||||
}
|
||||
|
||||
@@ -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.0"]
|
||||
"requirements": ["home-assistant-frontend==20240403.1"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.45", "babel==2.13.1"]
|
||||
"requirements": ["holidays==0.46", "babel==2.13.1"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Motionblinds BLE integration."""
|
||||
"""Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -34,9 +34,9 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Motionblinds BLE integration."""
|
||||
"""Set up Motionblinds Bluetooth integration."""
|
||||
|
||||
_LOGGER.debug("Setting up Motionblinds BLE integration")
|
||||
_LOGGER.debug("Setting up Motionblinds Bluetooth integration")
|
||||
|
||||
# The correct time is needed for encryption
|
||||
_LOGGER.debug("Setting timezone for encryption: %s", hass.config.time_zone)
|
||||
@@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Motionblinds BLE device from a config entry."""
|
||||
"""Set up Motionblinds Bluetooth device from a config entry."""
|
||||
|
||||
_LOGGER.debug("(%s) Setting up device", entry.data[CONF_MAC_CODE])
|
||||
|
||||
@@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Motionblinds BLE device from a config entry."""
|
||||
"""Unload Motionblinds Bluetooth device from a config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Button entities for the Motionblinds BLE integration."""
|
||||
"""Button entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Config flow for Motionblinds BLE integration."""
|
||||
"""Config flow for Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_MAC_CODE): str})
|
||||
|
||||
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Motionblinds BLE."""
|
||||
"""Handle a config flow for Motionblinds Bluetooth."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a ConfigFlow."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Constants for the Motionblinds BLE integration."""
|
||||
"""Constants for the Motionblinds Bluetooth integration."""
|
||||
|
||||
ATTR_CONNECT = "connect"
|
||||
ATTR_DISCONNECT = "disconnect"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Cover entities for the Motionblinds BLE integration."""
|
||||
"""Cover entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Base entities for the Motionblinds BLE integration."""
|
||||
"""Base entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MotionblindsBLEEntity(Entity):
|
||||
"""Base class for Motionblinds BLE entities."""
|
||||
"""Base class for Motionblinds Bluetooth entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "motionblinds_ble",
|
||||
"name": "Motionblinds BLE",
|
||||
"name": "Motionblinds Bluetooth",
|
||||
"bluetooth": [
|
||||
{
|
||||
"local_name": "MOTION_*",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Select entities for the Motionblinds BLE integration."""
|
||||
"""Select entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -298,6 +298,11 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
|
||||
OverkizState.OVP_FAN_SPEED,
|
||||
OverkizCommandParam.AUTO,
|
||||
)
|
||||
# Sanitize fan mode: Overkiz is sometimes providing a state that
|
||||
# cannot be used as a command. Convert it to HA space and back to Overkiz
|
||||
if fan_mode not in FAN_MODES_TO_OVERKIZ.values():
|
||||
fan_mode = FAN_MODES_TO_OVERKIZ[OVERKIZ_TO_FAN_MODES[fan_mode]]
|
||||
|
||||
hvac_mode = self._control_backfill(
|
||||
hvac_mode,
|
||||
OverkizState.OVP_MODE_CHANGE,
|
||||
@@ -357,5 +362,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
|
||||
]
|
||||
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.GLOBAL_CONTROL, command_data
|
||||
OverkizCommand.GLOBAL_CONTROL, *command_data
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["plexapi", "plexwebsocket"],
|
||||
"requirements": [
|
||||
"PlexAPI==4.15.10",
|
||||
"PlexAPI==4.15.11",
|
||||
"plexauth==0.0.6",
|
||||
"plexwebsocket==0.0.14"
|
||||
],
|
||||
|
||||
@@ -46,7 +46,6 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
"""Initialize ReolinkVODMediaSource."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
self.data: dict[str, ReolinkData] = hass.data[DOMAIN]
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
@@ -57,7 +56,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
_, config_entry_id, channel_str, stream_res, filename = identifier
|
||||
channel = int(channel_str)
|
||||
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
vod_type = VodRequestType.RTMP
|
||||
if host.api.is_nvr:
|
||||
@@ -130,7 +130,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
continue
|
||||
channels: list[str] = []
|
||||
host = self.data[config_entry.entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry.entry_id].host
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_reg, config_entry.entry_id
|
||||
)
|
||||
@@ -187,7 +188,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
self, config_entry_id: str, channel: int
|
||||
) -> BrowseMediaSource:
|
||||
"""Allow the user to select the high or low playback resolution, (low loads faster)."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
main_enc = await host.api.get_encoding(channel, "main")
|
||||
if main_enc == "h265":
|
||||
@@ -236,7 +238,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
self, config_entry_id: str, channel: int, stream: str
|
||||
) -> BrowseMediaSource:
|
||||
"""Return all days on which recordings are available for a reolink camera."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
# We want today of the camera, not necessarily today of the server
|
||||
now = host.api.time() or await host.api.async_get_time()
|
||||
@@ -288,7 +291,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
day: int,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return all recording files on a specific day of a Reolink camera."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
start = dt.datetime(year, month, day, hour=0, minute=0, second=0)
|
||||
end = dt.datetime(year, month, day, hour=23, minute=59, second=59)
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ring",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ring_doorbell"],
|
||||
"requirements": ["ring-doorbell[listen]==0.8.8"]
|
||||
"requirements": ["ring-doorbell[listen]==0.8.9"]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/roomba",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["paho_mqtt", "roombapy"],
|
||||
"requirements": ["roombapy==1.6.13"],
|
||||
"requirements": ["roombapy==1.8.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_amzn-alexa._tcp.local.",
|
||||
|
||||
@@ -234,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
|
||||
)
|
||||
|
||||
CONF_GEN = "gen"
|
||||
|
||||
SHELLY_PLUS_RGBW_CHANNELS = 4
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.light import (
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_RGBW_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
@@ -34,12 +35,14 @@ from .const import (
|
||||
RGBW_MODELS,
|
||||
RPC_MIN_TRANSITION_TIME_SEC,
|
||||
SHBLB_1_RGB_EFFECTS,
|
||||
SHELLY_PLUS_RGBW_CHANNELS,
|
||||
STANDARD_RGB_EFFECTS,
|
||||
)
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
|
||||
from .entity import ShellyBlockEntity, ShellyRpcEntity
|
||||
from .utils import (
|
||||
async_remove_shelly_entity,
|
||||
async_remove_shelly_rpc_entities,
|
||||
brightness_to_percentage,
|
||||
get_device_entry_gen,
|
||||
get_rpc_key_ids,
|
||||
@@ -118,14 +121,28 @@ def async_setup_rpc_entry(
|
||||
return
|
||||
|
||||
if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"):
|
||||
# Light mode remove RGB & RGBW entities, add light entities
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, ["rgb:0", "rgbw:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids)
|
||||
return
|
||||
|
||||
light_keys = [f"light:{i}" for i in range(SHELLY_PLUS_RGBW_CHANNELS)]
|
||||
|
||||
if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"):
|
||||
# RGB mode remove light & RGBW entities, add RGB entity
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgbw:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids)
|
||||
return
|
||||
|
||||
if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"):
|
||||
# RGBW mode remove light & RGB entities, add RGBW entity
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgb:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids)
|
||||
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
) -> None:
|
||||
"""Initialize update entity."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
self._ota_in_progress: bool = False
|
||||
self._ota_in_progress: bool | int = False
|
||||
self._attr_release_url = get_release_url(
|
||||
coordinator.device.gen, coordinator.model, description.beta
|
||||
)
|
||||
@@ -237,14 +237,13 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
@callback
|
||||
def _ota_progress_callback(self, event: dict[str, Any]) -> None:
|
||||
"""Handle device OTA progress."""
|
||||
if self._ota_in_progress:
|
||||
if self.in_progress is not False:
|
||||
event_type = event["event"]
|
||||
if event_type == OTA_BEGIN:
|
||||
self._attr_in_progress = 0
|
||||
self._ota_in_progress = 0
|
||||
elif event_type == OTA_PROGRESS:
|
||||
self._attr_in_progress = event["progress_percent"]
|
||||
self._ota_in_progress = event["progress_percent"]
|
||||
elif event_type in (OTA_ERROR, OTA_SUCCESS):
|
||||
self._attr_in_progress = False
|
||||
self._ota_in_progress = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -262,6 +261,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
|
||||
return self.installed_version
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool | int:
|
||||
"""Update installation in progress."""
|
||||
return self._ota_in_progress
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
@@ -292,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
await self.coordinator.async_shutdown_device_and_start_reauth()
|
||||
else:
|
||||
self._ota_in_progress = True
|
||||
LOGGER.debug("OTA update call successful")
|
||||
LOGGER.info("OTA update call for %s successful", self.coordinator.name)
|
||||
|
||||
|
||||
class RpcSleepingUpdateEntity(
|
||||
|
||||
@@ -488,3 +488,15 @@ async def async_shutdown_device(device: BlockDevice | RpcDevice) -> None:
|
||||
await device.shutdown()
|
||||
if isinstance(device, BlockDevice):
|
||||
device.shutdown()
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove_shelly_rpc_entities(
|
||||
hass: HomeAssistant, domain: str, mac: str, keys: list[str]
|
||||
) -> None:
|
||||
"""Remove RPC based Shelly entity."""
|
||||
entity_reg = er_async_get(hass)
|
||||
for key in keys:
|
||||
if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"):
|
||||
LOGGER.debug("Removing entity: %s", entity_id)
|
||||
entity_reg.async_remove(entity_id)
|
||||
|
||||
@@ -7,6 +7,7 @@ from contextlib import suppress
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import cast
|
||||
import urllib.parse
|
||||
|
||||
from soco.data_structures import DidlObject
|
||||
from soco.ms_data_structures import MusicServiceItem
|
||||
@@ -60,12 +61,14 @@ def get_thumbnail_url_full(
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
)
|
||||
return getattr(item, "album_art_uri", None)
|
||||
return urllib.parse.unquote(getattr(item, "album_art_uri", ""))
|
||||
|
||||
return get_browse_image_url(
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
media_image_id,
|
||||
return urllib.parse.unquote(
|
||||
get_browse_image_url(
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
media_image_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -166,6 +169,7 @@ def build_item_response(
|
||||
payload["idstring"] = "A:ALBUMARTIST/" + "/".join(
|
||||
payload["idstring"].split("/")[2:]
|
||||
)
|
||||
payload["idstring"] = urllib.parse.unquote(payload["idstring"])
|
||||
|
||||
try:
|
||||
search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]]
|
||||
@@ -201,7 +205,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"]]
|
||||
|
||||
@@ -493,10 +497,24 @@ def get_media(
|
||||
"""Fetch media/album."""
|
||||
search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type)
|
||||
|
||||
if search_type == "playlists":
|
||||
# Format is S:TITLE or S:ITEM_ID
|
||||
splits = item_id.split(":")
|
||||
title = splits[1] if len(splits) > 1 else None
|
||||
playlist = next(
|
||||
(
|
||||
p
|
||||
for p in media_library.get_playlists()
|
||||
if (item_id == p.item_id or title == p.title)
|
||||
),
|
||||
None,
|
||||
)
|
||||
return playlist
|
||||
|
||||
if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM:
|
||||
item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:])
|
||||
|
||||
search_term = item_id.split("/")[-1]
|
||||
search_term = urllib.parse.unquote(item_id.split("/")[-1])
|
||||
matches = media_library.get_music_library_information(
|
||||
search_type, search_term=search_term, full_album_art_uri=True
|
||||
)
|
||||
|
||||
@@ -626,13 +626,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
soco.play_uri(media_id, force_radio=is_radio)
|
||||
elif media_type == MediaType.PLAYLIST:
|
||||
if media_id.startswith("S:"):
|
||||
item = media_browser.get_media(self.media.library, media_id, media_type)
|
||||
soco.play_uri(item.get_uri())
|
||||
return
|
||||
try:
|
||||
playlist = media_browser.get_media(
|
||||
self.media.library, media_id, media_type
|
||||
)
|
||||
else:
|
||||
playlists = soco.get_sonos_playlists(complete_result=True)
|
||||
playlist = next(p for p in playlists if p.title == media_id)
|
||||
except StopIteration:
|
||||
playlist = next((p for p in playlists if p.title == media_id), None)
|
||||
if not playlist:
|
||||
_LOGGER.error('Could not find a Sonos playlist named "%s"', media_id)
|
||||
else:
|
||||
soco.clear_queue()
|
||||
|
||||
@@ -25,10 +25,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
partial(speedtest.Speedtest, secure=True)
|
||||
)
|
||||
coordinator = SpeedTestDataCoordinator(hass, config_entry, api)
|
||||
await hass.async_add_executor_job(coordinator.update_servers)
|
||||
except speedtest.SpeedtestException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data[DOMAIN] = coordinator
|
||||
|
||||
async def _async_finish_startup(hass: HomeAssistant) -> None:
|
||||
"""Run this only when HA has finished its startup."""
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@@ -36,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
# Don't start a speedtest during startup
|
||||
async_at_started(hass, _async_finish_startup)
|
||||
|
||||
hass.data[DOMAIN] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -58,14 +58,14 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
|
||||
async def _async_update_data(self) -> StarlinkData:
|
||||
async with asyncio.timeout(4):
|
||||
try:
|
||||
status, location, sleep = await asyncio.gather(
|
||||
self.hass.async_add_executor_job(status_data, self.channel_context),
|
||||
self.hass.async_add_executor_job(
|
||||
location_data, self.channel_context
|
||||
),
|
||||
self.hass.async_add_executor_job(
|
||||
get_sleep_config, self.channel_context
|
||||
),
|
||||
status = await self.hass.async_add_executor_job(
|
||||
status_data, self.channel_context
|
||||
)
|
||||
location = await self.hass.async_add_executor_job(
|
||||
location_data, self.channel_context
|
||||
)
|
||||
sleep = await self.hass.async_add_executor_job(
|
||||
get_sleep_config, self.channel_context
|
||||
)
|
||||
return StarlinkData(location, sleep, *status)
|
||||
except GrpcError as exc:
|
||||
|
||||
@@ -10,6 +10,7 @@ import math
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -62,14 +63,22 @@ class StarlinkTimeEntity(StarlinkEntity, TimeEntity):
|
||||
def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time:
|
||||
hour = math.floor(utc_minutes / 60)
|
||||
minute = utc_minutes % 60
|
||||
utc = datetime.now(UTC).replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
try:
|
||||
utc = datetime.now(UTC).replace(
|
||||
hour=hour, minute=minute, second=0, microsecond=0
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
return utc.astimezone(timezone).time()
|
||||
|
||||
|
||||
def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
try:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
utc_time = zoned_time.astimezone(UTC).time()
|
||||
return (utc_time.hour * 60) + utc_time.minute
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if (
|
||||
SynoSurveillanceStation.INFO_API_KEY in available_apis
|
||||
and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis
|
||||
and api.surveillance_station is not None
|
||||
):
|
||||
coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api)
|
||||
await coordinator_switches.async_config_entry_first_refresh()
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reboot": "mdi:reboot",
|
||||
"reboot": "mdi:restart",
|
||||
"shutdown": "mdi:power"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,6 @@ class LogEntry:
|
||||
"level",
|
||||
"message",
|
||||
"exception",
|
||||
"extracted_tb",
|
||||
"root_cause",
|
||||
"source",
|
||||
"count",
|
||||
@@ -200,7 +199,6 @@ class LogEntry:
|
||||
else:
|
||||
self.source = (record.pathname, record.lineno)
|
||||
self.count = 1
|
||||
self.extracted_tb = extracted_tb
|
||||
self.key = (self.name, self.source, self.root_cause)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
|
||||
@@ -34,7 +34,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = (
|
||||
is_on=lambda x: x == TessieState.ONLINE,
|
||||
),
|
||||
TessieBinarySensorEntityDescription(
|
||||
key="charge_state_battery_heater_on",
|
||||
key="climate_state_battery_heater",
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
"state": {
|
||||
"name": "Status"
|
||||
},
|
||||
"charge_state_battery_heater_on": {
|
||||
"climate_state_battery_heater": {
|
||||
"name": "Battery heater"
|
||||
},
|
||||
"charge_state_charge_enable_request": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"services": {
|
||||
"start": "mdi:start",
|
||||
"start": "mdi:play",
|
||||
"pause": "mdi:pause",
|
||||
"cancel": "mdi:cancel",
|
||||
"finish": "mdi:check",
|
||||
|
||||
@@ -107,6 +107,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"inaccurate_position",
|
||||
"not_reporting",
|
||||
"operational",
|
||||
"system_shutdown_user",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"tracker_state": {
|
||||
"name": "Tracker state",
|
||||
"state": {
|
||||
"inaccurate_position": "Inaccurate position",
|
||||
"not_reporting": "Not reporting",
|
||||
"operational": "Operational",
|
||||
"system_shutdown_user": "System shutdown user",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiounifi==73"],
|
||||
"requirements": ["aiounifi==74"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"velbus-packet",
|
||||
"velbus-protocol"
|
||||
],
|
||||
"requirements": ["velbus-aio==2023.12.0"],
|
||||
"requirements": ["velbus-aio==2024.4.0"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10CF",
|
||||
|
||||
49
homeassistant/components/whirlpool/diagnostics.py
Normal file
49
homeassistant/components/whirlpool/diagnostics.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Diagnostics support for Whirlpool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import WhirlpoolData
|
||||
from .const import DOMAIN
|
||||
|
||||
TO_REDACT = {
|
||||
"SERIAL_NUMBER",
|
||||
"macaddress",
|
||||
"username",
|
||||
"password",
|
||||
"token",
|
||||
"unique_id",
|
||||
"SAID",
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
whirlpool: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
diagnostics_data = {
|
||||
"Washer_dryers": {
|
||||
wd["NAME"]: dict(wd.items())
|
||||
for wd in whirlpool.appliances_manager.washer_dryers
|
||||
},
|
||||
"aircons": {
|
||||
ac["NAME"]: dict(ac.items()) for ac in whirlpool.appliances_manager.aircons
|
||||
},
|
||||
"ovens": {
|
||||
oven["NAME"]: dict(oven.items())
|
||||
for oven in whirlpool.appliances_manager.ovens
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
||||
"appliances": async_redact_data(diagnostics_data, TO_REDACT),
|
||||
}
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["whirlpool"],
|
||||
"requirements": ["whirlpool-sixth-sense==0.18.6"]
|
||||
"requirements": ["whirlpool-sixth-sense==0.18.7"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from holidays import HolidayBase, country_holidays
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -13,7 +15,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
|
||||
from .const import CONF_PROVINCE, DOMAIN, PLATFORMS
|
||||
|
||||
|
||||
def _validate_country_and_province(
|
||||
async def _async_validate_country_and_province(
|
||||
hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None
|
||||
) -> None:
|
||||
"""Validate country and province."""
|
||||
@@ -21,7 +23,7 @@ def _validate_country_and_province(
|
||||
if not country:
|
||||
return
|
||||
try:
|
||||
country_holidays(country)
|
||||
await hass.async_add_executor_job(country_holidays, country)
|
||||
except NotImplementedError as ex:
|
||||
async_create_issue(
|
||||
hass,
|
||||
@@ -39,7 +41,9 @@ def _validate_country_and_province(
|
||||
if not province:
|
||||
return
|
||||
try:
|
||||
country_holidays(country, subdiv=province)
|
||||
await hass.async_add_executor_job(
|
||||
partial(country_holidays, country, subdiv=province)
|
||||
)
|
||||
except NotImplementedError as ex:
|
||||
async_create_issue(
|
||||
hass,
|
||||
@@ -66,10 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
country: str | None = entry.options.get(CONF_COUNTRY)
|
||||
province: str | None = entry.options.get(CONF_PROVINCE)
|
||||
|
||||
_validate_country_and_province(hass, entry, country, province)
|
||||
await _async_validate_country_and_province(hass, entry, country, province)
|
||||
|
||||
if country and CONF_LANGUAGE not in entry.options:
|
||||
cls: HolidayBase = country_holidays(country, subdiv=province)
|
||||
cls: HolidayBase = await hass.async_add_executor_job(
|
||||
partial(country_holidays, country, subdiv=province)
|
||||
)
|
||||
default_language = cls.default_language
|
||||
new_options = entry.options.copy()
|
||||
new_options[CONF_LANGUAGE] = default_language
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.45"]
|
||||
"requirements": ["holidays==0.46"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -124,8 +124,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
zha_data = get_zha_data(hass)
|
||||
|
||||
if zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True):
|
||||
setup_quirks(
|
||||
custom_quirks_path=zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH)
|
||||
await hass.async_add_import_executor_job(
|
||||
setup_quirks, zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH)
|
||||
)
|
||||
|
||||
# Load and cache device trigger information early
|
||||
|
||||
@@ -553,6 +553,13 @@ class OtaClientClusterHandler(ClientClusterHandler):
|
||||
Ota.AttributeDefs.current_file_version.name: True,
|
||||
}
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid: int, value: Any, timestamp: Any) -> None:
|
||||
"""Handle an attribute updated on this cluster."""
|
||||
# We intentionally avoid the `ClientClusterHandler` attribute update handler:
|
||||
# it emits a logbook event on every update, which pollutes the logbook
|
||||
ClusterHandler.attribute_updated(self, attrid, value, timestamp)
|
||||
|
||||
@property
|
||||
def current_file_version(self) -> int | None:
|
||||
"""Return cached value of current_file_version attribute."""
|
||||
|
||||
@@ -130,14 +130,9 @@ class ZHAFirmwareUpdateEntity(
|
||||
def _get_cluster_version(self) -> str | None:
|
||||
"""Synchronize current file version with the cluster."""
|
||||
|
||||
device = self._ota_cluster_handler._endpoint.device # pylint: disable=protected-access
|
||||
|
||||
if self._ota_cluster_handler.current_file_version is not None:
|
||||
return f"0x{self._ota_cluster_handler.current_file_version:08x}"
|
||||
|
||||
if device.sw_version is not None:
|
||||
return device.sw_version
|
||||
|
||||
return None
|
||||
|
||||
@callback
|
||||
|
||||
@@ -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 = "0b2"
|
||||
PATCH_VERSION: Final = "0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
||||
@@ -774,8 +774,11 @@ class HomeAssistant:
|
||||
) -> asyncio.Future[_T]:
|
||||
"""Add an executor job from within the event loop."""
|
||||
task = self.loop.run_in_executor(None, target, *args)
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(self._tasks.remove)
|
||||
|
||||
tracked = asyncio.current_task() in self._tasks
|
||||
task_bucket = self._tasks if tracked else self._background_tasks
|
||||
task_bucket.add(task)
|
||||
task.add_done_callback(task_bucket.remove)
|
||||
|
||||
return task
|
||||
|
||||
@@ -783,11 +786,11 @@ class HomeAssistant:
|
||||
def async_add_import_executor_job(
|
||||
self, target: Callable[[*_Ts], _T], *args: *_Ts
|
||||
) -> asyncio.Future[_T]:
|
||||
"""Add an import executor job from within the event loop."""
|
||||
task = self.loop.run_in_executor(self.import_executor, target, *args)
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(self._tasks.remove)
|
||||
return task
|
||||
"""Add an import executor job from within the event loop.
|
||||
|
||||
The future returned from this method must be awaited in the event loop.
|
||||
"""
|
||||
return self.loop.run_in_executor(self.import_executor, target, *args)
|
||||
|
||||
@overload
|
||||
@callback
|
||||
|
||||
@@ -3741,7 +3741,7 @@
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "assumed_state",
|
||||
"name": "Motionblinds BLE"
|
||||
"name": "Motionblinds Bluetooth"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -198,15 +198,16 @@ async def async_check_ha_config_file( # noqa: C901
|
||||
|
||||
# Check if the integration has a custom config validator
|
||||
config_validator = None
|
||||
try:
|
||||
config_validator = await integration.async_get_platform("config")
|
||||
except ImportError as err:
|
||||
# Filter out import error of the config platform.
|
||||
# If the config platform contains bad imports, make sure
|
||||
# that still fails.
|
||||
if err.name != f"{integration.pkg_path}.config":
|
||||
result.add_error(f"Error importing config platform {domain}: {err}")
|
||||
continue
|
||||
if integration.platforms_exists(("config",)):
|
||||
try:
|
||||
config_validator = await integration.async_get_platform("config")
|
||||
except ImportError as err:
|
||||
# Filter out import error of the config platform.
|
||||
# If the config platform contains bad imports, make sure
|
||||
# that still fails.
|
||||
if err.name != f"{integration.pkg_path}.config":
|
||||
result.add_error(f"Error importing config platform {domain}: {err}")
|
||||
continue
|
||||
|
||||
if config_validator is not None and hasattr(
|
||||
config_validator, "async_validate_config"
|
||||
|
||||
@@ -24,7 +24,13 @@ from homeassistant.core import Context, HomeAssistant, State, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from . import area_registry, config_validation as cv, device_registry, entity_registry
|
||||
from . import (
|
||||
area_registry,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity_registry,
|
||||
floor_registry,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SlotsType = dict[str, Any]
|
||||
@@ -144,16 +150,18 @@ class NoStatesMatchedError(IntentError):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None,
|
||||
area: str | None,
|
||||
domains: set[str] | None,
|
||||
device_classes: set[str] | None,
|
||||
name: str | None = None,
|
||||
area: str | None = None,
|
||||
floor: str | None = None,
|
||||
domains: set[str] | None = None,
|
||||
device_classes: set[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize error."""
|
||||
super().__init__()
|
||||
|
||||
self.name = name
|
||||
self.area = area
|
||||
self.floor = floor
|
||||
self.domains = domains
|
||||
self.device_classes = device_classes
|
||||
|
||||
@@ -220,12 +228,35 @@ def _find_area(
|
||||
return None
|
||||
|
||||
|
||||
def _filter_by_area(
|
||||
def _find_floor(
|
||||
id_or_name: str, floors: floor_registry.FloorRegistry
|
||||
) -> floor_registry.FloorEntry | None:
|
||||
"""Find an floor by id or name, checking aliases too."""
|
||||
floor = floors.async_get_floor(id_or_name) or floors.async_get_floor_by_name(
|
||||
id_or_name
|
||||
)
|
||||
if floor is not None:
|
||||
return floor
|
||||
|
||||
# Check floor aliases
|
||||
for maybe_floor in floors.floors.values():
|
||||
if not maybe_floor.aliases:
|
||||
continue
|
||||
|
||||
for floor_alias in maybe_floor.aliases:
|
||||
if id_or_name == floor_alias.casefold():
|
||||
return maybe_floor
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _filter_by_areas(
|
||||
states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]],
|
||||
area: area_registry.AreaEntry,
|
||||
areas: Iterable[area_registry.AreaEntry],
|
||||
devices: device_registry.DeviceRegistry,
|
||||
) -> Iterable[tuple[State, entity_registry.RegistryEntry | None]]:
|
||||
"""Filter state/entity pairs by an area."""
|
||||
filter_area_ids: set[str | None] = {a.id for a in areas}
|
||||
entity_area_ids: dict[str, str | None] = {}
|
||||
for _state, entity in states_and_entities:
|
||||
if entity is None:
|
||||
@@ -241,7 +272,7 @@ def _filter_by_area(
|
||||
entity_area_ids[entity.id] = device.area_id
|
||||
|
||||
for state, entity in states_and_entities:
|
||||
if (entity is not None) and (entity_area_ids.get(entity.id) == area.id):
|
||||
if (entity is not None) and (entity_area_ids.get(entity.id) in filter_area_ids):
|
||||
yield (state, entity)
|
||||
|
||||
|
||||
@@ -252,11 +283,14 @@ def async_match_states(
|
||||
name: str | None = None,
|
||||
area_name: str | None = None,
|
||||
area: area_registry.AreaEntry | None = None,
|
||||
floor_name: str | None = None,
|
||||
floor: floor_registry.FloorEntry | None = None,
|
||||
domains: Collection[str] | None = None,
|
||||
device_classes: Collection[str] | None = None,
|
||||
states: Iterable[State] | None = None,
|
||||
entities: entity_registry.EntityRegistry | None = None,
|
||||
areas: area_registry.AreaRegistry | None = None,
|
||||
floors: floor_registry.FloorRegistry | None = None,
|
||||
devices: device_registry.DeviceRegistry | None = None,
|
||||
assistant: str | None = None,
|
||||
) -> Iterable[State]:
|
||||
@@ -268,6 +302,15 @@ def async_match_states(
|
||||
if entities is None:
|
||||
entities = entity_registry.async_get(hass)
|
||||
|
||||
if devices is None:
|
||||
devices = device_registry.async_get(hass)
|
||||
|
||||
if areas is None:
|
||||
areas = area_registry.async_get(hass)
|
||||
|
||||
if floors is None:
|
||||
floors = floor_registry.async_get(hass)
|
||||
|
||||
# Gather entities
|
||||
states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]] = []
|
||||
for state in states:
|
||||
@@ -294,20 +337,35 @@ def async_match_states(
|
||||
if _is_device_class(state, entity, device_classes)
|
||||
]
|
||||
|
||||
filter_areas: list[area_registry.AreaEntry] = []
|
||||
|
||||
if (floor is None) and (floor_name is not None):
|
||||
# Look up floor by name
|
||||
floor = _find_floor(floor_name, floors)
|
||||
if floor is None:
|
||||
_LOGGER.warning("Floor not found: %s", floor_name)
|
||||
return
|
||||
|
||||
if floor is not None:
|
||||
filter_areas = [
|
||||
a for a in areas.async_list_areas() if a.floor_id == floor.floor_id
|
||||
]
|
||||
|
||||
if (area is None) and (area_name is not None):
|
||||
# Look up area by name
|
||||
if areas is None:
|
||||
areas = area_registry.async_get(hass)
|
||||
|
||||
area = _find_area(area_name, areas)
|
||||
assert area is not None, f"No area named {area_name}"
|
||||
if area is None:
|
||||
_LOGGER.warning("Area not found: %s", area_name)
|
||||
return
|
||||
|
||||
if area is not None:
|
||||
# Filter by states/entities by area
|
||||
if devices is None:
|
||||
devices = device_registry.async_get(hass)
|
||||
filter_areas = [area]
|
||||
|
||||
states_and_entities = list(_filter_by_area(states_and_entities, area, devices))
|
||||
if filter_areas:
|
||||
# Filter by states/entities by area
|
||||
states_and_entities = list(
|
||||
_filter_by_areas(states_and_entities, filter_areas, devices)
|
||||
)
|
||||
|
||||
if assistant is not None:
|
||||
# Filter by exposure
|
||||
@@ -318,9 +376,6 @@ def async_match_states(
|
||||
]
|
||||
|
||||
if name is not None:
|
||||
if devices is None:
|
||||
devices = device_registry.async_get(hass)
|
||||
|
||||
# Filter by name
|
||||
name = name.casefold()
|
||||
|
||||
@@ -389,7 +444,7 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
"""
|
||||
|
||||
slot_schema = {
|
||||
vol.Any("name", "area"): cv.string,
|
||||
vol.Any("name", "area", "floor"): cv.string,
|
||||
vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
@@ -453,7 +508,7 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
# Don't match on name if targeting all entities
|
||||
entity_name = None
|
||||
|
||||
# Look up area first to fail early
|
||||
# Look up area to fail early
|
||||
area_slot = slots.get("area", {})
|
||||
area_id = area_slot.get("value")
|
||||
area_name = area_slot.get("text")
|
||||
@@ -464,6 +519,17 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
if area is None:
|
||||
raise IntentHandleError(f"No area named {area_name}")
|
||||
|
||||
# Look up floor to fail early
|
||||
floor_slot = slots.get("floor", {})
|
||||
floor_id = floor_slot.get("value")
|
||||
floor_name = floor_slot.get("text")
|
||||
floor: floor_registry.FloorEntry | None = None
|
||||
if floor_id is not None:
|
||||
floors = floor_registry.async_get(hass)
|
||||
floor = floors.async_get_floor(floor_id)
|
||||
if floor is None:
|
||||
raise IntentHandleError(f"No floor named {floor_name}")
|
||||
|
||||
# Optional domain/device class filters.
|
||||
# Convert to sets for speed.
|
||||
domains: set[str] | None = None
|
||||
@@ -480,6 +546,7 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
hass,
|
||||
name=entity_name,
|
||||
area=area,
|
||||
floor=floor,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
assistant=intent_obj.assistant,
|
||||
@@ -491,6 +558,7 @@ class DynamicServiceIntentHandler(IntentHandler):
|
||||
raise NoStatesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
floor=floor_name or floor_id,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
)
|
||||
|
||||
@@ -844,6 +844,48 @@ class EntitySelector(Selector[EntitySelectorConfig]):
|
||||
return cast(list, vol.Schema([validate])(data)) # Output is a list
|
||||
|
||||
|
||||
class FloorSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent an floor selector config."""
|
||||
|
||||
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
||||
multiple: bool
|
||||
|
||||
|
||||
@SELECTORS.register("floor")
|
||||
class FloorSelector(Selector[AreaSelectorConfig]):
|
||||
"""Selector of a single or list of floors."""
|
||||
|
||||
selector_type = "floor"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("entity"): vol.All(
|
||||
cv.ensure_list,
|
||||
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("device"): vol.All(
|
||||
cv.ensure_list,
|
||||
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, config: FloorSelectorConfig | None = None) -> None:
|
||||
"""Instantiate a selector."""
|
||||
super().__init__(config)
|
||||
|
||||
def __call__(self, data: Any) -> str | list[str]:
|
||||
"""Validate the passed selection."""
|
||||
if not self.config["multiple"]:
|
||||
floor_id: str = vol.Schema(str)(data)
|
||||
return floor_id
|
||||
if not isinstance(data, list):
|
||||
raise vol.Invalid("Value should be a list")
|
||||
return [vol.Schema(str)(val) for val in data]
|
||||
|
||||
|
||||
class IconSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent an icon selector config."""
|
||||
|
||||
|
||||
@@ -1408,6 +1408,12 @@ def floor_id(hass: HomeAssistant, lookup_value: Any) -> str | None:
|
||||
floor_registry = fr.async_get(hass)
|
||||
if floor := floor_registry.async_get_floor_by_name(str(lookup_value)):
|
||||
return floor.floor_id
|
||||
|
||||
if aid := area_id(hass, lookup_value):
|
||||
area_reg = area_registry.async_get(hass)
|
||||
if area := area_reg.async_get_area(aid):
|
||||
return area.floor_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -1416,6 +1422,16 @@ def floor_name(hass: HomeAssistant, lookup_value: str) -> str | None:
|
||||
floor_registry = fr.async_get(hass)
|
||||
if floor := floor_registry.async_get_floor(lookup_value):
|
||||
return floor.name
|
||||
|
||||
if aid := area_id(hass, lookup_value):
|
||||
area_reg = area_registry.async_get(hass)
|
||||
if (
|
||||
(area := area_reg.async_get_area(aid))
|
||||
and area.floor_id
|
||||
and (floor := floor_registry.async_get_floor(area.floor_id))
|
||||
):
|
||||
return floor.name
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -750,9 +750,7 @@ class Integration:
|
||||
self._import_futures: dict[str, asyncio.Future[ModuleType]] = {}
|
||||
cache: dict[str, ModuleType | ComponentProtocol] = hass.data[DATA_COMPONENTS]
|
||||
self._cache = cache
|
||||
missing_platforms_cache: dict[str, ImportError] = hass.data[
|
||||
DATA_MISSING_PLATFORMS
|
||||
]
|
||||
missing_platforms_cache: dict[str, bool] = hass.data[DATA_MISSING_PLATFORMS]
|
||||
self._missing_platforms_cache = missing_platforms_cache
|
||||
self._top_level_files = top_level_files or set()
|
||||
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
||||
@@ -1085,8 +1083,7 @@ class Integration:
|
||||
import_futures: list[tuple[str, asyncio.Future[ModuleType]]] = []
|
||||
|
||||
for platform_name in platform_names:
|
||||
full_name = f"{domain}.{platform_name}"
|
||||
if platform := self._get_platform_cached_or_raise(full_name):
|
||||
if platform := self._get_platform_cached_or_raise(platform_name):
|
||||
platforms[platform_name] = platform
|
||||
continue
|
||||
|
||||
@@ -1095,6 +1092,7 @@ class Integration:
|
||||
in_progress_imports[platform_name] = future
|
||||
continue
|
||||
|
||||
full_name = f"{domain}.{platform_name}"
|
||||
if (
|
||||
self.import_executor
|
||||
and full_name not in self.hass.config.components
|
||||
@@ -1166,14 +1164,18 @@ class Integration:
|
||||
|
||||
return platforms
|
||||
|
||||
def _get_platform_cached_or_raise(self, full_name: str) -> ModuleType | None:
|
||||
def _get_platform_cached_or_raise(self, platform_name: str) -> ModuleType | None:
|
||||
"""Return a platform for an integration from cache."""
|
||||
full_name = f"{self.domain}.{platform_name}"
|
||||
if full_name in self._cache:
|
||||
# the cache is either a ModuleType or a ComponentProtocol
|
||||
# but we only care about the ModuleType here
|
||||
return self._cache[full_name] # type: ignore[return-value]
|
||||
if full_name in self._missing_platforms_cache:
|
||||
raise self._missing_platforms_cache[full_name]
|
||||
raise ModuleNotFoundError(
|
||||
f"Platform {full_name} not found",
|
||||
name=f"{self.pkg_path}.{platform_name}",
|
||||
)
|
||||
return None
|
||||
|
||||
def platforms_are_loaded(self, platform_names: Iterable[str]) -> bool:
|
||||
@@ -1189,9 +1191,7 @@ class Integration:
|
||||
|
||||
def get_platform(self, platform_name: str) -> ModuleType:
|
||||
"""Return a platform for an integration."""
|
||||
if platform := self._get_platform_cached_or_raise(
|
||||
f"{self.domain}.{platform_name}"
|
||||
):
|
||||
if platform := self._get_platform_cached_or_raise(platform_name):
|
||||
return platform
|
||||
return self._load_platform(platform_name)
|
||||
|
||||
@@ -1212,10 +1212,7 @@ class Integration:
|
||||
):
|
||||
existing_platforms.append(platform_name)
|
||||
continue
|
||||
missing_platforms[full_name] = ModuleNotFoundError(
|
||||
f"Platform {full_name} not found",
|
||||
name=f"{self.pkg_path}.{platform_name}",
|
||||
)
|
||||
missing_platforms[full_name] = True
|
||||
|
||||
return existing_platforms
|
||||
|
||||
@@ -1233,11 +1230,13 @@ class Integration:
|
||||
cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS]
|
||||
try:
|
||||
cache[full_name] = self._import_platform(platform_name)
|
||||
except ImportError as ex:
|
||||
except ModuleNotFoundError:
|
||||
if self.domain in cache:
|
||||
# If the domain is loaded, cache that the platform
|
||||
# does not exist so we do not try to load it again
|
||||
self._missing_platforms_cache[full_name] = ex
|
||||
self._missing_platforms_cache[full_name] = True
|
||||
raise
|
||||
except ImportError:
|
||||
raise
|
||||
except RuntimeError as err:
|
||||
# _DeadlockError inherits from RuntimeError
|
||||
|
||||
@@ -30,8 +30,8 @@ habluetooth==2.4.2
|
||||
hass-nabucasa==0.79.0
|
||||
hassil==1.6.1
|
||||
home-assistant-bluetooth==1.12.0
|
||||
home-assistant-frontend==20240329.0
|
||||
home-assistant-intents==2024.3.27
|
||||
home-assistant-frontend==20240403.1
|
||||
home-assistant-intents==2024.4.3
|
||||
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
|
||||
|
||||
@@ -504,6 +504,12 @@ async def async_prepare_setup_platform(
|
||||
log_error(f"Unable to import the component ({exc}).")
|
||||
return None
|
||||
|
||||
if not integration.platforms_exists((domain,)):
|
||||
log_error(
|
||||
f"Platform not found (No module named '{integration.pkg_path}.{domain}')"
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
platform = await integration.async_get_platform(domain)
|
||||
except ImportError as exc:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.4.0b2"
|
||||
version = "2024.4.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -45,7 +45,7 @@ Mastodon.py==1.8.1
|
||||
Pillow==10.2.0
|
||||
|
||||
# homeassistant.components.plex
|
||||
PlexAPI==4.15.10
|
||||
PlexAPI==4.15.11
|
||||
|
||||
# homeassistant.components.progettihwsw
|
||||
ProgettiHWSW==0.1.3
|
||||
@@ -392,7 +392,7 @@ aiotankerkoenig==0.4.1
|
||||
aiotractive==0.5.6
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==73
|
||||
aiounifi==74
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@@ -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
|
||||
@@ -867,7 +867,7 @@ fixerio==1.0.0a0
|
||||
fjaraskupan==2.3.0
|
||||
|
||||
# homeassistant.components.flexit_bacnet
|
||||
flexit_bacnet==2.1.0
|
||||
flexit_bacnet==2.2.1
|
||||
|
||||
# homeassistant.components.flipr
|
||||
flipr-api==1.5.1
|
||||
@@ -1074,13 +1074,13 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.45
|
||||
holidays==0.46
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240329.0
|
||||
home-assistant-frontend==20240403.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.3.27
|
||||
home-assistant-intents==2024.4.3
|
||||
|
||||
# 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
|
||||
@@ -2444,7 +2444,7 @@ rfk101py==0.0.1
|
||||
rflink==0.0.66
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring-doorbell[listen]==0.8.8
|
||||
ring-doorbell[listen]==0.8.9
|
||||
|
||||
# homeassistant.components.fleetgo
|
||||
ritassist==0.9.2
|
||||
@@ -2462,7 +2462,7 @@ rokuecp==0.19.2
|
||||
romy==0.0.7
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.13
|
||||
roombapy==1.8.1
|
||||
|
||||
# homeassistant.components.roon
|
||||
roonapi==0.1.6
|
||||
@@ -2795,7 +2795,7 @@ vallox-websocket-api==5.1.1
|
||||
vehicle==2.2.1
|
||||
|
||||
# homeassistant.components.velbus
|
||||
velbus-aio==2023.12.0
|
||||
velbus-aio==2024.4.0
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.19
|
||||
@@ -2847,7 +2847,7 @@ webmin-xmlrpc==0.0.2
|
||||
webrtc-noise-gain==1.2.3
|
||||
|
||||
# homeassistant.components.whirlpool
|
||||
whirlpool-sixth-sense==0.18.6
|
||||
whirlpool-sixth-sense==0.18.7
|
||||
|
||||
# homeassistant.components.whois
|
||||
whois==0.9.27
|
||||
@@ -2925,7 +2925,7 @@ zamg==0.3.6
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.131.0
|
||||
zeroconf==0.132.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.1
|
||||
|
||||
@@ -39,7 +39,7 @@ HATasmota==0.8.0
|
||||
Pillow==10.2.0
|
||||
|
||||
# homeassistant.components.plex
|
||||
PlexAPI==4.15.10
|
||||
PlexAPI==4.15.11
|
||||
|
||||
# homeassistant.components.progettihwsw
|
||||
ProgettiHWSW==0.1.3
|
||||
@@ -365,7 +365,7 @@ aiotankerkoenig==0.4.1
|
||||
aiotractive==0.5.6
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==73
|
||||
aiounifi==74
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@@ -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
|
||||
@@ -705,7 +705,7 @@ fivem-api==0.1.2
|
||||
fjaraskupan==2.3.0
|
||||
|
||||
# homeassistant.components.flexit_bacnet
|
||||
flexit_bacnet==2.1.0
|
||||
flexit_bacnet==2.2.1
|
||||
|
||||
# homeassistant.components.flipr
|
||||
flipr-api==1.5.1
|
||||
@@ -873,13 +873,13 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.45
|
||||
holidays==0.46
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240329.0
|
||||
home-assistant-frontend==20240403.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.3.27
|
||||
home-assistant-intents==2024.4.3
|
||||
|
||||
# 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
|
||||
@@ -1887,7 +1887,7 @@ reolink-aio==0.8.9
|
||||
rflink==0.0.66
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring-doorbell[listen]==0.8.8
|
||||
ring-doorbell[listen]==0.8.9
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.19.2
|
||||
@@ -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
|
||||
|
||||
@@ -1461,7 +1461,10 @@ def mock_integration(
|
||||
|
||||
|
||||
def mock_platform(
|
||||
hass: HomeAssistant, platform_path: str, module: Mock | MockPlatform | None = None
|
||||
hass: HomeAssistant,
|
||||
platform_path: str,
|
||||
module: Mock | MockPlatform | None = None,
|
||||
built_in=True,
|
||||
) -> None:
|
||||
"""Mock a platform.
|
||||
|
||||
@@ -1472,7 +1475,7 @@ def mock_platform(
|
||||
module_cache = hass.data[loader.DATA_COMPONENTS]
|
||||
|
||||
if domain not in integration_cache:
|
||||
mock_integration(hass, MockModule(domain))
|
||||
mock_integration(hass, MockModule(domain), built_in=built_in)
|
||||
|
||||
integration_cache[domain]._top_level_files.add(f"{platform_name}.py")
|
||||
_LOGGER.info("Adding mock integration platform: %s", platform_path)
|
||||
@@ -1665,6 +1668,7 @@ def setup_test_component_platform(
|
||||
domain: str,
|
||||
entities: Sequence[Entity],
|
||||
from_config_entry: bool = False,
|
||||
built_in: bool = True,
|
||||
) -> MockPlatform:
|
||||
"""Mock a test component platform for tests."""
|
||||
|
||||
@@ -1695,9 +1699,5 @@ def setup_test_component_platform(
|
||||
platform.async_setup_entry = _async_setup_entry
|
||||
platform.async_setup_platform = None
|
||||
|
||||
mock_platform(
|
||||
hass,
|
||||
f"test.{domain}",
|
||||
platform,
|
||||
)
|
||||
mock_platform(hass, f"test.{domain}", platform, built_in=built_in)
|
||||
return platform
|
||||
|
||||
@@ -201,7 +201,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory)
|
||||
):
|
||||
freezer.tick(SCAN_INTERVAL * 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
power = hass.states.get("sensor.mydevicename_total_energy")
|
||||
assert power.state == "unknown"
|
||||
# sun rose again
|
||||
@@ -218,7 +218,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory)
|
||||
):
|
||||
freezer.tick(SCAN_INTERVAL * 4)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
power = hass.states.get("sensor.mydevicename_power_output")
|
||||
assert power is not None
|
||||
assert power.state == "45.7"
|
||||
@@ -237,7 +237,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory)
|
||||
):
|
||||
freezer.tick(SCAN_INTERVAL * 6)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
power = hass.states.get("sensor.mydevicename_power_output")
|
||||
assert power.state == "unknown" # should this be 'available'?
|
||||
|
||||
@@ -277,7 +277,7 @@ async def test_sensor_unknown_error(
|
||||
):
|
||||
freezer.tick(SCAN_INTERVAL * 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert (
|
||||
"Exception: AuroraError('another error') occurred, 2 retries remaining"
|
||||
in caplog.text
|
||||
|
||||
@@ -74,6 +74,7 @@ MQTT_CLIENT_RESPONSE = {
|
||||
"status": {"state": "active", "connectionStatus": "Connected"},
|
||||
"config": {
|
||||
"server": {"protocol": "tcp", "host": "192.168.0.90", "port": 1883},
|
||||
"deviceTopicPrefix": f"axis/{MAC}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -91,9 +91,9 @@ async def test_device_support_mqtt(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry
|
||||
) -> None:
|
||||
"""Successful setup."""
|
||||
mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8")
|
||||
mqtt_mock.async_subscribe.assert_called_with(f"axis/{MAC}/#", mock.ANY, 0, "utf-8")
|
||||
|
||||
topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0"
|
||||
topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0"
|
||||
message = (
|
||||
b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR",'
|
||||
b' "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}'
|
||||
|
||||
@@ -278,7 +278,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None:
|
||||
result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
|
||||
)
|
||||
assert result["type"] == "create_entry"
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entry = hass.config_entries.async_entries("cast")[0]
|
||||
|
||||
assert castbrowser_mock.return_value.start_discovery.call_count == 1
|
||||
@@ -291,7 +291,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None:
|
||||
user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
castbrowser_mock.return_value.start_discovery.assert_not_called()
|
||||
castbrowser_mock.assert_not_called()
|
||||
|
||||
@@ -137,8 +137,8 @@ async def async_setup_cast_internal_discovery(hass, config=None):
|
||||
return_value=browser,
|
||||
) as cast_browser:
|
||||
add_entities = await async_setup_cast(hass, config)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert browser.start_discovery.call_count == 1
|
||||
|
||||
@@ -209,8 +209,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf
|
||||
entry = MockConfigEntry(data=data, domain="cast")
|
||||
entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
discovery_callback = cast_browser.call_args[0][0].add_cast
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.const import STATE_OFF, STATE_ON
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tests.components.light.common import MockLight
|
||||
from tests.components.sensor.common import MockSensor
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@@ -118,3 +119,11 @@ def mock_light_entities() -> list["MockLight"]:
|
||||
MockLight("Ceiling", STATE_OFF),
|
||||
MockLight(None, STATE_OFF),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sensor_entities() -> dict[str, "MockSensor"]:
|
||||
"""Return mocked sensor entities."""
|
||||
from tests.components.sensor.common import get_mock_sensor_entities
|
||||
|
||||
return get_mock_sensor_entities()
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -480,6 +481,20 @@ async def test_error_no_area(hass: HomeAssistant, init_components) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_error_no_floor(hass: HomeAssistant, init_components) -> None:
|
||||
"""Test error message when floor is missing."""
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all the lights on missing floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Sorry, I am not aware of any floor called missing"
|
||||
)
|
||||
|
||||
|
||||
async def test_error_no_device_in_area(
|
||||
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
|
||||
) -> None:
|
||||
@@ -549,6 +564,48 @@ async def test_error_no_domain_in_area(
|
||||
)
|
||||
|
||||
|
||||
async def test_error_no_domain_in_floor(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
area_registry: ar.AreaRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test error message when no devices/entities for a domain exist on a floor."""
|
||||
floor_ground = floor_registry.async_create("ground")
|
||||
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||
area_kitchen = area_registry.async_update(
|
||||
area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id
|
||||
)
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all lights on the ground floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Sorry, I am not aware of any light on the ground floor"
|
||||
)
|
||||
|
||||
# Add a new floor/area to trigger registry event handlers
|
||||
floor_upstairs = floor_registry.async_create("upstairs")
|
||||
area_bedroom = area_registry.async_get_or_create("bedroom_id")
|
||||
area_bedroom = area_registry.async_update(
|
||||
area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id
|
||||
)
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all lights upstairs", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Sorry, I am not aware of any light on the upstairs floor"
|
||||
)
|
||||
|
||||
|
||||
async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None:
|
||||
"""Test error message when no entities of a device class exist."""
|
||||
|
||||
@@ -736,7 +793,7 @@ async def test_no_states_matched_default_error(
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.conversation.default_agent.intent.async_handle",
|
||||
side_effect=intent.NoStatesMatchedError(None, None, None, None),
|
||||
side_effect=intent.NoStatesMatchedError(),
|
||||
):
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on lights in the kitchen", None, Context(), None
|
||||
@@ -759,11 +816,16 @@ async def test_empty_aliases(
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test that empty aliases are not added to slot lists."""
|
||||
floor_1 = floor_registry.async_create("first floor", aliases={" "})
|
||||
|
||||
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
|
||||
area_kitchen = area_registry.async_update(area_kitchen.id, aliases={" "})
|
||||
area_kitchen = area_registry.async_update(
|
||||
area_kitchen.id, aliases={" "}, floor_id=floor_1
|
||||
)
|
||||
|
||||
entry = MockConfigEntry()
|
||||
entry.add_to_hass(hass)
|
||||
@@ -799,7 +861,7 @@ async def test_empty_aliases(
|
||||
slot_lists = mock_recognize_all.call_args[0][2]
|
||||
|
||||
# Slot lists should only contain non-empty text
|
||||
assert slot_lists.keys() == {"area", "name"}
|
||||
assert slot_lists.keys() == {"area", "name", "floor"}
|
||||
areas = slot_lists["area"]
|
||||
assert len(areas.values) == 1
|
||||
assert areas.values[0].value_out == area_kitchen.id
|
||||
@@ -810,6 +872,11 @@ async def test_empty_aliases(
|
||||
assert names.values[0].value_out == kitchen_light.name
|
||||
assert names.values[0].text_in.text == kitchen_light.name
|
||||
|
||||
floors = slot_lists["floor"]
|
||||
assert len(floors.values) == 1
|
||||
assert floors.values[0].value_out == floor_1.floor_id
|
||||
assert floors.values[0].text_in.text == floor_1.name
|
||||
|
||||
|
||||
async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None:
|
||||
"""Test that sentences for all domains are always loaded."""
|
||||
|
||||
@@ -2,14 +2,26 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import conversation, cover, media_player, vacuum, valve
|
||||
from homeassistant.components import (
|
||||
conversation,
|
||||
cover,
|
||||
light,
|
||||
media_player,
|
||||
vacuum,
|
||||
valve,
|
||||
)
|
||||
from homeassistant.components.cover import intent as cover_intent
|
||||
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
|
||||
from homeassistant.components.media_player import intent as media_player_intent
|
||||
from homeassistant.components.vacuum import intent as vaccum_intent
|
||||
from homeassistant.const import STATE_CLOSED
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_mock_service
|
||||
@@ -244,3 +256,92 @@ async def test_media_player_intents(
|
||||
"entity_id": entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.75,
|
||||
}
|
||||
|
||||
|
||||
async def test_turn_floor_lights_on_off(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
entity_registry: er.EntityRegistry,
|
||||
area_registry: ar.AreaRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test that we can turn lights on/off for an entire floor."""
|
||||
floor_ground = floor_registry.async_create("ground", aliases={"downstairs"})
|
||||
floor_upstairs = floor_registry.async_create("upstairs")
|
||||
|
||||
# Kitchen and living room are on the ground floor
|
||||
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||
area_kitchen = area_registry.async_update(
|
||||
area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id
|
||||
)
|
||||
|
||||
area_living_room = area_registry.async_get_or_create("living_room_id")
|
||||
area_living_room = area_registry.async_update(
|
||||
area_living_room.id, name="living_room", floor_id=floor_ground.floor_id
|
||||
)
|
||||
|
||||
# Bedroom is upstairs
|
||||
area_bedroom = area_registry.async_get_or_create("bedroom_id")
|
||||
area_bedroom = area_registry.async_update(
|
||||
area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id
|
||||
)
|
||||
|
||||
# One light per area
|
||||
kitchen_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "kitchen_light"
|
||||
)
|
||||
kitchen_light = entity_registry.async_update_entity(
|
||||
kitchen_light.entity_id, area_id=area_kitchen.id
|
||||
)
|
||||
hass.states.async_set(kitchen_light.entity_id, "off")
|
||||
|
||||
living_room_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "living_room_light"
|
||||
)
|
||||
living_room_light = entity_registry.async_update_entity(
|
||||
living_room_light.entity_id, area_id=area_living_room.id
|
||||
)
|
||||
hass.states.async_set(living_room_light.entity_id, "off")
|
||||
|
||||
bedroom_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "bedroom_light"
|
||||
)
|
||||
bedroom_light = entity_registry.async_update_entity(
|
||||
bedroom_light.entity_id, area_id=area_bedroom.id
|
||||
)
|
||||
hass.states.async_set(bedroom_light.entity_id, "off")
|
||||
|
||||
# Target by floor
|
||||
on_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all lights downstairs", None, Context(), None
|
||||
)
|
||||
|
||||
assert len(on_calls) == 2
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
kitchen_light.entity_id,
|
||||
living_room_light.entity_id,
|
||||
}
|
||||
|
||||
on_calls.clear()
|
||||
result = await conversation.async_converse(
|
||||
hass, "upstairs lights on", None, Context(), None
|
||||
)
|
||||
|
||||
assert len(on_calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
bedroom_light.entity_id
|
||||
}
|
||||
|
||||
off_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_OFF)
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn upstairs lights off", None, Context(), None
|
||||
)
|
||||
|
||||
assert len(off_calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
bedroom_light.entity_id
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ async def test_image_update_unavailable(
|
||||
# fritzbox becomes unavailable
|
||||
fc_class_mock().call_action_side_effect(ReadTimeout)
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("image.mock_title_guestwifi")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
@@ -207,7 +207,7 @@ async def test_image_update_unavailable(
|
||||
# fritzbox is available again
|
||||
fc_class_mock().call_action_side_effect(None)
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("image.mock_title_guestwifi")
|
||||
assert state.state != STATE_UNKNOWN
|
||||
|
||||
@@ -134,7 +134,7 @@ async def test_sensor_update_fail(
|
||||
|
||||
fc_class_mock().call_action_side_effect(FritzConnectionException)
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
sensors = hass.states.async_all(SENSOR_DOMAIN)
|
||||
for sensor in sensors:
|
||||
|
||||
@@ -104,7 +104,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -123,7 +123,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -146,7 +146,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_device_alarm")
|
||||
assert state
|
||||
|
||||
@@ -65,7 +65,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_template")
|
||||
assert state
|
||||
|
||||
@@ -145,7 +145,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_preset")
|
||||
assert state
|
||||
@@ -203,7 +203,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
@@ -243,7 +243,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 4
|
||||
assert fritz().login.call_count == 4
|
||||
@@ -386,7 +386,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
@@ -397,7 +397,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert fritz().update_devices.call_count == 3
|
||||
@@ -422,7 +422,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_climate")
|
||||
assert state
|
||||
|
||||
@@ -108,7 +108,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_climate")
|
||||
assert state
|
||||
|
||||
@@ -237,7 +237,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -259,7 +259,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 4
|
||||
assert fritz().login.call_count == 4
|
||||
@@ -294,7 +294,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_light")
|
||||
assert state
|
||||
|
||||
@@ -87,7 +87,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -105,7 +105,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 4
|
||||
assert fritz().login.call_count == 4
|
||||
@@ -128,7 +128,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_device_temperature")
|
||||
assert state
|
||||
|
||||
@@ -151,7 +151,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
@@ -169,7 +169,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert fritz().update_devices.call_count == 4
|
||||
assert fritz().login.call_count == 4
|
||||
@@ -207,7 +207,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_switch")
|
||||
assert state
|
||||
|
||||
@@ -99,7 +99,7 @@ async def test_setup(
|
||||
# so no changes to entities.
|
||||
mock_feed.return_value.update.return_value = "OK_NO_DATA", None
|
||||
async_fire_time_changed(hass, utcnow + geo_rss_events.SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
all_states = hass.states.async_all()
|
||||
assert len(all_states) == 1
|
||||
@@ -109,7 +109,7 @@ async def test_setup(
|
||||
# Simulate an update - empty data, removes all entities
|
||||
mock_feed.return_value.update.return_value = "ERROR", None
|
||||
async_fire_time_changed(hass, utcnow + 2 * geo_rss_events.SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
all_states = hass.states.async_all()
|
||||
assert len(all_states) == 1
|
||||
|
||||
@@ -46,7 +46,7 @@ async def test_sensors(
|
||||
):
|
||||
next_update = dt_util.utcnow() + timedelta(minutes=15)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(SENSOR)
|
||||
assert state.state == result
|
||||
@@ -61,7 +61,7 @@ async def test_sensor_reauth_trigger(
|
||||
with patch(TOKEN, side_effect=RefreshError):
|
||||
next_update = dt_util.utcnow() + timedelta(minutes=15)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ async def test_window_shuttler(
|
||||
|
||||
windowshutter.is_open = False
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
@@ -68,12 +68,12 @@ async def test_window_shuttler_battery(
|
||||
|
||||
windowshutter.battery = 1 # maxcube-api MAX_DEVICE_BATTERY_LOW
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(BATTERY_ENTITY_ID)
|
||||
assert state.state == STATE_ON # on means low
|
||||
|
||||
windowshutter.battery = 0 # maxcube-api MAX_DEVICE_BATTERY_OK
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(BATTERY_ENTITY_ID)
|
||||
assert state.state == STATE_OFF # off means normal
|
||||
|
||||
@@ -140,7 +140,7 @@ async def test_thermostat_set_hvac_mode_off(
|
||||
thermostat.valve_position = 0
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.OFF
|
||||
@@ -168,8 +168,8 @@ async def test_thermostat_set_hvac_mode_heat(
|
||||
thermostat.mode = MAX_DEVICE_MODE_MANUAL
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -204,7 +204,7 @@ async def test_thermostat_set_temperature(
|
||||
thermostat.valve_position = 0
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.AUTO
|
||||
@@ -248,7 +248,7 @@ async def test_thermostat_set_preset_on(
|
||||
thermostat.target_temperature = ON_TEMPERATURE
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -273,7 +273,7 @@ async def test_thermostat_set_preset_comfort(
|
||||
thermostat.target_temperature = thermostat.comfort_temperature
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -298,7 +298,7 @@ async def test_thermostat_set_preset_eco(
|
||||
thermostat.target_temperature = thermostat.eco_temperature
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -323,7 +323,7 @@ async def test_thermostat_set_preset_away(
|
||||
thermostat.target_temperature = thermostat.eco_temperature
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -348,7 +348,7 @@ async def test_thermostat_set_preset_boost(
|
||||
thermostat.target_temperature = thermostat.eco_temperature
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.AUTO
|
||||
@@ -401,7 +401,7 @@ async def test_wallthermostat_set_hvac_mode_heat(
|
||||
wallthermostat.target_temperature = MIN_TEMPERATURE
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(WALL_ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
@@ -425,7 +425,7 @@ async def test_wallthermostat_set_hvac_mode_auto(
|
||||
wallthermostat.target_temperature = 23.0
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(WALL_ENTITY_ID)
|
||||
assert state.state == HVACMode.AUTO
|
||||
|
||||
@@ -125,7 +125,7 @@ async def test_site_cannot_update(
|
||||
|
||||
future_time = utcnow() + timedelta(minutes=20)
|
||||
async_fire_time_changed(hass, future_time)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
weather = hass.states.get("weather.met_office_wavertree_daily")
|
||||
assert weather.state == STATE_UNAVAILABLE
|
||||
@@ -297,7 +297,7 @@ async def test_forecast_service(
|
||||
# Trigger data refetch
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert wavertree_data["wavertree_daily_mock"].call_count == 2
|
||||
assert wavertree_data["wavertree_hourly_mock"].call_count == 1
|
||||
@@ -324,7 +324,7 @@ async def test_forecast_service(
|
||||
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
@@ -412,7 +412,7 @@ async def test_forecast_subscription(
|
||||
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["id"] == subscription_id
|
||||
@@ -430,6 +430,6 @@ async def test_forecast_subscription(
|
||||
)
|
||||
freezer.tick(timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
@@ -88,7 +88,7 @@ async def test_device_trackers(
|
||||
WIRELESS_DATA.append(DEVICE_2_WIRELESS)
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
device_2 = hass.states.get("device_tracker.device_2")
|
||||
assert device_2
|
||||
@@ -101,7 +101,7 @@ async def test_device_trackers(
|
||||
del WIRELESS_DATA[1] # device 2 is removed from wireless list
|
||||
with freeze_time(utcnow() + timedelta(minutes=4)):
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=4))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
device_2 = hass.states.get("device_tracker.device_2")
|
||||
assert device_2
|
||||
@@ -110,7 +110,7 @@ async def test_device_trackers(
|
||||
# test state changes to away if last_seen past consider_home_interval
|
||||
with freeze_time(utcnow() + timedelta(minutes=6)):
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=6))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
device_2 = hass.states.get("device_tracker.device_2")
|
||||
assert device_2
|
||||
@@ -266,7 +266,7 @@ async def test_update_failed(hass: HomeAssistant, mock_device_registry_devices)
|
||||
mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect
|
||||
):
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
device_1 = hass.states.get("device_tracker.device_1")
|
||||
assert device_1
|
||||
|
||||
@@ -183,7 +183,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None:
|
||||
# Restoring other media player to its previous state
|
||||
# The zone should not be restored
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Checking that values were not (!) restored
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
@@ -193,7 +193,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None:
|
||||
|
||||
# Restoring media player to its previous state
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -226,7 +226,7 @@ async def test_service_calls_with_all_entities(hass: HomeAssistant) -> None:
|
||||
|
||||
# Restoring media player to its previous state
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "all"})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -259,7 +259,7 @@ async def test_service_calls_without_relevant_entities(hass: HomeAssistant) -> N
|
||||
|
||||
# Restoring media player to its previous state
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -273,7 +273,7 @@ async def test_restore_without_snapshort(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch.object(MockMonoprice, "restore_zone") as method_call:
|
||||
await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert not method_call.called
|
||||
|
||||
@@ -295,7 +295,7 @@ async def test_update(hass: HomeAssistant) -> None:
|
||||
monoprice.set_volume(11, 38)
|
||||
|
||||
await async_update_entity(hass, ZONE_1_ID)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -321,7 +321,7 @@ async def test_failed_update(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch.object(MockMonoprice, "zone_status", side_effect=SerialException):
|
||||
await async_update_entity(hass, ZONE_1_ID)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -347,7 +347,7 @@ async def test_empty_update(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch.object(MockMonoprice, "zone_status", return_value=None):
|
||||
await async_update_entity(hass, ZONE_1_ID)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
@@ -418,7 +418,7 @@ async def test_unknown_source(hass: HomeAssistant) -> None:
|
||||
monoprice.set_source(11, 5)
|
||||
|
||||
await async_update_entity(hass, ZONE_1_ID)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ZONE_1_ID)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Tests for the Motionblinds BLE integration."""
|
||||
"""Tests for the Motionblinds Bluetooth integration."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Setup the MotionBlinds BLE tests."""
|
||||
"""Setup the Motionblinds Bluetooth tests."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test the MotionBlinds BLE config flow."""
|
||||
"""Test the Motionblinds Bluetooth config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.components.mqtt.models import (
|
||||
MqttValueTemplateException,
|
||||
ReceiveMessage,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE,
|
||||
@@ -52,10 +53,9 @@ from tests.common import (
|
||||
async_fire_mqtt_message,
|
||||
async_fire_time_changed,
|
||||
mock_restore_cache,
|
||||
setup_test_component_platform,
|
||||
)
|
||||
from tests.testing_config.custom_components.test.sensor import ( # type: ignore[attr-defined]
|
||||
DEVICE_CLASSES,
|
||||
)
|
||||
from tests.components.sensor.common import MockSensor
|
||||
from tests.typing import (
|
||||
MqttMockHAClient,
|
||||
MqttMockHAClientGenerator,
|
||||
@@ -3142,12 +3142,12 @@ async def test_debug_info_non_mqtt(
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
mock_sensor_entities: dict[str, MockSensor],
|
||||
) -> None:
|
||||
"""Test we get empty debug_info for a device with non MQTT entities."""
|
||||
await mqtt_mock_entry()
|
||||
domain = "sensor"
|
||||
platform = getattr(hass.components, f"test.{domain}")
|
||||
platform.init()
|
||||
setup_test_component_platform(hass, domain, mock_sensor_entities)
|
||||
|
||||
config_entry = MockConfigEntry(domain="test", data={})
|
||||
config_entry.add_to_hass(hass)
|
||||
@@ -3155,11 +3155,11 @@ async def test_debug_info_non_mqtt(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
)
|
||||
for device_class in DEVICE_CLASSES:
|
||||
for device_class in SensorDeviceClass:
|
||||
entity_registry.async_get_or_create(
|
||||
domain,
|
||||
"test",
|
||||
platform.ENTITIES[device_class].unique_id,
|
||||
mock_sensor_entities[device_class].unique_id,
|
||||
device_id=device_entry.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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user