Compare commits

..

40 Commits

Author SHA1 Message Date
Paulus Schoutsen bce18bf61a 2023.5.1 (#92513) 2023-05-04 12:45:55 -04:00
Paulus Schoutsen eda0731e60 Bumped version to 2023.5.1 2023-05-04 10:23:58 -04:00
Bram Kragten 238c87055f Update frontend to 20230503.2 (#92508) 2023-05-04 10:23:53 -04:00
Erik Montnemery 4b4464a3de Force migration of cloud settings to exposed_entities (#92499) 2023-05-04 10:23:52 -04:00
J. Nick Koston a07fbdd61c Bump bluetooth-auto-recovery 1.1.2 (#92495)
Improve handling when getting the power state times out

https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/compare/v1.1.1...v1.1.2
2023-05-04 10:23:52 -04:00
J. Nick Koston 3126ebe9d6 Fix lifx light strips when color zones are not initially populated (#92487)
fixes #92456
2023-05-04 10:23:51 -04:00
Aaron Bach 89aec9d356 Bump aionotion to 2023.05.0 (#92451) 2023-05-04 10:23:49 -04:00
J. Nick Koston 0cfa566ff6 Fix onvif cameras with invalid encodings in device info (#92450)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2023-05-04 10:23:49 -04:00
J. Nick Koston fffece95f5 Fix onvif setup when time set service is not functional (#92447) 2023-05-04 10:23:48 -04:00
Franck Nijhof c61e29709c 2023.5.0 (#92422) 2023-05-03 20:46:28 +02:00
Michael Hansen 458fe17a48 Bump voip-utils to 0.0.7 (#92372) 2023-05-03 20:02:45 +02:00
Franck Nijhof 15fdefd23b Bumped version to 2023.5.0 2023-05-03 19:44:53 +02:00
Michael Hansen 576f9600b5 Pass OPUS payload ID through VoIP (#92421) 2023-05-03 19:44:34 +02:00
Franck Nijhof 7a62574360 Bumped version to 2023.5.0b9 2023-05-03 18:59:42 +02:00
Erik Montnemery 0251d677d8 Migrate cloud settings for all Google entities (#92416) 2023-05-03 18:59:32 +02:00
Michael Hansen 2cd9b94ecb Skip unexposed entities in intent handlers (#92415)
* Filter intent handler entities by exposure

* Add test for skipping unexposed entities
2023-05-03 18:59:29 +02:00
Erik Montnemery 3cd2ab2319 Migrate cloud settings for all Alexa entities (#92413)
* Migrate cloud settings for all Alexa entities

* Also set settings for unknown entities
2023-05-03 18:59:25 +02:00
J. Nick Koston 4f0d403393 Bump bluetooth-auto-recovery to 1.1.1 (#92412)
* Bump bluetooth-auto-recovery to 1.1.0

https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/releases/tag/v1.1.0

In https://github.com/home-assistant/operating-system/issues/2485 is was discovered that a more aggressive reset strategy is needed due to a yet unsolved bug in the linux 6.1.x kernel series

* bump to 1.1.1 since event 47 cannot be decoded (newer kernels only)
2023-05-03 18:59:22 +02:00
Bram Kragten b558cf8b59 Update frontend to 20230503.1 (#92410) 2023-05-03 18:59:18 +02:00
Erik Montnemery 820c7b77ce Update cloud WS API for getting entity (#92409)
* Update cloud WS API for getting entity

* Adjust comment
2023-05-03 18:59:15 +02:00
Erik Montnemery 9d0fc916fc Use exposed_entities API in cloud tests (#92408) 2023-05-03 18:59:11 +02:00
Erik Montnemery 387f07a97f Include all entities in cloud lists (#92406) 2023-05-03 18:59:08 +02:00
J. Nick Koston 44968cfc7c Handle webhook URL rejection in onvif (#92405) 2023-05-03 18:59:04 +02:00
Erik Montnemery c6751bed86 Allow setting google disable 2fa flag on any entity (#92403)
* Allow setting google disable 2fa flag on any entity

* Fix test

* Include disable_2fa flag in cloud/google_assistant/entities/get
2023-05-03 18:59:01 +02:00
Bram Kragten b87e3860d9 Update frontend to 20230503.0 (#92402) 2023-05-03 18:58:57 +02:00
David F. Mulcahey 8ef6bd85f5 Bump ZHA quirks (#92400) 2023-05-03 18:58:54 +02:00
Erik Montnemery ad4fed4f60 Allow exposing any entity to the default conversation agent (#92398)
* Allow exposing any entity to the default conversation agent

* Tweak

* Fix race, update tests

* Update tests
2023-05-03 18:58:51 +02:00
Erik Montnemery 1050895657 Don't use storage collection helper in ExposedEntities (#92396)
* Don't use storage collection helper in ExposedEntities

* Fix tests
2023-05-03 18:58:47 +02:00
Erik Montnemery c31d657206 Improve exposed entities tests (#92389) 2023-05-03 18:58:44 +02:00
repaxan 88343bed77 Add ZHA binding for window coverings (#92387) 2023-05-03 18:58:40 +02:00
Artem Draft 51a10a84da Bump pybravia to 0.3.3 (#92378) 2023-05-03 18:58:35 +02:00
Paulus Schoutsen 5f3bbf2804 Bumped version to 2023.5.0b8 2023-05-02 22:39:38 -04:00
Paulus Schoutsen b8eebf085c Fix deserialize bug + add test coverage (#92382) 2023-05-02 22:39:33 -04:00
Franck Nijhof cdfd53e1cc Bumped version to 2023.5.0b7 2023-05-02 22:44:32 +02:00
Bram Kragten ca147dd97e Update frontend to 20230502.0 (#92373) 2023-05-02 22:43:23 +02:00
Erik Montnemery 5b1278d885 Allow exposing entities not in the entity registry to assistants (#92363) 2023-05-02 22:43:19 +02:00
J. Nick Koston 0db28dcf4d Start onvif events later (#92354) 2023-05-02 22:43:15 +02:00
Raman Gupta 7c651665c5 Clean up zwave_js.cover (#92353) 2023-05-02 22:43:12 +02:00
J. Nick Koston 2f3964e3ce Bump ulid-transform to 0.7.2 (#92344) 2023-05-02 22:43:08 +02:00
John Pettitt eef95fa0d4 Increase default timeout in sense (#90556)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-05-02 22:43:03 +02:00
88 changed files with 1469 additions and 626 deletions
@@ -60,6 +60,7 @@ class AlexaConfig(AbstractConfig):
"""Return an identifier for the user that represents this config."""
return ""
@core.callback
def should_expose(self, entity_id):
"""If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter:
@@ -18,7 +18,7 @@
"bleak==0.20.2",
"bleak-retry-connector==3.0.2",
"bluetooth-adapters==0.15.3",
"bluetooth-auto-recovery==1.0.3",
"bluetooth-auto-recovery==1.1.2",
"bluetooth-data-tools==0.4.0",
"dbus-fast==1.85.0"
]
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pybravia"],
"requirements": ["pybravia==0.3.2"],
"requirements": ["pybravia==0.3.3"],
"ssdp": [
{
"st": "urn:schemas-sony-com:service:ScalarWebAPI:1",
+34 -18
View File
@@ -22,6 +22,7 @@ from homeassistant.components.alexa import (
)
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
async_get_assistant_settings,
async_listen_entity_updates,
async_should_expose,
@@ -29,6 +30,7 @@ from homeassistant.components.homeassistant.exposed_entities import (
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, start
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.event import async_call_later
@@ -104,7 +106,11 @@ def entity_supported(hass: HomeAssistant, entity_id: str) -> bool:
if domain in SUPPORTED_DOMAINS:
return True
device_class = get_device_class(hass, entity_id)
try:
device_class = get_device_class(hass, entity_id)
except HomeAssistantError:
# The entity no longer exists
return False
if (
domain == "binary_sensor"
and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES
@@ -193,35 +199,44 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
# Don't migrate if there's a YAML config
return
entity_registry = er.async_get(self.hass)
for entity_id, entry in entity_registry.entities.items():
if CLOUD_ALEXA in entry.options:
continue
options = {"should_expose": self._should_expose_legacy(entity_id)}
entity_registry.async_update_entity_options(entity_id, CLOUD_ALEXA, options)
for state in self.hass.states.async_all():
async_expose_entity(
self.hass,
CLOUD_ALEXA,
state.entity_id,
self._should_expose_legacy(state.entity_id),
)
for entity_id in self._prefs.alexa_entity_configs:
async_expose_entity(
self.hass,
CLOUD_ALEXA,
entity_id,
self._should_expose_legacy(entity_id),
)
async def async_initialize(self):
"""Initialize the Alexa config."""
await super().async_initialize()
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
if self._prefs.alexa_settings_version < 2:
self._migrate_alexa_entity_settings_v1()
await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION
async def on_hass_started(hass):
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
if self._prefs.alexa_settings_version < 2:
self._migrate_alexa_entity_settings_v1()
await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION
)
async_listen_entity_updates(
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
)
async def hass_started(hass):
async def on_hass_start(hass):
if self.enabled and ALEXA_DOMAIN not in self.hass.config.components:
await async_setup_component(self.hass, ALEXA_DOMAIN, {})
start.async_at_start(self.hass, hass_started)
start.async_at_start(self.hass, on_hass_start)
start.async_at_started(self.hass, on_hass_started)
self._prefs.async_listen_updates(self._async_prefs_updated)
async_listen_entity_updates(
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
)
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
self._handle_entity_registry_updated,
@@ -257,6 +272,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
and entity_supported(self.hass, entity_id)
)
@callback
def should_expose(self, entity_id):
"""If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter:
+51 -26
View File
@@ -11,7 +11,10 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
from homeassistant.components.google_assistant.helpers import AbstractConfig
from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
async_get_entity_settings,
async_listen_entity_updates,
async_set_assistant_option,
async_should_expose,
)
from homeassistant.components.sensor import SensorDeviceClass
@@ -23,6 +26,7 @@ from homeassistant.core import (
callback,
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er, start
from homeassistant.helpers.entity import get_device_class
from homeassistant.setup import async_setup_component
@@ -171,34 +175,59 @@ class CloudGoogleConfig(AbstractConfig):
# Don't migrate if there's a YAML config
return
entity_registry = er.async_get(self.hass)
for entity_id, entry in entity_registry.entities.items():
if CLOUD_GOOGLE in entry.options:
continue
options = {"should_expose": self._should_expose_legacy(entity_id)}
if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None):
options[PREF_DISABLE_2FA] = _2fa_disabled
entity_registry.async_update_entity_options(
entity_id, CLOUD_GOOGLE, options
for state in self.hass.states.async_all():
entity_id = state.entity_id
async_expose_entity(
self.hass,
CLOUD_GOOGLE,
entity_id,
self._should_expose_legacy(entity_id),
)
if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None):
async_set_assistant_option(
self.hass,
CLOUD_GOOGLE,
entity_id,
PREF_DISABLE_2FA,
_2fa_disabled,
)
for entity_id in self._prefs.google_entity_configs:
async_expose_entity(
self.hass,
CLOUD_GOOGLE,
entity_id,
self._should_expose_legacy(entity_id),
)
if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None):
async_set_assistant_option(
self.hass,
CLOUD_GOOGLE,
entity_id,
PREF_DISABLE_2FA,
_2fa_disabled,
)
async def async_initialize(self):
"""Perform async initialization of config."""
await super().async_initialize()
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
if self._prefs.google_settings_version < 2:
self._migrate_google_entity_settings_v1()
await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION
async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
if self._prefs.google_settings_version < 2:
self._migrate_google_entity_settings_v1()
await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION
)
async_listen_entity_updates(
self.hass, CLOUD_GOOGLE, self._async_exposed_entities_updated
)
async def hass_started(hass):
async def on_hass_start(hass: HomeAssistant) -> None:
if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components:
await async_setup_component(self.hass, GOOGLE_DOMAIN, {})
start.async_at_start(self.hass, hass_started)
start.async_at_start(self.hass, on_hass_start)
start.async_at_started(self.hass, on_hass_started)
# Remove any stored user agent id that is not ours
remove_agent_user_ids = []
@@ -210,9 +239,6 @@ class CloudGoogleConfig(AbstractConfig):
await self.async_disconnect_agent_user(agent_user_id)
self._prefs.async_listen_updates(self._async_prefs_updated)
async_listen_entity_updates(
self.hass, CLOUD_GOOGLE, self._async_exposed_entities_updated
)
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
self._handle_entity_registry_updated,
@@ -289,14 +315,13 @@ class CloudGoogleConfig(AbstractConfig):
def should_2fa(self, state):
"""If an entity should be checked for 2FA."""
entity_registry = er.async_get(self.hass)
registry_entry = entity_registry.async_get(state.entity_id)
if not registry_entry:
try:
settings = async_get_entity_settings(self.hass, state.entity_id)
except HomeAssistantError:
# Handle the entity has been removed
return False
assistant_options = registry_entry.options.get(CLOUD_GOOGLE, {})
assistant_options = settings.get(CLOUD_GOOGLE, {})
return not assistant_options.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
async def async_report_state(self, message, agent_user_id: str):
@@ -382,7 +407,7 @@ class CloudGoogleConfig(AbstractConfig):
self.async_schedule_google_sync_all()
@callback
def _handle_device_registry_updated(self, event: Event) -> None:
async def _handle_device_registry_updated(self, event: Event) -> None:
"""Handle when device registry updated."""
if (
not self.enabled
+18 -34
View File
@@ -1,6 +1,7 @@
"""The HTTP api to control the cloud integration."""
import asyncio
from collections.abc import Mapping
from contextlib import suppress
import dataclasses
from functools import wraps
from http import HTTPStatus
@@ -21,11 +22,12 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
)
from homeassistant.components.google_assistant import helpers as google_helpers
from homeassistant.components.homeassistant import exposed_entities
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.location import async_detect_location_info
@@ -566,15 +568,14 @@ async def google_assistant_get(
"""Get data for a single google assistant entity."""
cloud = hass.data[DOMAIN]
gconf = await cloud.client.get_google_config()
entity_registry = er.async_get(hass)
entity_id: str = msg["entity_id"]
state = hass.states.get(entity_id)
if not entity_registry.async_is_registered(entity_id) or not state:
if not state:
connection.send_error(
msg["id"],
websocket_api.const.ERR_NOT_FOUND,
f"{entity_id} unknown or not in the entity registry",
f"{entity_id} unknown",
)
return
@@ -587,10 +588,16 @@ async def google_assistant_get(
)
return
assistant_options: Mapping[str, Any] = {}
with suppress(HomeAssistantError, KeyError):
settings = exposed_entities.async_get_entity_settings(hass, entity_id)
assistant_options = settings[CLOUD_GOOGLE]
result = {
"entity_id": entity.entity_id,
"traits": [trait.name for trait in entity.traits()],
"might_2fa": entity.might_2fa_traits(),
PREF_DISABLE_2FA: assistant_options.get(PREF_DISABLE_2FA),
}
connection.send_result(msg["id"], result)
@@ -609,14 +616,11 @@ async def google_assistant_list(
"""List all google assistant entities."""
cloud = hass.data[DOMAIN]
gconf = await cloud.client.get_google_config()
entity_registry = er.async_get(hass)
entities = google_helpers.async_get_entities(hass, gconf)
result = []
for entity in entities:
if not entity_registry.async_is_registered(entity.entity_id):
continue
result.append(
{
"entity_id": entity.entity_id,
@@ -645,27 +649,19 @@ async def google_assistant_update(
msg: dict[str, Any],
) -> None:
"""Update google assistant entity config."""
entity_registry = er.async_get(hass)
entity_id: str = msg["entity_id"]
if not (registry_entry := entity_registry.async_get(entity_id)):
connection.send_error(
msg["id"],
websocket_api.const.ERR_NOT_ALLOWED,
f"can't configure {entity_id}",
)
return
assistant_options: Mapping[str, Any] = {}
with suppress(HomeAssistantError, KeyError):
settings = exposed_entities.async_get_entity_settings(hass, entity_id)
assistant_options = settings[CLOUD_GOOGLE]
disable_2fa = msg[PREF_DISABLE_2FA]
assistant_options: Mapping[str, Any]
if (
assistant_options := registry_entry.options.get(CLOUD_GOOGLE, {})
) and assistant_options.get(PREF_DISABLE_2FA) == disable_2fa:
if assistant_options.get(PREF_DISABLE_2FA) == disable_2fa:
return
assistant_options = assistant_options | {PREF_DISABLE_2FA: disable_2fa}
entity_registry.async_update_entity_options(
entity_id, CLOUD_GOOGLE, assistant_options
exposed_entities.async_set_assistant_option(
hass, CLOUD_GOOGLE, entity_id, PREF_DISABLE_2FA, disable_2fa
)
connection.send_result(msg["id"])
@@ -686,17 +682,8 @@ async def alexa_get(
msg: dict[str, Any],
) -> None:
"""Get data for a single alexa entity."""
entity_registry = er.async_get(hass)
entity_id: str = msg["entity_id"]
if not entity_registry.async_is_registered(entity_id):
connection.send_error(
msg["id"],
websocket_api.const.ERR_NOT_FOUND,
f"{entity_id} not in the entity registry",
)
return
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity_supported_by_alexa(
hass, entity_id
):
@@ -723,14 +710,11 @@ async def alexa_list(
"""List all alexa entities."""
cloud = hass.data[DOMAIN]
alexa_config = await cloud.client.get_alexa_config()
entity_registry = er.async_get(hass)
entities = alexa_entities.async_get_entities(hass, alexa_config)
result = []
for entity in entities:
if not entity_registry.async_is_registered(entity.entity_id):
continue
result.append(
{
"entity_id": entity.entity_id,
@@ -1,4 +1,5 @@
"""Const for conversation integration."""
DOMAIN = "conversation"
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
HOME_ASSISTANT_AGENT = "homeassistant"
@@ -21,19 +21,21 @@ from homeassistant.components.homeassistant.exposed_entities import (
async_listen_entity_updates,
async_should_expose,
)
from homeassistant.const import ATTR_DEVICE_CLASS
from homeassistant.const import MATCH_ALL
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity_registry as er,
intent,
start,
template,
translation,
)
from homeassistant.helpers.event import async_track_state_change
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent import AbstractConversationAgent, ConversationInput, ConversationResult
from .const import DOMAIN
from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN
_LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
@@ -81,16 +83,24 @@ def async_setup(hass: core.HomeAssistant) -> None:
async_should_expose(hass, DOMAIN, entity_id)
@core.callback
def async_handle_entity_registry_changed(event: core.Event) -> None:
"""Set expose flag on newly created entities."""
if event.data["action"] == "create":
async_should_expose(hass, DOMAIN, event.data["entity_id"])
def async_entity_state_listener(
changed_entity: str,
old_state: core.State | None,
new_state: core.State | None,
):
"""Set expose flag on new entities."""
if old_state is not None or new_state is None:
return
async_should_expose(hass, DOMAIN, changed_entity)
hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
async_handle_entity_registry_changed,
run_immediately=True,
)
@core.callback
def async_hass_started(hass: core.HomeAssistant) -> None:
"""Set expose flag on all entities."""
for state in hass.states.async_all():
async_should_expose(hass, DOMAIN, state.entity_id)
async_track_state_change(hass, MATCH_ALL, async_entity_state_listener)
start.async_at_started(hass, async_hass_started)
class DefaultAgent(AbstractConversationAgent):
@@ -130,6 +140,11 @@ class DefaultAgent(AbstractConversationAgent):
self._async_handle_entity_registry_changed,
run_immediately=True,
)
self.hass.bus.async_listen(
core.EVENT_STATE_CHANGED,
self._async_handle_state_changed,
run_immediately=True,
)
async_listen_entity_updates(
self.hass, DOMAIN, self._async_exposed_entities_updated
)
@@ -186,6 +201,7 @@ class DefaultAgent(AbstractConversationAgent):
user_input.text,
user_input.context,
language,
assistant=DOMAIN,
)
except intent.IntentHandleError:
_LOGGER.exception("Intent handling error")
@@ -475,12 +491,19 @@ class DefaultAgent(AbstractConversationAgent):
@core.callback
def _async_handle_entity_registry_changed(self, event: core.Event) -> None:
"""Clear names list cache when an entity registry entry has changed."""
if event.data["action"] == "update" and not any(
if event.data["action"] != "update" or not any(
field in event.data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
):
return
self._slot_lists = None
@core.callback
def _async_handle_state_changed(self, event: core.Event) -> None:
"""Clear names list cache when a state is added or removed from the state machine."""
if event.data.get("old_state") and event.data.get("new_state"):
return
self._slot_lists = None
@core.callback
def _async_exposed_entities_updated(self) -> None:
"""Handle updated preferences."""
@@ -493,30 +516,38 @@ class DefaultAgent(AbstractConversationAgent):
area_ids_with_entities: set[str] = set()
entity_registry = er.async_get(self.hass)
entities = [
entity
for entity in entity_registry.entities.values()
if async_should_expose(self.hass, DOMAIN, entity.entity_id)
states = [
state
for state in self.hass.states.async_all()
if async_should_expose(self.hass, DOMAIN, state.entity_id)
]
devices = dr.async_get(self.hass)
# Gather exposed entity names
entity_names = []
for entity in entities:
for state in states:
# Checked against "requires_context" and "excludes_context" in hassil
context = {"domain": entity.domain}
if entity.device_class:
context[ATTR_DEVICE_CLASS] = entity.device_class
context = {"domain": state.domain}
if state.attributes:
# Include some attributes
for attr in DEFAULT_EXPOSED_ATTRIBUTES:
if attr not in state.attributes:
continue
context[attr] = state.attributes[attr]
entity = entity_registry.async_get(state.entity_id)
if not entity:
# Default name
entity_names.append((state.name, state.name, context))
continue
if entity.aliases:
for alias in entity.aliases:
entity_names.append((alias, alias, context))
# Default name
name = entity.async_friendly_name(self.hass) or entity.entity_id.replace(
"_", " "
)
entity_names.append((name, name, context))
entity_names.append((state.name, state.name, context))
if entity.area_id:
# Expose area too
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230501.0"]
"requirements": ["home-assistant-frontend==20230503.2"]
}
@@ -3,7 +3,8 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
import dataclasses
from typing import Any
from itertools import chain
from typing import Any, TypedDict
import voluptuous as vol
@@ -77,16 +78,41 @@ class AssistantPreferences:
return {"expose_new": self.expose_new}
@dataclasses.dataclass(frozen=True)
class ExposedEntity:
"""An exposed entity without a unique_id."""
assistants: dict[str, dict[str, Any]]
def to_json(self) -> dict[str, Any]:
"""Return a JSON serializable representation for storage."""
return {
"assistants": self.assistants,
}
class SerializedExposedEntities(TypedDict):
"""Serialized exposed entities storage storage collection."""
assistants: dict[str, dict[str, Any]]
exposed_entities: dict[str, dict[str, Any]]
class ExposedEntities:
"""Control assistant settings."""
"""Control assistant settings.
Settings for entities without a unique_id are stored in the store.
Settings for entities with a unique_id are stored in the entity registry.
"""
_assistants: dict[str, AssistantPreferences]
entities: dict[str, ExposedEntity]
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
self._hass = hass
self._listeners: dict[str, list[Callable[[], None]]] = {}
self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
self._store: Store[SerializedExposedEntities] = Store(
hass, STORAGE_VERSION, STORAGE_KEY
)
@@ -95,7 +121,8 @@ class ExposedEntities:
websocket_api.async_register_command(self._hass, ws_expose_entity)
websocket_api.async_register_command(self._hass, ws_expose_new_entities_get)
websocket_api.async_register_command(self._hass, ws_expose_new_entities_set)
await self.async_load()
websocket_api.async_register_command(self._hass, ws_list_exposed_entities)
await self._async_load_data()
@callback
def async_listen_entity_updates(
@@ -105,30 +132,57 @@ class ExposedEntities:
self._listeners.setdefault(assistant, []).append(listener)
@callback
def async_expose_entity(
self, assistant: str, entity_id: str, should_expose: bool
def async_set_assistant_option(
self, assistant: str, entity_id: str, key: str, value: Any
) -> None:
"""Expose an entity to an assistant.
"""Set an option for an assistant.
Notify listeners if expose flag was changed.
"""
entity_registry = er.async_get(self._hass)
if not (registry_entry := entity_registry.async_get(entity_id)):
raise HomeAssistantError("Unknown entity")
return self._async_set_legacy_assistant_option(
assistant, entity_id, key, value
)
assistant_options: Mapping[str, Any]
if (
assistant_options := registry_entry.options.get(assistant, {})
) and assistant_options.get("should_expose") == should_expose:
) and assistant_options.get(key) == value:
return
assistant_options = assistant_options | {"should_expose": should_expose}
assistant_options = assistant_options | {key: value}
entity_registry.async_update_entity_options(
entity_id, assistant, assistant_options
)
for listener in self._listeners.get(assistant, []):
listener()
def _async_set_legacy_assistant_option(
self, assistant: str, entity_id: str, key: str, value: Any
) -> None:
"""Set an option for an assistant.
Notify listeners if expose flag was changed.
"""
if (
(exposed_entity := self.entities.get(entity_id))
and (assistant_options := exposed_entity.assistants.get(assistant, {}))
and assistant_options.get(key) == value
):
return
if exposed_entity:
new_exposed_entity = self._update_exposed_entity(
assistant, entity_id, key, value
)
else:
new_exposed_entity = self._new_exposed_entity(assistant, key, value)
self.entities[entity_id] = new_exposed_entity
self._async_schedule_save()
for listener in self._listeners.get(assistant, []):
listener()
@callback
def async_get_expose_new_entities(self, assistant: str) -> bool:
"""Check if new entities are exposed to an assistant."""
@@ -150,6 +204,11 @@ class ExposedEntities:
entity_registry = er.async_get(self._hass)
result: dict[str, Mapping[str, Any]] = {}
options: Mapping | None
for entity_id, exposed_entity in self.entities.items():
if options := exposed_entity.assistants.get(assistant):
result[entity_id] = options
for entity_id, entry in entity_registry.entities.items():
if options := entry.options.get(assistant):
result[entity_id] = options
@@ -162,11 +221,16 @@ class ExposedEntities:
entity_registry = er.async_get(self._hass)
result: dict[str, Mapping[str, Any]] = {}
if not (registry_entry := entity_registry.async_get(entity_id)):
assistant_settings: Mapping
if registry_entry := entity_registry.async_get(entity_id):
assistant_settings = registry_entry.options
elif exposed_entity := self.entities.get(entity_id):
assistant_settings = exposed_entity.assistants
else:
raise HomeAssistantError("Unknown entity")
for assistant in KNOWN_ASSISTANTS:
if options := registry_entry.options.get(assistant):
if options := assistant_settings.get(assistant):
result[assistant] = options
return result
@@ -181,9 +245,7 @@ class ExposedEntities:
entity_registry = er.async_get(self._hass)
if not (registry_entry := entity_registry.async_get(entity_id)):
# Entities which are not in the entity registry are not exposed
return False
return self._async_should_expose_legacy_entity(assistant, entity_id)
if assistant in registry_entry.options:
if "should_expose" in registry_entry.options[assistant]:
should_expose = registry_entry.options[assistant]["should_expose"]
@@ -202,11 +264,42 @@ class ExposedEntities:
return should_expose
def _async_should_expose_legacy_entity(
self, assistant: str, entity_id: str
) -> bool:
"""Return True if an entity should be exposed to an assistant."""
should_expose: bool
if (
exposed_entity := self.entities.get(entity_id)
) and assistant in exposed_entity.assistants:
if "should_expose" in exposed_entity.assistants[assistant]:
should_expose = exposed_entity.assistants[assistant]["should_expose"]
return should_expose
if self.async_get_expose_new_entities(assistant):
should_expose = self._is_default_exposed(entity_id, None)
else:
should_expose = False
if exposed_entity:
new_exposed_entity = self._update_exposed_entity(
assistant, entity_id, "should_expose", should_expose
)
else:
new_exposed_entity = self._new_exposed_entity(
assistant, "should_expose", should_expose
)
self.entities[entity_id] = new_exposed_entity
self._async_schedule_save()
return should_expose
def _is_default_exposed(
self, entity_id: str, registry_entry: er.RegistryEntry
self, entity_id: str, registry_entry: er.RegistryEntry | None
) -> bool:
"""Return True if an entity is exposed by default."""
if (
if registry_entry and (
registry_entry.entity_category is not None
or registry_entry.hidden_by is not None
):
@@ -216,7 +309,11 @@ class ExposedEntities:
if domain in DEFAULT_EXPOSED_DOMAINS:
return True
device_class = get_device_class(self._hass, entity_id)
try:
device_class = get_device_class(self._hass, entity_id)
except HomeAssistantError:
# The entity no longer exists
return False
if (
domain == "binary_sensor"
and device_class in DEFAULT_EXPOSED_BINARY_SENSOR_DEVICE_CLASSES
@@ -228,17 +325,43 @@ class ExposedEntities:
return False
async def async_load(self) -> None:
def _update_exposed_entity(
self, assistant: str, entity_id: str, key: str, value: Any
) -> ExposedEntity:
"""Update an exposed entity."""
entity = self.entities[entity_id]
assistants = dict(entity.assistants)
old_settings = assistants.get(assistant, {})
assistants[assistant] = old_settings | {key: value}
return ExposedEntity(assistants)
def _new_exposed_entity(
self, assistant: str, key: str, value: Any
) -> ExposedEntity:
"""Create a new exposed entity."""
return ExposedEntity(
assistants={assistant: {key: value}},
)
async def _async_load_data(self) -> SerializedExposedEntities | None:
"""Load from the store."""
data = await self._store.async_load()
assistants: dict[str, AssistantPreferences] = {}
exposed_entities: dict[str, ExposedEntity] = {}
if data:
for domain, preferences in data["assistants"].items():
assistants[domain] = AssistantPreferences(**preferences)
if data and "exposed_entities" in data:
for entity_id, preferences in data["exposed_entities"].items():
exposed_entities[entity_id] = ExposedEntity(**preferences)
self._assistants = assistants
self.entities = exposed_entities
return data
@callback
def _async_schedule_save(self) -> None:
@@ -246,17 +369,19 @@ class ExposedEntities:
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@callback
def _data_to_save(self) -> dict[str, dict[str, dict[str, Any]]]:
"""Return data to store in a file."""
data = {}
data["assistants"] = {
domain: preferences.to_json()
for domain, preferences in self._assistants.items()
def _data_to_save(self) -> SerializedExposedEntities:
"""Return JSON-compatible date for storing to file."""
return {
"assistants": {
domain: preferences.to_json()
for domain, preferences in self._assistants.items()
},
"exposed_entities": {
entity_id: entity.to_json()
for entity_id, entity in self.entities.items()
},
}
return data
@callback
@websocket_api.require_admin
@@ -272,7 +397,6 @@ def ws_expose_entity(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Expose an entity to an assistant."""
entity_registry = er.async_get(hass)
entity_ids: str = msg["entity_ids"]
if blocked := next(
@@ -288,28 +412,37 @@ def ws_expose_entity(
)
return
if unknown := next(
(
entity_id
for entity_id in entity_ids
if entity_id not in entity_registry.entities
),
None,
):
connection.send_error(
msg["id"], websocket_api.const.ERR_NOT_FOUND, f"can't expose '{unknown}'"
)
return
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
for entity_id in entity_ids:
for assistant in msg["assistants"]:
exposed_entities.async_expose_entity(
assistant, entity_id, msg["should_expose"]
)
async_expose_entity(hass, assistant, entity_id, msg["should_expose"])
connection.send_result(msg["id"])
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "homeassistant/expose_entity/list",
}
)
def ws_list_exposed_entities(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Expose an entity to an assistant."""
result: dict[str, Any] = {}
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
entity_registry = er.async_get(hass)
for entity_id in chain(exposed_entities.entities, entity_registry.entities):
result[entity_id] = {}
entity_settings = async_get_entity_settings(hass, entity_id)
for assistant, settings in entity_settings.items():
if "should_expose" not in settings:
continue
result[entity_id][assistant] = settings["should_expose"]
connection.send_result(msg["id"], {"exposed_entities": result})
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
@@ -380,8 +513,9 @@ def async_expose_entity(
should_expose: bool,
) -> None:
"""Get assistant expose settings for an entity."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity(assistant, entity_id, should_expose)
async_set_assistant_option(
hass, assistant, entity_id, "should_expose", should_expose
)
@callback
@@ -389,3 +523,15 @@ def async_should_expose(hass: HomeAssistant, assistant: str, entity_id: str) ->
"""Return True if an entity should be exposed to an assistant."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
return exposed_entities.async_should_expose(assistant, entity_id)
@callback
def async_set_assistant_option(
hass: HomeAssistant, assistant: str, entity_id: str, option: str, value: Any
) -> None:
"""Set an option for an assistant.
Notify listeners if expose flag was changed.
"""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_set_assistant_option(assistant, entity_id, option, value)
+3 -1
View File
@@ -140,16 +140,18 @@ class GetStateIntentHandler(intent.IntentHandler):
area=area,
domains=domains,
device_classes=device_classes,
assistant=intent_obj.assistant,
)
)
_LOGGER.debug(
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s",
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s, assistant=%s",
len(states),
name,
area,
domains,
device_classes,
intent_obj.assistant,
)
# Create response
+10 -3
View File
@@ -205,13 +205,20 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
methods, DEFAULT_ATTEMPTS, OVERALL_TIMEOUT
)
def get_number_of_zones(self) -> int:
"""Return the number of zones.
If the number of zones is not yet populated, return 0
"""
return len(self.device.color_zones) if self.device.color_zones else 0
@callback
def _async_build_color_zones_update_requests(self) -> list[Callable]:
"""Build a color zones update request."""
device = self.device
return [
partial(device.get_color_zones, start_index=zone)
for zone in range(0, len(device.color_zones), 8)
for zone in range(0, self.get_number_of_zones(), 8)
]
async def _async_update_data(self) -> None:
@@ -224,7 +231,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
):
await self._async_populate_device_info()
num_zones = len(device.color_zones) if device.color_zones is not None else 0
num_zones = self.get_number_of_zones()
features = lifx_features(self.device)
is_extended_multizone = features["extended_multizone"]
is_legacy_multizone = not is_extended_multizone and features["multizone"]
@@ -256,7 +263,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
if is_extended_multizone or is_legacy_multizone:
self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")]
if is_legacy_multizone and num_zones != len(device.color_zones):
if is_legacy_multizone and num_zones != self.get_number_of_zones():
# The number of zones has changed so we need
# to update the zones again. This happens rarely.
await self.async_get_color_zones()
+1 -1
View File
@@ -382,7 +382,7 @@ class LIFXMultiZone(LIFXColor):
"""Send a color change to the bulb."""
bulb = self.bulb
color_zones = bulb.color_zones
num_zones = len(color_zones)
num_zones = self.coordinator.get_number_of_zones()
# Zone brightness is not reported when powered off
if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None:
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aionotion"],
"requirements": ["aionotion==2023.04.2"]
"requirements": ["aionotion==2023.05.0"]
}
+127 -81
View File
@@ -12,7 +12,7 @@ from httpx import RequestError
import onvif
from onvif import ONVIFCamera
from onvif.exceptions import ONVIFError
from zeep.exceptions import Fault, XMLParseError
from zeep.exceptions import Fault, TransportError, XMLParseError, XMLSyntaxError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -100,6 +100,7 @@ class ONVIFDevice:
# Get all device info
await self.device.update_xaddrs()
LOGGER.debug("%s: xaddrs = %s", self.name, self.device.xaddrs)
# Get device capabilities
self.onvif_capabilities = await self.device.get_capabilities()
@@ -112,10 +113,20 @@ class ONVIFDevice:
# Fetch basic device info and capabilities
self.info = await self.async_get_device_info()
LOGGER.debug("Camera %s info = %s", self.name, self.info)
LOGGER.debug("%s: camera info = %s", self.name, self.info)
# Check profiles before capabilities since the camera may be slow to respond
# once the event manager is started in async_get_capabilities.
#
# We need to check capabilities before profiles, because we need the data
# from capabilities to determine profiles correctly.
#
# We no longer initialize events in capabilities to avoid the problem
# where cameras become slow to respond for a bit after starting events, and
# instead we start events last and than update capabilities.
#
LOGGER.debug("%s: fetching initial capabilities", self.name)
self.capabilities = await self.async_get_capabilities()
LOGGER.debug("%s: fetching profiles", self.name)
self.profiles = await self.async_get_profiles()
LOGGER.debug("Camera %s profiles = %s", self.name, self.profiles)
@@ -123,10 +134,8 @@ class ONVIFDevice:
if not self.profiles:
raise ONVIFError("No camera profiles found")
self.capabilities = await self.async_get_capabilities()
LOGGER.debug("Camera %s capabilities = %s", self.name, self.capabilities)
if self.capabilities.ptz:
LOGGER.debug("%s: creating PTZ service", self.name)
self.device.create_ptz_service()
# Determine max resolution from profiles
@@ -136,6 +145,12 @@ class ONVIFDevice:
if profile.video.encoding == "H264"
)
# Start events last since some cameras become slow to respond
# for a bit after starting events
LOGGER.debug("%s: starting events", self.name)
self.capabilities.events = await self.async_start_events()
LOGGER.debug("Camera %s capabilities = %s", self.name, self.capabilities)
async def async_stop(self, event=None):
"""Shut it all down."""
if self.events:
@@ -188,81 +203,104 @@ class ONVIFDevice:
"""Warns if device and system date not synced."""
LOGGER.debug("%s: Setting up the ONVIF device management service", self.name)
device_mgmt = self.device.create_devicemgmt_service()
system_date = dt_util.utcnow()
LOGGER.debug("%s: Retrieving current device date/time", self.name)
try:
system_date = dt_util.utcnow()
device_time = await device_mgmt.GetSystemDateAndTime()
if not device_time:
LOGGER.debug(
"""Couldn't get device '%s' date/time.
GetSystemDateAndTime() return null/empty""",
self.name,
)
return
LOGGER.debug("%s: Device time: %s", self.name, device_time)
tzone = dt_util.DEFAULT_TIME_ZONE
cdate = device_time.LocalDateTime
if device_time.UTCDateTime:
tzone = dt_util.UTC
cdate = device_time.UTCDateTime
elif device_time.TimeZone:
tzone = dt_util.get_time_zone(device_time.TimeZone.TZ) or tzone
if cdate is None:
LOGGER.warning(
"%s: Could not retrieve date/time on this camera", self.name
)
else:
cam_date = dt.datetime(
cdate.Date.Year,
cdate.Date.Month,
cdate.Date.Day,
cdate.Time.Hour,
cdate.Time.Minute,
cdate.Time.Second,
0,
tzone,
)
cam_date_utc = cam_date.astimezone(dt_util.UTC)
LOGGER.debug(
"%s: Device date/time: %s | System date/time: %s",
self.name,
cam_date_utc,
system_date,
)
dt_diff = cam_date - system_date
self._dt_diff_seconds = dt_diff.total_seconds()
# It could be off either direction, so we need to check the absolute value
if abs(self._dt_diff_seconds) > 5:
LOGGER.warning(
(
"The date/time on %s (UTC) is '%s', "
"which is different from the system '%s', "
"this could lead to authentication issues"
),
self.name,
cam_date_utc,
system_date,
)
if device_time.DateTimeType == "Manual":
# Set Date and Time ourselves if Date and Time is set manually in the camera.
await self.async_manually_set_date_and_time()
except RequestError as err:
LOGGER.warning(
"Couldn't get device '%s' date/time. Error: %s", self.name, err
)
return
if not device_time:
LOGGER.debug(
"""Couldn't get device '%s' date/time.
GetSystemDateAndTime() return null/empty""",
self.name,
)
return
LOGGER.debug("%s: Device time: %s", self.name, device_time)
tzone = dt_util.DEFAULT_TIME_ZONE
cdate = device_time.LocalDateTime
if device_time.UTCDateTime:
tzone = dt_util.UTC
cdate = device_time.UTCDateTime
elif device_time.TimeZone:
tzone = dt_util.get_time_zone(device_time.TimeZone.TZ) or tzone
if cdate is None:
LOGGER.warning("%s: Could not retrieve date/time on this camera", self.name)
return
cam_date = dt.datetime(
cdate.Date.Year,
cdate.Date.Month,
cdate.Date.Day,
cdate.Time.Hour,
cdate.Time.Minute,
cdate.Time.Second,
0,
tzone,
)
cam_date_utc = cam_date.astimezone(dt_util.UTC)
LOGGER.debug(
"%s: Device date/time: %s | System date/time: %s",
self.name,
cam_date_utc,
system_date,
)
dt_diff = cam_date - system_date
self._dt_diff_seconds = dt_diff.total_seconds()
# It could be off either direction, so we need to check the absolute value
if abs(self._dt_diff_seconds) < 5:
return
LOGGER.warning(
(
"The date/time on %s (UTC) is '%s', "
"which is different from the system '%s', "
"this could lead to authentication issues"
),
self.name,
cam_date_utc,
system_date,
)
if device_time.DateTimeType != "Manual":
return
# Set Date and Time ourselves if Date and Time is set manually in the camera.
try:
await self.async_manually_set_date_and_time()
except (RequestError, TransportError):
LOGGER.warning("%s: Could not sync date/time on this camera", self.name)
async def async_get_device_info(self) -> DeviceInfo:
"""Obtain information about this device."""
device_mgmt = self.device.create_devicemgmt_service()
device_info = await device_mgmt.GetDeviceInformation()
manufacturer = None
model = None
firmware_version = None
serial_number = None
try:
device_info = await device_mgmt.GetDeviceInformation()
except (XMLParseError, XMLSyntaxError, TransportError) as ex:
# Some cameras have invalid UTF-8 in their device information (TransportError)
# and others have completely invalid XML (XMLParseError, XMLSyntaxError)
LOGGER.warning("%s: Failed to fetch device information: %s", self.name, ex)
else:
manufacturer = device_info.Manufacturer
model = device_info.Model
firmware_version = device_info.FirmwareVersion
serial_number = device_info.SerialNumber
# Grab the last MAC address for backwards compatibility
mac = None
@@ -282,10 +320,10 @@ class ONVIFDevice:
)
return DeviceInfo(
device_info.Manufacturer,
device_info.Model,
device_info.FirmwareVersion,
device_info.SerialNumber,
manufacturer,
model,
firmware_version,
serial_number,
mac,
)
@@ -307,23 +345,31 @@ class ONVIFDevice:
self.device.create_imaging_service()
imaging = True
events = False
return Capabilities(snapshot=snapshot, ptz=ptz, imaging=imaging)
async def async_start_events(self):
"""Start the event handler."""
with suppress(*GET_CAPABILITIES_EXCEPTIONS, XMLParseError):
onvif_capabilities = self.onvif_capabilities or {}
pull_point_support = onvif_capabilities.get("Events", {}).get(
pull_point_support = (onvif_capabilities.get("Events") or {}).get(
"WSPullPointSupport"
)
LOGGER.debug("%s: WSPullPointSupport: %s", self.name, pull_point_support)
events = await self.events.async_start(
pull_point_support is not False, True
)
return await self.events.async_start(pull_point_support is not False, True)
return Capabilities(snapshot, events, ptz, imaging)
return False
async def async_get_profiles(self) -> list[Profile]:
"""Obtain media profiles for this device."""
media_service = self.device.create_media_service()
result = await media_service.GetProfiles()
LOGGER.debug("%s: xaddr for media_service: %s", self.name, media_service.xaddr)
try:
result = await media_service.GetProfiles()
except GET_CAPABILITIES_EXCEPTIONS:
LOGGER.debug(
"%s: Could not get profiles from ONVIF device", self.name, exc_info=True
)
raise
profiles: list[Profile] = []
if not isinstance(result, list):
+23 -5
View File
@@ -11,7 +11,7 @@ from httpx import RemoteProtocolError, RequestError, TransportError
from onvif import ONVIFCamera, ONVIFService
from onvif.client import NotificationManager
from onvif.exceptions import ONVIFError
from zeep.exceptions import Fault, XMLParseError
from zeep.exceptions import Fault, ValidationError, XMLParseError
from homeassistant.components import webhook
from homeassistant.config_entries import ConfigEntry
@@ -35,7 +35,7 @@ from .util import stringify_onvif_error
UNHANDLED_TOPICS: set[str] = {"tns1:MediaControl/VideoEncoderConfiguration"}
SUBSCRIPTION_ERRORS = (Fault, asyncio.TimeoutError, TransportError)
CREATE_ERRORS = (ONVIFError, Fault, RequestError, XMLParseError)
CREATE_ERRORS = (ONVIFError, Fault, RequestError, XMLParseError, ValidationError)
SET_SYNCHRONIZATION_POINT_ERRORS = (*SUBSCRIPTION_ERRORS, TypeError)
UNSUBSCRIBE_ERRORS = (XMLParseError, *SUBSCRIPTION_ERRORS)
RENEW_ERRORS = (ONVIFError, RequestError, XMLParseError, *SUBSCRIPTION_ERRORS)
@@ -657,16 +657,34 @@ class WebHookManager:
async def _async_create_webhook_subscription(self) -> None:
"""Create webhook subscription."""
LOGGER.debug("%s: Creating webhook subscription", self._name)
LOGGER.debug(
"%s: Creating webhook subscription with URL: %s",
self._name,
self._webhook_url,
)
self._notification_manager = self._device.create_notification_manager(
{
"InitialTerminationTime": SUBSCRIPTION_RELATIVE_TIME,
"ConsumerReference": {"Address": self._webhook_url},
}
)
self._webhook_subscription = await self._notification_manager.setup()
try:
self._webhook_subscription = await self._notification_manager.setup()
except ValidationError as err:
# This should only happen if there is a problem with the webhook URL
# that is causing it to not be well formed.
LOGGER.exception(
"%s: validation error while creating webhook subscription: %s",
self._name,
err,
)
raise
await self._notification_manager.start()
LOGGER.debug("%s: Webhook subscription created", self._name)
LOGGER.debug(
"%s: Webhook subscription created with URL: %s",
self._name,
self._webhook_url,
)
async def _async_start_webhook(self) -> bool:
"""Start webhook."""
+1 -1
View File
@@ -10,7 +10,7 @@ from sense_energy import (
)
DOMAIN = "sense"
DEFAULT_TIMEOUT = 10
DEFAULT_TIMEOUT = 30
ACTIVE_UPDATE_RATE = 60
DEFAULT_NAME = "Sense"
SENSE_DATA = "sense_data"
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/voip",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["voip-utils==0.0.6"]
"requirements": ["voip-utils==0.0.7"]
}
+35 -28
View File
@@ -52,7 +52,11 @@ def make_protocol(
or (pipeline.tts_engine is None)
):
# Play pre-recorded message instead of failing
return PreRecordMessageProtocol(hass, "problem.pcm")
return PreRecordMessageProtocol(
hass,
"problem.pcm",
opus_payload_type=call_info.opus_payload_type,
)
# Pipeline is properly configured
return PipelineRtpDatagramProtocol(
@@ -60,6 +64,7 @@ def make_protocol(
hass.config.language,
voip_device,
Context(user_id=devices.config_entry.data["user"]),
opus_payload_type=call_info.opus_payload_type,
)
@@ -79,7 +84,9 @@ class HassVoipDatagramProtocol(VoipDatagramProtocol):
hass, devices, call_info
),
invalid_protocol_factory=lambda call_info: PreRecordMessageProtocol(
hass, "not_configured.pcm"
hass,
"not_configured.pcm",
opus_payload_type=call_info.opus_payload_type,
),
)
self.hass = hass
@@ -109,6 +116,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
language: str,
voip_device: VoIPDevice,
context: Context,
opus_payload_type: int,
pipeline_timeout: float = 30.0,
audio_timeout: float = 2.0,
buffered_chunks_before_speech: int = 100,
@@ -119,7 +127,12 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
tts_extra_timeout: float = 1.0,
) -> None:
"""Set up pipeline RTP server."""
super().__init__(rate=RATE, width=WIDTH, channels=CHANNELS)
super().__init__(
rate=RATE,
width=WIDTH,
channels=CHANNELS,
opus_payload_type=opus_payload_type,
)
self.hass = hass
self.language = language
@@ -350,9 +363,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
async with async_timeout.timeout(tts_seconds + self.tts_extra_timeout):
# Assume TTS audio is 16Khz 16-bit mono
await self.hass.async_add_executor_job(
partial(self.send_audio, audio_bytes, **RTP_AUDIO_SETTINGS)
)
await self._async_send_audio(audio_bytes)
except asyncio.TimeoutError as err:
_LOGGER.warning("TTS timeout")
raise err
@@ -360,6 +371,12 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
# Signal pipeline to restart
self._tts_done.set()
async def _async_send_audio(self, audio_bytes: bytes, **kwargs):
"""Send audio in executor."""
await self.hass.async_add_executor_job(
partial(self.send_audio, audio_bytes, **RTP_AUDIO_SETTINGS, **kwargs)
)
async def _play_listening_tone(self) -> None:
"""Play a tone to indicate that Home Assistant is listening."""
if self._tone_bytes is None:
@@ -369,13 +386,9 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
"tone.pcm",
)
await self.hass.async_add_executor_job(
partial(
self.send_audio,
self._tone_bytes,
silence_before=self.tone_delay,
**RTP_AUDIO_SETTINGS,
)
await self._async_send_audio(
self._tone_bytes,
silence_before=self.tone_delay,
)
async def _play_processing_tone(self) -> None:
@@ -387,13 +400,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
"processing.pcm",
)
await self.hass.async_add_executor_job(
partial(
self.send_audio,
self._processing_bytes,
**RTP_AUDIO_SETTINGS,
)
)
await self._async_send_audio(self._processing_bytes)
async def _play_error_tone(self) -> None:
"""Play a tone to indicate a pipeline error occurred."""
@@ -404,13 +411,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
"error.pcm",
)
await self.hass.async_add_executor_job(
partial(
self.send_audio,
self._error_bytes,
**RTP_AUDIO_SETTINGS,
)
)
await self._async_send_audio(self._error_bytes)
def _load_pcm(self, file_name: str) -> bytes:
"""Load raw audio (16Khz, 16-bit mono)."""
@@ -424,11 +425,17 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
self,
hass: HomeAssistant,
file_name: str,
opus_payload_type: int,
message_delay: float = 1.0,
loop_delay: float = 2.0,
) -> None:
"""Set up RTP server."""
super().__init__(rate=RATE, width=WIDTH, channels=CHANNELS)
super().__init__(
rate=RATE,
width=WIDTH,
channels=CHANNELS,
opus_payload_type=opus_payload_type,
)
self.hass = hass
self.file_name = file_name
self.message_delay = message_delay
@@ -117,6 +117,7 @@ class WindowCoveringClient(ClientClusterHandler):
"""Window client cluster handler."""
@registries.BINDABLE_CLUSTERS.register(closures.WindowCovering.cluster_id)
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(closures.WindowCovering.cluster_id)
class WindowCovering(ClusterHandler):
"""Window cluster handler."""
+1 -1
View File
@@ -23,7 +23,7 @@
"bellows==0.35.2",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.98",
"zha-quirks==0.0.99",
"zigpy-deconz==0.21.0",
"zigpy==0.55.0",
"zigpy-xbee==0.18.0",
+36 -30
View File
@@ -51,7 +51,7 @@ async def async_setup_entry(
entities: list[ZWaveBaseEntity] = []
if info.platform_hint == "motorized_barrier":
entities.append(ZwaveMotorizedBarrier(config_entry, driver, info))
elif info.platform_hint == "window_shutter_tilt":
elif info.platform_hint and info.platform_hint.endswith("tilt"):
entities.append(ZWaveTiltCover(config_entry, driver, info))
else:
entities.append(ZWaveCover(config_entry, driver, info))
@@ -99,6 +99,12 @@ def zwave_tilt_to_percent(value: int) -> int:
class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave Cover device."""
_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)
def __init__(
self,
config_entry: ConfigEntry,
@@ -108,11 +114,20 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Initialize a ZWaveCover entity."""
super().__init__(config_entry, driver, info)
self._stop_cover_value = (
self.get_zwave_value(COVER_OPEN_PROPERTY)
or self.get_zwave_value(COVER_UP_PROPERTY)
or self.get_zwave_value(COVER_ON_PROPERTY)
)
if self._stop_cover_value:
self._attr_supported_features |= CoverEntityFeature.STOP
# Entity class attributes
self._attr_device_class = CoverDeviceClass.WINDOW
if self.info.platform_hint in ("window_shutter", "window_shutter_tilt"):
if self.info.platform_hint and self.info.platform_hint.startswith("shutter"):
self._attr_device_class = CoverDeviceClass.SHUTTER
if self.info.platform_hint == "window_blind":
if self.info.platform_hint and self.info.platform_hint.startswith("blind"):
self._attr_device_class = CoverDeviceClass.BLIND
@property
@@ -153,28 +168,13 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop cover."""
cover_property = (
self.get_zwave_value(COVER_OPEN_PROPERTY)
or self.get_zwave_value(COVER_UP_PROPERTY)
or self.get_zwave_value(COVER_ON_PROPERTY)
)
if cover_property:
# Stop the cover, will stop regardless of the actual direction of travel.
await self.info.node.async_set_value(cover_property, False)
assert self._stop_cover_value
# Stop the cover, will stop regardless of the actual direction of travel.
await self.info.node.async_set_value(self._stop_cover_value, False)
class ZWaveTiltCover(ZWaveCover):
"""Representation of a Z-Wave Cover device with tilt."""
_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.STOP
| CoverEntityFeature.SET_POSITION
| CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
"""Representation of a Z-Wave cover device with tilt."""
def __init__(
self,
@@ -184,8 +184,15 @@ class ZWaveTiltCover(ZWaveCover):
) -> None:
"""Initialize a ZWaveCover entity."""
super().__init__(config_entry, driver, info)
self.data_template = cast(
self._current_tilt_value = cast(
CoverTiltDataTemplate, self.info.platform_data_template
).current_tilt_value(self.info.platform_data)
self._attr_supported_features |= (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
@property
@@ -194,19 +201,18 @@ class ZWaveTiltCover(ZWaveCover):
None is unknown, 0 is closed, 100 is fully open.
"""
value = self.data_template.current_tilt_value(self.info.platform_data)
value = self._current_tilt_value
if value is None or value.value is None:
return None
return zwave_tilt_to_percent(int(value.value))
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
tilt_value = self.data_template.current_tilt_value(self.info.platform_data)
if tilt_value:
await self.info.node.async_set_value(
tilt_value,
percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]),
)
assert self._current_tilt_value
await self.info.node.async_set_value(
self._current_tilt_value,
percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]),
)
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
@@ -347,7 +347,7 @@ DISCOVERY_SCHEMAS = [
# Fibaro Shutter Fibaro FGR222
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_shutter_tilt",
hint="shutter_tilt",
manufacturer_id={0x010F},
product_id={0x1000, 0x1001},
product_type={0x0301, 0x0302},
@@ -371,7 +371,7 @@ DISCOVERY_SCHEMAS = [
# Qubino flush shutter
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_shutter",
hint="shutter",
manufacturer_id={0x0159},
product_id={0x0052, 0x0053},
product_type={0x0003},
@@ -380,7 +380,7 @@ DISCOVERY_SCHEMAS = [
# Graber/Bali/Spring Fashion Covers
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_blind",
hint="blind",
manufacturer_id={0x026E},
product_id={0x5A31},
product_type={0x4353},
@@ -389,7 +389,7 @@ DISCOVERY_SCHEMAS = [
# iBlinds v2 window blind motor
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_blind",
hint="blind",
manufacturer_id={0x0287},
product_id={0x000D},
product_type={0x0003},
@@ -398,7 +398,7 @@ DISCOVERY_SCHEMAS = [
# Merten 507801 Connect Roller Shutter
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_shutter",
hint="shutter",
manufacturer_id={0x007A},
product_id={0x0001},
product_type={0x8003},
@@ -414,7 +414,7 @@ DISCOVERY_SCHEMAS = [
# Disable endpoint 2, as it has no practical function. CC: Switch_Multilevel
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_shutter",
hint="shutter",
manufacturer_id={0x007A},
product_id={0x0001},
product_type={0x8003},
@@ -807,7 +807,7 @@ DISCOVERY_SCHEMAS = [
# window coverings
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_cover",
hint="cover",
device_class_generic={"Multilevel Switch"},
device_class_specific={
"Motor Control Class A",
+1 -1
View File
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "0b6"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
-20
View File
@@ -307,26 +307,6 @@ class RegistryEntry:
hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs)
def async_friendly_name(self, hass: HomeAssistant) -> str | None:
"""Return the friendly name.
If self.name is not None, this returns self.name
If has_entity_name is False, self.original_name
If has_entity_name is True, this returns device.name + self.original_name
"""
if not self.has_entity_name or self.name is not None:
return self.name or self.original_name
device_registry = dr.async_get(hass)
if not (device_id := self.device_id) or not (
device_entry := device_registry.async_get(device_id)
):
return self.original_name
if not (original_name := self.original_name):
return device_entry.name_by_user or device_entry.name
return f"{device_entry.name_by_user or device_entry.name} {original_name}"
class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
"""Store entity registry data."""
+23 -1
View File
@@ -11,6 +11,7 @@ from typing import Any, TypeVar
import voluptuous as vol
from homeassistant.components.homeassistant.exposed_entities import async_should_expose
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
@@ -65,6 +66,7 @@ async def async_handle(
text_input: str | None = None,
context: Context | None = None,
language: str | None = None,
assistant: str | None = None,
) -> IntentResponse:
"""Handle an intent."""
handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type)
@@ -79,7 +81,14 @@ async def async_handle(
language = hass.config.language
intent = Intent(
hass, platform, intent_type, slots or {}, text_input, context, language
hass,
platform=platform,
intent_type=intent_type,
slots=slots or {},
text_input=text_input,
context=context,
language=language,
assistant=assistant,
)
try:
@@ -208,6 +217,7 @@ def async_match_states(
entities: entity_registry.EntityRegistry | None = None,
areas: area_registry.AreaRegistry | None = None,
devices: device_registry.DeviceRegistry | None = None,
assistant: str | None = None,
) -> Iterable[State]:
"""Find states that match the constraints."""
if states is None:
@@ -258,6 +268,14 @@ def async_match_states(
states_and_entities = list(_filter_by_area(states_and_entities, area, devices))
if assistant is not None:
# Filter by exposure
states_and_entities = [
(state, entity)
for state, entity in states_and_entities
if async_should_expose(hass, assistant, state.entity_id)
]
if name is not None:
if devices is None:
devices = device_registry.async_get(hass)
@@ -387,6 +405,7 @@ class ServiceIntentHandler(IntentHandler):
area=area,
domains=domains,
device_classes=device_classes,
assistant=intent_obj.assistant,
)
)
@@ -496,6 +515,7 @@ class Intent:
"context",
"language",
"category",
"assistant",
]
def __init__(
@@ -508,6 +528,7 @@ class Intent:
context: Context,
language: str,
category: IntentCategory | None = None,
assistant: str | None = None,
) -> None:
"""Initialize an intent."""
self.hass = hass
@@ -518,6 +539,7 @@ class Intent:
self.context = context
self.language = language
self.category = category
self.assistant = assistant
@callback
def create_response(self) -> IntentResponse:
+3 -3
View File
@@ -14,7 +14,7 @@ bcrypt==4.0.1
bleak-retry-connector==3.0.2
bleak==0.20.2
bluetooth-adapters==0.15.3
bluetooth-auto-recovery==1.0.3
bluetooth-auto-recovery==1.1.2
bluetooth-data-tools==0.4.0
certifi>=2021.5.30
ciso8601==2.3.0
@@ -25,7 +25,7 @@ ha-av==10.0.0
hass-nabucasa==0.66.2
hassil==1.0.6
home-assistant-bluetooth==1.10.0
home-assistant-frontend==20230501.0
home-assistant-frontend==20230503.2
home-assistant-intents==2023.4.26
httpx==0.24.0
ifaddr==0.1.7
@@ -47,7 +47,7 @@ requests==2.28.2
scapy==2.5.0
sqlalchemy==2.0.12
typing-extensions>=4.5.0,<5.0
ulid-transform==0.7.0
ulid-transform==0.7.2
voluptuous-serialize==2.6.0
voluptuous==0.13.1
webrtcvad==2.0.10
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.5.0b6"
version = "2023.5.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@@ -50,7 +50,7 @@ dependencies = [
"pyyaml==6.0",
"requests==2.28.2",
"typing-extensions>=4.5.0,<5.0",
"ulid-transform==0.7.0",
"ulid-transform==0.7.2",
"voluptuous==0.13.1",
"voluptuous-serialize==2.6.0",
"yarl==1.9.2",
+1 -1
View File
@@ -24,7 +24,7 @@ python-slugify==4.0.1
pyyaml==6.0
requests==2.28.2
typing-extensions>=4.5.0,<5.0
ulid-transform==0.7.0
ulid-transform==0.7.2
voluptuous==0.13.1
voluptuous-serialize==2.6.0
yarl==1.9.2
+6 -6
View File
@@ -223,7 +223,7 @@ aionanoleaf==0.2.1
aionotify==0.2.0
# homeassistant.components.notion
aionotion==2023.04.2
aionotion==2023.05.0
# homeassistant.components.oncue
aiooncue==0.3.4
@@ -465,7 +465,7 @@ bluemaestro-ble==0.2.3
bluetooth-adapters==0.15.3
# homeassistant.components.bluetooth
bluetooth-auto-recovery==1.0.3
bluetooth-auto-recovery==1.1.2
# homeassistant.components.bluetooth
# homeassistant.components.esphome
@@ -911,7 +911,7 @@ hole==0.8.0
holidays==0.21.13
# homeassistant.components.frontend
home-assistant-frontend==20230501.0
home-assistant-frontend==20230503.2
# homeassistant.components.conversation
home-assistant-intents==2023.4.26
@@ -1533,7 +1533,7 @@ pyblackbird==0.6
pybotvac==0.0.23
# homeassistant.components.braviatv
pybravia==0.3.2
pybravia==0.3.3
# homeassistant.components.nissan_leaf
pycarwings2==2.14
@@ -2594,7 +2594,7 @@ venstarcolortouch==0.19
vilfo-api-client==0.3.2
# homeassistant.components.voip
voip-utils==0.0.6
voip-utils==0.0.7
# homeassistant.components.volkszaehler
volkszaehler==0.4.0
@@ -2718,7 +2718,7 @@ zeroconf==0.58.2
zeversolar==0.3.1
# homeassistant.components.zha
zha-quirks==0.0.98
zha-quirks==0.0.99
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
+6 -6
View File
@@ -204,7 +204,7 @@ aiomusiccast==0.14.8
aionanoleaf==0.2.1
# homeassistant.components.notion
aionotion==2023.04.2
aionotion==2023.05.0
# homeassistant.components.oncue
aiooncue==0.3.4
@@ -385,7 +385,7 @@ bluemaestro-ble==0.2.3
bluetooth-adapters==0.15.3
# homeassistant.components.bluetooth
bluetooth-auto-recovery==1.0.3
bluetooth-auto-recovery==1.1.2
# homeassistant.components.bluetooth
# homeassistant.components.esphome
@@ -700,7 +700,7 @@ hole==0.8.0
holidays==0.21.13
# homeassistant.components.frontend
home-assistant-frontend==20230501.0
home-assistant-frontend==20230503.2
# homeassistant.components.conversation
home-assistant-intents==2023.4.26
@@ -1130,7 +1130,7 @@ pyblackbird==0.6
pybotvac==0.0.23
# homeassistant.components.braviatv
pybravia==0.3.2
pybravia==0.3.3
# homeassistant.components.cloudflare
pycfdns==2.0.1
@@ -1870,7 +1870,7 @@ venstarcolortouch==0.19
vilfo-api-client==0.3.2
# homeassistant.components.voip
voip-utils==0.0.6
voip-utils==0.0.7
# homeassistant.components.volvooncall
volvooncall==0.10.2
@@ -1964,7 +1964,7 @@ zeroconf==0.58.2
zeversolar==0.3.1
# homeassistant.components.zha
zha-quirks==0.0.98
zha-quirks==0.0.99
# homeassistant.components.zha
zigpy-deconz==0.21.0
@@ -1,4 +1,6 @@
"""The tests for the Air Quality component."""
import pytest
from homeassistant.components.air_quality import ATTR_N2O, ATTR_OZONE, ATTR_PM_10
from homeassistant.const import (
ATTR_ATTRIBUTION,
@@ -9,6 +11,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
async def test_state(hass: HomeAssistant) -> None:
"""Test Air Quality state."""
config = {"air_quality": {"platform": "demo"}}
@@ -39,6 +39,7 @@ def events(hass: HomeAssistant) -> list[Event]:
@pytest.fixture
async def mock_camera(hass: HomeAssistant) -> None:
"""Initialize a demo camera platform."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
)
+1
View File
@@ -1539,6 +1539,7 @@ async def test_automation_restore_last_triggered_with_initial_state(
async def test_extraction_functions(hass: HomeAssistant) -> None:
"""Test extraction functions."""
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
assert await async_setup_component(
hass,
+11
View File
@@ -0,0 +1,11 @@
"""Test fixtures for calendar sensor platforms."""
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@@ -1,6 +1,8 @@
"""The tests for calendar recorder."""
from datetime import timedelta
import pytest
from homeassistant.components.recorder import Recorder
from homeassistant.components.recorder.history import get_significant_states
from homeassistant.const import ATTR_FRIENDLY_NAME
@@ -12,9 +14,15 @@ from tests.common import async_fire_time_changed
from tests.components.recorder.common import async_wait_recording_done
@pytest.fixture(autouse=True)
async def setup_homeassistant():
"""Override the fixture in calendar.conftest."""
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test sensor attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
await hass.async_block_till_done()
+7
View File
@@ -5,11 +5,18 @@ import pytest
from homeassistant.components import camera
from homeassistant.components.camera.const import StreamType
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .common import WEBRTC_ANSWER
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture(name="mock_camera")
async def mock_camera_fixture(hass):
"""Initialize a demo camera platform."""
+1
View File
@@ -370,6 +370,7 @@ async def test_websocket_update_orientation_prefs(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera
) -> None:
"""Test updating camera preferences."""
await async_setup_component(hass, "homeassistant", {})
client = await hass_ws_client(hass)
+8
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from datetime import timedelta
import pytest
from homeassistant.components import camera
from homeassistant.components.recorder import Recorder
from homeassistant.components.recorder.history import get_significant_states
@@ -20,9 +22,15 @@ from tests.common import async_fire_time_changed
from tests.components.recorder.common import async_wait_recording_done
@pytest.fixture(autouse=True)
async def setup_homeassistant():
"""Override the fixture in calendar.conftest."""
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test camera registered attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}}
)
@@ -1888,6 +1888,7 @@ async def test_failed_cast_other_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test warning when casting from internal_url fails."""
await async_setup_component(hass, "homeassistant", {})
with assert_setup_component(1, tts.DOMAIN):
assert await async_setup_component(
hass,
@@ -1911,6 +1912,7 @@ async def test_failed_cast_internal_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test warning when casting from internal_url fails."""
await async_setup_component(hass, "homeassistant", {})
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
@@ -1939,6 +1941,7 @@ async def test_failed_cast_external_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test warning when casting from external_url fails."""
await async_setup_component(hass, "homeassistant", {})
await async_process_ha_core_config(
hass,
{"external_url": "http://example.com:8123"},
@@ -1969,6 +1972,7 @@ async def test_failed_cast_tts_base_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test warning when casting from tts.base_url fails."""
await async_setup_component(hass, "homeassistant", {})
with assert_setup_component(1, tts.DOMAIN):
assert await async_setup_component(
hass,
@@ -29,6 +29,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test climate registered attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, climate.DOMAIN, {climate.DOMAIN: {"platform": "demo"}}
)
+73 -46
View File
@@ -15,10 +15,15 @@ from homeassistant.components.cloud.prefs import CloudPreferences
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
async_expose_entity,
async_get_entity_settings,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED,
EntityCategory,
)
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.setup import async_setup_component
@@ -41,8 +46,7 @@ def expose_new(hass, expose_new):
def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to Alexa."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity("cloud.alexa", entity_id, should_expose)
async_expose_entity(hass, "cloud.alexa", entity_id, should_expose)
async def test_alexa_config_expose_entity_prefs(
@@ -102,10 +106,9 @@ async def test_alexa_config_expose_entity_prefs(
)
await conf.async_initialize()
# can't expose an entity which is not in the entity registry
with pytest.raises(HomeAssistantError):
expose_entity(hass, "light.kitchen", True)
assert not conf.should_expose("light.kitchen")
# an entity which is not in the entity registry can be exposed
expose_entity(hass, "light.kitchen", True)
assert conf.should_expose("light.kitchen")
# categorized and hidden entities should not be exposed
assert not conf.should_expose(entity_entry1.entity_id)
assert not conf.should_expose(entity_entry2.entity_id)
@@ -368,6 +371,8 @@ async def test_alexa_update_expose_trigger_sync(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
with patch_sync_helper() as (to_update, to_remove):
expose_entity(hass, light_entry.entity_id, True)
@@ -544,8 +549,10 @@ async def test_alexa_config_migrate_expose_entity_prefs(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.state_only", "on")
entity_exposed = entity_registry.async_get_or_create(
"light",
"test",
@@ -593,6 +600,9 @@ async def test_alexa_config_migrate_expose_entity_prefs(
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.unknown"] = {
PREF_SHOULD_EXPOSE: True
}
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.state_only"] = {
PREF_SHOULD_EXPOSE: False
}
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_exposed.entity_id] = {
PREF_SHOULD_EXPOSE: True
}
@@ -603,21 +613,32 @@ async def test_alexa_config_migrate_expose_entity_prefs(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
entity_exposed = entity_registry.async_get(entity_exposed.entity_id)
assert entity_exposed.options == {"cloud.alexa": {"should_expose": True}}
entity_migrated = entity_registry.async_get(entity_migrated.entity_id)
assert entity_migrated.options == {"cloud.alexa": {"should_expose": False}}
entity_config = entity_registry.async_get(entity_config.entity_id)
assert entity_config.options == {"cloud.alexa": {"should_expose": False}}
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.alexa": {"should_expose": True}}
entity_blocked = entity_registry.async_get(entity_blocked.entity_id)
assert entity_blocked.options == {"cloud.alexa": {"should_expose": False}}
assert async_get_entity_settings(hass, "light.unknown") == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, "light.state_only") == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_exposed.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_migrated.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_config.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_default.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_blocked.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
async def test_alexa_config_migrate_expose_entity_prefs_default_none(
@@ -627,6 +648,7 @@ async def test_alexa_config_migrate_expose_entity_prefs_default_none(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
entity_default = entity_registry.async_get_or_create(
@@ -647,9 +669,14 @@ async def test_alexa_config_migrate_expose_entity_prefs_default_none(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.alexa": {"should_expose": True}}
assert async_get_entity_settings(hass, entity_default.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
async def test_alexa_config_migrate_expose_entity_prefs_default(
@@ -659,6 +686,7 @@ async def test_alexa_config_migrate_expose_entity_prefs_default(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
@@ -724,27 +752,26 @@ async def test_alexa_config_migrate_expose_entity_prefs_default(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
binary_sensor_supported = entity_registry.async_get(
binary_sensor_supported.entity_id
)
assert binary_sensor_supported.options == {"cloud.alexa": {"should_expose": True}}
binary_sensor_unsupported = entity_registry.async_get(
binary_sensor_unsupported.entity_id
)
assert binary_sensor_unsupported.options == {
assert async_get_entity_settings(hass, binary_sensor_supported.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, binary_sensor_unsupported.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, light.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, sensor_supported.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, sensor_unsupported.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, water_heater.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
light = entity_registry.async_get(light.entity_id)
assert light.options == {"cloud.alexa": {"should_expose": True}}
sensor_supported = entity_registry.async_get(sensor_supported.entity_id)
assert sensor_supported.options == {"cloud.alexa": {"should_expose": True}}
sensor_unsupported = entity_registry.async_get(sensor_unsupported.entity_id)
assert sensor_unsupported.options == {"cloud.alexa": {"should_expose": False}}
water_heater = entity_registry.async_get(water_heater.entity_id)
assert water_heater.options == {"cloud.alexa": {"should_expose": False}}
+2 -3
View File
@@ -16,6 +16,7 @@ from homeassistant.components.cloud.const import (
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
async_expose_entity,
)
from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant, State
@@ -267,9 +268,7 @@ async def test_google_config_expose_entity(
assert gconf.should_expose(state)
exposed_entities.async_expose_entity(
"cloud.google_assistant", entity_entry.entity_id, False
)
async_expose_entity(hass, "cloud.google_assistant", entity_entry.entity_id, False)
assert not gconf.should_expose(state)
+67 -53
View File
@@ -18,10 +18,15 @@ from homeassistant.components.google_assistant import helpers as ga_helpers
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
async_expose_entity,
async_get_entity_settings,
)
from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED,
EntityCategory,
)
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EntityCategory
from homeassistant.core import CoreState, HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -49,10 +54,7 @@ def expose_new(hass, expose_new):
def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to Google."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity(
"cloud.google_assistant", entity_id, should_expose
)
async_expose_entity(hass, "cloud.google_assistant", entity_id, should_expose)
async def test_google_update_report_state(
@@ -146,6 +148,8 @@ async def test_google_update_expose_trigger_sync(
Mock(claims={"cognito:username": "abcdefghjkl"}),
)
await config.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
await config.async_connect_agent_user("mock-user-id")
with patch.object(config, "async_sync_entities") as mock_sync, patch.object(
@@ -395,10 +399,9 @@ async def test_google_config_expose_entity_prefs(
state_not_exposed = State(entity_entry5.entity_id, "on")
state_exposed_default = State(entity_entry6.entity_id, "on")
# can't expose an entity which is not in the entity registry
with pytest.raises(HomeAssistantError):
expose_entity(hass, "light.kitchen", True)
assert not mock_conf.should_expose(state)
# an entity which is not in the entity registry can be exposed
expose_entity(hass, "light.kitchen", True)
assert mock_conf.should_expose(state)
# categorized and hidden entities should not be exposed
assert not mock_conf.should_expose(state_config)
assert not mock_conf.should_expose(state_diagnostic)
@@ -486,8 +489,10 @@ async def test_google_config_migrate_expose_entity_prefs(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Google entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.state_only", "on")
entity_exposed = entity_registry.async_get_or_create(
"light",
"test",
@@ -540,7 +545,11 @@ async def test_google_config_migrate_expose_entity_prefs(
expose_entity(hass, entity_migrated.entity_id, False)
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.unknown"] = {
PREF_SHOULD_EXPOSE: True
PREF_SHOULD_EXPOSE: True,
PREF_DISABLE_2FA: True,
}
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.state_only"] = {
PREF_SHOULD_EXPOSE: False
}
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_exposed.entity_id] = {
PREF_SHOULD_EXPOSE: True
@@ -556,28 +565,33 @@ async def test_google_config_migrate_expose_entity_prefs(
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
entity_exposed = entity_registry.async_get(entity_exposed.entity_id)
assert entity_exposed.options == {"cloud.google_assistant": {"should_expose": True}}
entity_migrated = entity_registry.async_get(entity_migrated.entity_id)
assert entity_migrated.options == {
"cloud.google_assistant": {"should_expose": False}
}
entity_no_2fa_exposed = entity_registry.async_get(entity_no_2fa_exposed.entity_id)
assert entity_no_2fa_exposed.options == {
assert async_get_entity_settings(hass, "light.unknown") == {
"cloud.google_assistant": {"disable_2fa": True, "should_expose": True}
}
entity_config = entity_registry.async_get(entity_config.entity_id)
assert entity_config.options == {"cloud.google_assistant": {"should_expose": False}}
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.google_assistant": {"should_expose": True}}
entity_blocked = entity_registry.async_get(entity_blocked.entity_id)
assert entity_blocked.options == {
assert async_get_entity_settings(hass, "light.state_only") == {
"cloud.google_assistant": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_exposed.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_migrated.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_no_2fa_exposed.entity_id) == {
"cloud.google_assistant": {"disable_2fa": True, "should_expose": True}
}
assert async_get_entity_settings(hass, entity_config.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_default.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_blocked.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
@@ -588,6 +602,7 @@ async def test_google_config_migrate_expose_entity_prefs_default_none(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Google entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
entity_default = entity_registry.async_get_or_create(
@@ -608,9 +623,14 @@ async def test_google_config_migrate_expose_entity_prefs_default_none(
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.google_assistant": {"should_expose": True}}
assert async_get_entity_settings(hass, entity_default.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
async def test_google_config_migrate_expose_entity_prefs_default(
@@ -619,6 +639,7 @@ async def test_google_config_migrate_expose_entity_prefs_default(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Google entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
@@ -684,33 +705,26 @@ async def test_google_config_migrate_expose_entity_prefs_default(
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
binary_sensor_supported = entity_registry.async_get(
binary_sensor_supported.entity_id
)
assert binary_sensor_supported.options == {
assert async_get_entity_settings(hass, binary_sensor_supported.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
binary_sensor_unsupported = entity_registry.async_get(
binary_sensor_unsupported.entity_id
)
assert binary_sensor_unsupported.options == {
assert async_get_entity_settings(hass, binary_sensor_unsupported.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
light = entity_registry.async_get(light.entity_id)
assert light.options == {"cloud.google_assistant": {"should_expose": True}}
sensor_supported = entity_registry.async_get(sensor_supported.entity_id)
assert sensor_supported.options == {
assert async_get_entity_settings(hass, light.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
sensor_unsupported = entity_registry.async_get(sensor_unsupported.entity_id)
assert sensor_unsupported.options == {
assert async_get_entity_settings(hass, sensor_supported.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, sensor_unsupported.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
assert async_get_entity_settings(hass, water_heater.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
water_heater = entity_registry.async_get(water_heater.entity_id)
assert water_heater.options == {"cloud.google_assistant": {"should_expose": False}}
+60 -17
View File
@@ -14,6 +14,7 @@ from homeassistant.components.alexa import errors as alexa_errors
from homeassistant.components.alexa.entities import LightCapabilities
from homeassistant.components.cloud.const import DOMAIN
from homeassistant.components.google_assistant.helpers import GoogleEntity
from homeassistant.components.homeassistant import exposed_entities
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
@@ -761,7 +762,17 @@ async def test_list_google_entities(
await client.send_json_auto_id({"type": "cloud/google_assistant/entities"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]) == 0
assert len(response["result"]) == 2
assert response["result"][0] == {
"entity_id": "light.kitchen",
"might_2fa": False,
"traits": ["action.devices.traits.OnOff"],
}
assert response["result"][1] == {
"entity_id": "cover.garage",
"might_2fa": True,
"traits": ["action.devices.traits.OpenClose"],
}
# Add the entities to the entity registry
entity_registry.async_get_or_create(
@@ -809,7 +820,7 @@ async def test_get_google_entity(
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "light.kitchen unknown or not in the entity registry",
"message": "light.kitchen unknown",
}
# Test getting a blocked entity
@@ -830,9 +841,6 @@ async def test_get_google_entity(
entity_registry.async_get_or_create(
"light", "test", "unique", suggested_object_id="kitchen"
)
entity_registry.async_get_or_create(
"cover", "test", "unique", suggested_object_id="garage"
)
hass.states.async_set("light.kitchen", "on")
hass.states.async_set("cover.garage", "open", {"device_class": "garage"})
@@ -842,6 +850,7 @@ async def test_get_google_entity(
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"disable_2fa": None,
"entity_id": "light.kitchen",
"might_2fa": False,
"traits": ["action.devices.traits.OnOff"],
@@ -853,6 +862,30 @@ async def test_get_google_entity(
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"disable_2fa": None,
"entity_id": "cover.garage",
"might_2fa": True,
"traits": ["action.devices.traits.OpenClose"],
}
# Set the disable 2fa flag
await client.send_json_auto_id(
{
"type": "cloud/google_assistant/entities/update",
"entity_id": "cover.garage",
"disable_2fa": True,
}
)
response = await client.receive_json()
assert response["success"]
await client.send_json_auto_id(
{"type": "cloud/google_assistant/entities/get", "entity_id": "cover.garage"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"disable_2fa": True,
"entity_id": "cover.garage",
"might_2fa": True,
"traits": ["action.devices.traits.OpenClose"],
@@ -867,9 +900,6 @@ async def test_update_google_entity(
mock_cloud_login,
) -> None:
"""Test that we can update config of a Google entity."""
entry = entity_registry.async_get_or_create(
"light", "test", "unique", suggested_object_id="kitchen"
)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
@@ -885,16 +915,16 @@ async def test_update_google_entity(
{
"type": "homeassistant/expose_entity",
"assistants": ["cloud.google_assistant"],
"entity_ids": [entry.entity_id],
"entity_ids": ["light.kitchen"],
"should_expose": False,
}
)
response = await client.receive_json()
assert response["success"]
assert entity_registry.async_get(entry.entity_id).options[
"cloud.google_assistant"
] == {"disable_2fa": False, "should_expose": False}
assert exposed_entities.async_get_entity_settings(hass, "light.kitchen") == {
"cloud.google_assistant": {"disable_2fa": False, "should_expose": False}
}
async def test_list_alexa_entities(
@@ -916,7 +946,12 @@ async def test_list_alexa_entities(
await client.send_json_auto_id({"id": 5, "type": "cloud/alexa/entities"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]) == 0
assert len(response["result"]) == 1
assert response["result"][0] == {
"entity_id": "light.kitchen",
"display_categories": ["LIGHT"],
"interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"],
}
# Add the entity to the entity registry
entity_registry.async_get_or_create(
@@ -953,10 +988,18 @@ async def test_get_alexa_entity(
{"type": "cloud/alexa/entities/get", "entity_id": "light.kitchen"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] is None
# Test getting an unknown sensor
await client.send_json_auto_id(
{"type": "cloud/alexa/entities/get", "entity_id": "sensor.temperature"}
)
response = await client.receive_json()
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "light.kitchen not in the entity registry",
"code": "not_supported",
"message": "sensor.temperature not supported by Alexa",
}
# Test getting a blocked entity
@@ -1022,8 +1065,8 @@ async def test_update_alexa_entity(
response = await client.receive_json()
assert response["success"]
assert entity_registry.async_get(entry.entity_id).options["cloud.alexa"] == {
"should_expose": False
assert exposed_entities.async_get_entity_settings(hass, entry.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
@@ -32,6 +32,12 @@ LIGHT_ENTITY = "light.kitchen_lights"
CLOSE_THRESHOLD = 10
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
def _close_enough(actual_rgb, testing_rgb):
"""Validate the given RGB value is in acceptable tolerance."""
# Convert the given RGB values to hue / saturation and then back again
+2 -2
View File
@@ -7,6 +7,7 @@ from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
async_expose_entity,
)
from homeassistant.helpers import intent
@@ -53,5 +54,4 @@ def expose_new(hass, expose_new):
def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to the default agent."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity(conversation.DOMAIN, entity_id, should_expose)
async_expose_entity(hass, conversation.DOMAIN, entity_id, should_expose)
@@ -91,21 +91,21 @@ async def test_exposed_areas(
)
device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id)
kitchen_light = entity_registry.async_get_or_create(
"light", "demo", "1234", original_name="kitchen light"
)
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
entity_registry.async_update_entity(
kitchen_light.entity_id, device_id=kitchen_device.id
)
hass.states.async_set(kitchen_light.entity_id, "on")
bedroom_light = entity_registry.async_get_or_create(
"light", "demo", "5678", original_name="bedroom light"
hass.states.async_set(
kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
)
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
entity_registry.async_update_entity(
bedroom_light.entity_id, area_id=area_bedroom.id
)
hass.states.async_set(bedroom_light.entity_id, "on")
hass.states.async_set(
bedroom_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"}
)
# Hide the bedroom light
expose_entity(hass, bedroom_light.entity_id, False)
@@ -156,6 +156,8 @@ async def test_expose_flag_automatically_set(
assert await async_setup_component(hass, "conversation", {})
await hass.async_block_till_done()
with patch("homeassistant.components.http.start_http_server_and_save_config"):
await hass.async_start()
# After setting up conversation, the expose flag should now be set on all entities
assert async_get_assistant_settings(hass, conversation.DOMAIN) == {
@@ -164,10 +166,60 @@ async def test_expose_flag_automatically_set(
}
# New entities will automatically have the expose flag set
new_light = entity_registry.async_get_or_create("light", "demo", "2345")
new_light = "light.demo_2345"
hass.states.async_set(new_light, "test")
await hass.async_block_till_done()
assert async_get_assistant_settings(hass, conversation.DOMAIN) == {
light.entity_id: {"should_expose": True},
new_light.entity_id: {"should_expose": True},
new_light: {"should_expose": True},
test.entity_id: {"should_expose": False},
}
async def test_unexposed_entities_skipped(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that unexposed entities are skipped in exposed areas."""
area_kitchen = area_registry.async_get_or_create("kitchen")
# Both lights are in the kitchen
exposed_light = entity_registry.async_get_or_create("light", "demo", "1234")
entity_registry.async_update_entity(
exposed_light.entity_id,
area_id=area_kitchen.id,
)
hass.states.async_set(exposed_light.entity_id, "off")
unexposed_light = entity_registry.async_get_or_create("light", "demo", "5678")
entity_registry.async_update_entity(
unexposed_light.entity_id,
area_id=area_kitchen.id,
)
hass.states.async_set(unexposed_light.entity_id, "off")
# On light is exposed, the other is not
expose_entity(hass, exposed_light.entity_id, True)
expose_entity(hass, unexposed_light.entity_id, False)
# Only one light should be turned on
calls = async_mock_service(hass, "light", "turn_on")
result = await conversation.async_converse(
hass, "turn on kitchen lights", None, Context(), None
)
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
# Only one light should be returned
hass.states.async_set(exposed_light.entity_id, "on")
hass.states.async_set(unexposed_light.entity_id, "on")
result = await conversation.async_converse(
hass, "how many lights are on in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == exposed_light.entity_id
+8 -73
View File
@@ -202,11 +202,7 @@ async def test_http_processing_intent_entity_added_removed(
# Add an entity
entity_registry.async_get_or_create(
"light",
"demo",
"5678",
suggested_object_id="late",
original_name="friendly light",
"light", "demo", "5678", suggested_object_id="late"
)
hass.states.async_set("light.late", "off", {"friendly_name": "friendly light"})
@@ -274,7 +270,7 @@ async def test_http_processing_intent_entity_added_removed(
}
# Now delete the entity
entity_registry.async_remove("light.late")
hass.states.async_remove("light.late")
client = await hass_client()
resp = await client.post(
@@ -313,11 +309,7 @@ async def test_http_processing_intent_alias_added_removed(
so that the new alias is available.
"""
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen light",
"light", "demo", "1234", suggested_object_id="kitchen"
)
hass.states.async_set("light.kitchen", "off", {"friendly_name": "kitchen light"})
@@ -438,7 +430,6 @@ async def test_http_processing_intent_entity_renamed(
LIGHT_DOMAIN,
{LIGHT_DOMAIN: [{"platform": "test"}]},
)
await hass.async_block_till_done()
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
@@ -882,20 +873,9 @@ async def test_http_processing_intent_conversion_not_expose_new(
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
@pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on"))
async def test_turn_on_intent(
hass: HomeAssistant,
init_components,
entity_registry: er.EntityRegistry,
sentence,
agent_id,
hass: HomeAssistant, init_components, sentence, agent_id
) -> None:
"""Test calling the turn on intent."""
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
@@ -913,17 +893,8 @@ async def test_turn_on_intent(
@pytest.mark.parametrize("sentence", ("turn off kitchen", "turn kitchen off"))
async def test_turn_off_intent(
hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry, sentence
) -> None:
async def test_turn_off_intent(hass: HomeAssistant, init_components, sentence) -> None:
"""Test calling the turn on intent."""
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "on")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_off")
@@ -969,21 +940,11 @@ async def test_http_api_no_match(
async def test_http_api_handle_failure(
hass: HomeAssistant,
init_components,
entity_registry: er.EntityRegistry,
hass_client: ClientSessionGenerator,
hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator
) -> None:
"""Test the HTTP conversation API with an error during handling."""
client = await hass_client()
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "off")
# Raise an error during intent handling
@@ -1020,19 +981,11 @@ async def test_http_api_handle_failure(
async def test_http_api_unexpected_failure(
hass: HomeAssistant,
init_components,
entity_registry: er.EntityRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test the HTTP conversation API with an unexpected error during handling."""
client = await hass_client()
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "off")
# Raise an "unexpected" error during intent handling
@@ -1351,17 +1304,8 @@ async def test_prepare_fail(hass: HomeAssistant) -> None:
assert not agent._lang_intents.get("not-a-language")
async def test_language_region(
hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry
) -> None:
async def test_language_region(hass: HomeAssistant, init_components) -> None:
"""Test calling the turn on intent."""
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
@@ -1409,17 +1353,8 @@ async def test_reload_on_new_component(hass: HomeAssistant) -> None:
assert {"light"} == (lang_intents.loaded_components - loaded_components)
async def test_non_default_response(
hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry
) -> None:
async def test_non_default_response(hass: HomeAssistant, init_components) -> None:
"""Test intent response that is not the default."""
entity_registry.async_get_or_create(
"cover",
"demo",
"1234",
suggested_object_id="front_door",
original_name="front door",
)
hass.states.async_set("cover.front_door", "closed")
calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
+11
View File
@@ -1,3 +1,14 @@
"""demo conftest."""
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@@ -218,6 +218,7 @@ async def test_discover_platform(
mock_demo_setup_scanner, mock_see, hass: HomeAssistant
) -> None:
"""Test discovery of device_tracker demo platform."""
await async_setup_component(hass, "homeassistant", {})
with patch("homeassistant.components.device_tracker.legacy.update_config"):
await discovery.async_load_platform(
hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}}
@@ -2,6 +2,8 @@
import math
from unittest.mock import AsyncMock, Mock, patch
import pytest
from homeassistant.components import emulated_kasa
from homeassistant.components.emulated_kasa.const import (
CONF_POWER,
@@ -132,6 +134,12 @@ CONFIG_SENSOR = {
}
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
def nested_value(ndict, *keys):
"""Return a nested dict value or None if it doesn't exist."""
if len(keys) == 0:
@@ -75,6 +75,12 @@ VALID_CONFIG = {
}
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def mock_healthybox():
"""Mock fb.check_box_health."""
+1
View File
@@ -19,6 +19,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test fan registered attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "demo"}})
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
@@ -20,6 +20,7 @@ async def test_diagnostics(
await setup.async_setup_component(
hass, switch.DOMAIN, {"switch": [{"platform": "demo"}]}
)
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass,
@@ -451,6 +451,7 @@ async def test_execute(
hass: HomeAssistant, report_state, on, brightness, value
) -> None:
"""Test an execute command."""
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, "light", {"light": {"platform": "demo"}})
await hass.async_block_till_done()
@@ -635,6 +636,7 @@ async def test_execute_times_out(
orig_execute_limit = sh.EXECUTE_LIMIT
sh.EXECUTE_LIMIT = 0.02 # Decrease timeout to 20ms
await async_setup_component(hass, "light", {"light": {"platform": "demo"}})
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
await hass.services.async_call(
@@ -209,6 +209,7 @@ async def test_send_text_command_expired_token_refresh_failure(
requires_reauth: ConfigEntryState,
) -> None:
"""Test failure refreshing token in send_text_command."""
await async_setup_component(hass, "homeassistant", {})
await setup_integration()
entries = hass.config_entries.async_entries(DOMAIN)
+11
View File
@@ -1,2 +1,13 @@
"""group conftest."""
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
+7
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from datetime import timedelta
import pytest
from homeassistant.components import group
from homeassistant.components.group import ATTR_AUTO, ATTR_ENTITY_ID, ATTR_ORDER
from homeassistant.components.recorder import Recorder
@@ -16,6 +18,11 @@ from tests.common import async_fire_time_changed
from tests.components.recorder.common import async_wait_recording_done
@pytest.fixture(autouse=True)
async def setup_homeassistant():
"""Override the fixture in group.conftest."""
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test number registered attributes to be excluded."""
now = dt_util.utcnow()
@@ -0,0 +1,25 @@
# serializer version: 1
# name: test_get_assistant_settings
dict({
'climate.test_unique1': mappingproxy({
'should_expose': True,
}),
'light.not_in_registry': dict({
'should_expose': True,
}),
})
# ---
# name: test_get_assistant_settings.1
dict({
})
# ---
# name: test_listeners
dict({
'light.kitchen': dict({
'should_expose': True,
}),
'switch.test_unique1': mappingproxy({
'should_expose': True,
}),
})
# ---
@@ -1,10 +1,14 @@
"""Test Home Assistant exposed entities helper."""
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
ExposedEntity,
async_expose_entity,
async_get_assistant_settings,
async_get_entity_settings,
async_listen_entity_updates,
async_should_expose,
)
@@ -18,6 +22,76 @@ from tests.common import flush_store
from tests.typing import WebSocketGenerator
@pytest.fixture(name="entities")
def entities_fixture(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
request: pytest.FixtureRequest,
) -> dict[str, str]:
"""Set up the test environment."""
if request.param == "entities_unique_id":
return entities_unique_id(entity_registry)
elif request.param == "entities_no_unique_id":
return entities_no_unique_id(hass)
else:
raise RuntimeError("Invalid setup fixture")
def entities_unique_id(entity_registry: er.EntityRegistry) -> dict[str, str]:
"""Create some entities in the entity registry."""
entry_blocked = entity_registry.async_get_or_create(
"group", "test", "unique", suggested_object_id="all_locks"
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
entry_lock = entity_registry.async_get_or_create("lock", "test", "unique1")
entry_binary_sensor = entity_registry.async_get_or_create(
"binary_sensor", "test", "unique1"
)
entry_binary_sensor_door = entity_registry.async_get_or_create(
"binary_sensor",
"test",
"unique2",
original_device_class="door",
)
entry_sensor = entity_registry.async_get_or_create("sensor", "test", "unique1")
entry_sensor_temperature = entity_registry.async_get_or_create(
"sensor",
"test",
"unique2",
original_device_class="temperature",
)
return {
"blocked": entry_blocked.entity_id,
"lock": entry_lock.entity_id,
"binary_sensor": entry_binary_sensor.entity_id,
"door_sensor": entry_binary_sensor_door.entity_id,
"sensor": entry_sensor.entity_id,
"temperature_sensor": entry_sensor_temperature.entity_id,
}
def entities_no_unique_id(hass: HomeAssistant) -> dict[str, str]:
"""Create some entities not in the entity registry."""
blocked = CLOUD_NEVER_EXPOSED_ENTITIES[0]
lock = "lock.test"
binary_sensor = "binary_sensor.test"
door_sensor = "binary_sensor.door"
sensor = "sensor.test"
sensor_temperature = "sensor.temperature"
hass.states.async_set(binary_sensor, "on", {})
hass.states.async_set(door_sensor, "on", {"device_class": "door"})
hass.states.async_set(sensor, "on", {})
hass.states.async_set(sensor_temperature, "on", {"device_class": "temperature"})
return {
"blocked": blocked,
"lock": lock,
"binary_sensor": binary_sensor,
"door_sensor": door_sensor,
"sensor": sensor,
"temperature_sensor": sensor_temperature,
}
async def test_load_preferences(hass: HomeAssistant) -> None:
"""Make sure that we can load/save data correctly."""
assert await async_setup_component(hass, "homeassistant", {})
@@ -28,13 +102,21 @@ async def test_load_preferences(hass: HomeAssistant) -> None:
exposed_entities.async_set_expose_new_entities("test1", True)
exposed_entities.async_set_expose_new_entities("test2", False)
async_expose_entity(hass, "test1", "light.kitchen", True)
async_expose_entity(hass, "test1", "light.living_room", True)
async_expose_entity(hass, "test2", "light.kitchen", True)
async_expose_entity(hass, "test2", "light.kitchen", True)
assert list(exposed_entities._assistants) == ["test1", "test2"]
assert list(exposed_entities.entities) == ["light.kitchen", "light.living_room"]
await flush_store(exposed_entities._store)
exposed_entities2 = ExposedEntities(hass)
await flush_store(exposed_entities._store)
await exposed_entities2.async_load()
await exposed_entities2.async_initialize()
assert exposed_entities._assistants == exposed_entities2._assistants
assert exposed_entities.entities == exposed_entities2.entities
async def test_expose_entity(
@@ -50,6 +132,9 @@ async def test_expose_entity(
entry1 = entity_registry.async_get_or_create("test", "test", "unique1")
entry2 = entity_registry.async_get_or_create("test", "test", "unique2")
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
assert len(exposed_entities.entities) == 0
# Set options
await ws_client.send_json_auto_id(
{
@@ -67,6 +152,7 @@ async def test_expose_entity(
assert entry1.options == {"cloud.alexa": {"should_expose": True}}
entry2 = entity_registry.async_get(entry2.entity_id)
assert entry2.options == {}
assert len(exposed_entities.entities) == 0
# Update options
await ws_client.send_json_auto_id(
@@ -91,6 +177,7 @@ async def test_expose_entity(
"cloud.alexa": {"should_expose": False},
"cloud.google_assistant": {"should_expose": False},
}
assert len(exposed_entities.entities) == 0
async def test_expose_entity_unknown(
@@ -103,6 +190,7 @@ async def test_expose_entity_unknown(
await hass.async_block_till_done()
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
assert len(exposed_entities.entities) == 0
# Set options
await ws_client.send_json_auto_id(
@@ -115,14 +203,41 @@ async def test_expose_entity_unknown(
)
response = await ws_client.receive_json()
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "can't expose 'test.test'",
assert response["success"]
assert len(exposed_entities.entities) == 1
assert exposed_entities.entities == {
"test.test": ExposedEntity({"cloud.alexa": {"should_expose": True}})
}
with pytest.raises(HomeAssistantError):
exposed_entities.async_expose_entity("cloud.alexa", "test.test", True)
# Update options
await ws_client.send_json_auto_id(
{
"type": "homeassistant/expose_entity",
"assistants": ["cloud.alexa", "cloud.google_assistant"],
"entity_ids": ["test.test", "test.test2"],
"should_expose": False,
}
)
response = await ws_client.receive_json()
assert response["success"]
assert len(exposed_entities.entities) == 2
assert exposed_entities.entities == {
"test.test": ExposedEntity(
{
"cloud.alexa": {"should_expose": False},
"cloud.google_assistant": {"should_expose": False},
}
),
"test.test2": ExposedEntity(
{
"cloud.alexa": {"should_expose": False},
"cloud.google_assistant": {"should_expose": False},
}
),
}
async def test_expose_entity_blocked(
@@ -220,55 +335,57 @@ async def test_listen_updates(
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
async_listen_entity_updates(hass, "cloud.alexa", listener)
entry = entity_registry.async_get_or_create("climate", "test", "unique1")
# Call for another assistant - listener not called
exposed_entities.async_expose_entity(
"cloud.google_assistant", entry.entity_id, True
)
async_expose_entity(hass, "cloud.google_assistant", entry.entity_id, True)
assert len(calls) == 0
# Call for our assistant - listener called
exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
async_expose_entity(hass, "cloud.alexa", entry.entity_id, True)
assert len(calls) == 1
# Settings not changed - listener not called
exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
async_expose_entity(hass, "cloud.alexa", entry.entity_id, True)
assert len(calls) == 1
# Settings changed - listener called
exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, False)
async_expose_entity(hass, "cloud.alexa", entry.entity_id, False)
assert len(calls) == 2
async def test_get_assistant_settings(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test get assistant settings."""
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
entry = entity_registry.async_get_or_create("climate", "test", "unique1")
assert async_get_assistant_settings(hass, "cloud.alexa") == {}
exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
assert async_get_assistant_settings(hass, "cloud.alexa") == {
"climate.test_unique1": {"should_expose": True}
}
assert async_get_assistant_settings(hass, "cloud.google_assistant") == {}
async_expose_entity(hass, "cloud.alexa", entry.entity_id, True)
async_expose_entity(hass, "cloud.alexa", "light.not_in_registry", True)
assert async_get_assistant_settings(hass, "cloud.alexa") == snapshot
assert async_get_assistant_settings(hass, "cloud.google_assistant") == snapshot
with pytest.raises(HomeAssistantError):
async_get_entity_settings(hass, "light.unknown")
@pytest.mark.parametrize(
"entities", ["entities_unique_id", "entities_no_unique_id"], indirect=True
)
async def test_should_expose(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
hass_ws_client: WebSocketGenerator,
entities: dict[str, str],
) -> None:
"""Test expose entity."""
ws_client = await hass_ws_client(hass)
@@ -290,59 +407,141 @@ async def test_should_expose(
assert async_should_expose(hass, "test.test", "test.test") is False
# Blocked entity is not exposed
entry_blocked = entity_registry.async_get_or_create(
"group", "test", "unique", suggested_object_id="all_locks"
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", entry_blocked.entity_id) is False
assert async_should_expose(hass, "cloud.alexa", entities["blocked"]) is False
# Lock is exposed
lock1 = entity_registry.async_get_or_create("lock", "test", "unique1")
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", lock1.entity_id) is True
# Hidden entity is not exposed
lock2 = entity_registry.async_get_or_create(
"lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", lock2.entity_id) is False
# Entity with category is not exposed
lock3 = entity_registry.async_get_or_create(
"lock", "test", "unique3", entity_category=EntityCategory.CONFIG
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", lock3.entity_id) is False
assert async_should_expose(hass, "cloud.alexa", entities["lock"]) is True
# Binary sensor without device class is not exposed
binarysensor1 = entity_registry.async_get_or_create(
"binary_sensor", "test", "unique1"
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", binarysensor1.entity_id) is False
assert async_should_expose(hass, "cloud.alexa", entities["binary_sensor"]) is False
# Binary sensor with certain device class is exposed
binarysensor2 = entity_registry.async_get_or_create(
"binary_sensor",
"test",
"unique2",
original_device_class="door",
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", binarysensor2.entity_id) is True
assert async_should_expose(hass, "cloud.alexa", entities["door_sensor"]) is True
# Sensor without device class is not exposed
sensor1 = entity_registry.async_get_or_create("sensor", "test", "unique1")
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", sensor1.entity_id) is False
assert async_should_expose(hass, "cloud.alexa", entities["sensor"]) is False
# Sensor with certain device class is exposed
sensor2 = entity_registry.async_get_or_create(
"sensor",
"test",
"unique2",
original_device_class="temperature",
assert (
async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", sensor2.entity_id) is True
# The second time we check, it should load it from storage
assert (
async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True
)
# Check with a different assistant
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_set_expose_new_entities("cloud.no_default_expose", False)
assert (
async_should_expose(
hass, "cloud.no_default_expose", entities["temperature_sensor"]
)
is False
)
async def test_should_expose_hidden_categorized(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test expose entity."""
ws_client = await hass_ws_client(hass)
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
# Expose new entities to Alexa
await ws_client.send_json_auto_id(
{
"type": "homeassistant/expose_new_entities/set",
"assistant": "cloud.alexa",
"expose_new": True,
}
)
response = await ws_client.receive_json()
assert response["success"]
entity_registry.async_get_or_create(
"lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER
)
assert async_should_expose(hass, "cloud.alexa", "lock.test_unique2") is False
# Entity with category is not exposed
entity_registry.async_get_or_create(
"lock", "test", "unique3", entity_category=EntityCategory.CONFIG
)
assert async_should_expose(hass, "cloud.alexa", "lock.test_unique3") is False
async def test_list_exposed_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test list exposed entities."""
ws_client = await hass_ws_client(hass)
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
entry1 = entity_registry.async_get_or_create("test", "test", "unique1")
entry2 = entity_registry.async_get_or_create("test", "test", "unique2")
# Set options for registered entities
await ws_client.send_json_auto_id(
{
"type": "homeassistant/expose_entity",
"assistants": ["cloud.alexa", "cloud.google_assistant"],
"entity_ids": [entry1.entity_id, entry2.entity_id],
"should_expose": True,
}
)
response = await ws_client.receive_json()
assert response["success"]
# Set options for entities not in the entity registry
await ws_client.send_json_auto_id(
{
"type": "homeassistant/expose_entity",
"assistants": ["cloud.alexa", "cloud.google_assistant"],
"entity_ids": [
"test.test",
"test.test2",
],
"should_expose": False,
}
)
response = await ws_client.receive_json()
assert response["success"]
# List exposed entities
await ws_client.send_json_auto_id({"type": "homeassistant/expose_entity/list"})
response = await ws_client.receive_json()
assert response["success"]
assert response["result"] == {
"exposed_entities": {
"test.test": {"cloud.alexa": False, "cloud.google_assistant": False},
"test.test2": {"cloud.alexa": False, "cloud.google_assistant": False},
"test.test_unique1": {"cloud.alexa": True, "cloud.google_assistant": True},
"test.test_unique2": {"cloud.alexa": True, "cloud.google_assistant": True},
},
}
async def test_listeners(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Make sure we call entity listeners."""
assert await async_setup_component(hass, "homeassistant", {})
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
callbacks = []
exposed_entities.async_listen_entity_updates("test1", lambda: callbacks.append(1))
async_expose_entity(hass, "test1", "light.kitchen", True)
assert len(callbacks) == 1
entry1 = entity_registry.async_get_or_create("switch", "test", "unique1")
async_expose_entity(hass, "test1", entry1.entity_id, True)
@@ -427,6 +427,7 @@ async def test_options_flow_devices(
demo_config_entry = MockConfigEntry(domain="domain")
demo_config_entry.add_to_hass(hass)
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
assert await async_setup_component(hass, "homekit", {"homekit": {}})
@@ -319,6 +319,7 @@ async def test_config_entry_with_trigger_accessory(
entity_registry: er.EntityRegistry,
) -> None:
"""Test generating diagnostics for a bridge config entry with a trigger accessory."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
hk_driver.publish = MagicMock()
+1
View File
@@ -747,6 +747,7 @@ async def test_homekit_start_with_a_device(
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
await hass.async_block_till_done()
+1
View File
@@ -81,6 +81,7 @@ async def test_bridge_with_triggers(
an above or below additional configuration which we have no way
to input, we ignore them.
"""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
await hass.async_block_till_done()
@@ -43,6 +43,12 @@ MOCK_START_STREAM_SESSION_UUID = UUID("3303d503-17cc-469a-b672-92436a71a2f6")
PID_THAT_WILL_NEVER_BE_ALIVE = 2147483647
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
async def _async_start_streaming(hass, acc):
"""Start streaming a camera."""
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
@@ -25,6 +25,7 @@ async def test_programmable_switch_button_fires_on_trigger(
demo_config_entry = MockConfigEntry(domain="domain")
demo_config_entry.add_to_hass(hass)
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
await hass.async_block_till_done()
hass.states.async_set("light.ceiling_lights", STATE_OFF)
@@ -16,6 +16,12 @@ from tests.common import assert_setup_component, async_capture_events
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def aiohttp_unused_port(event_loop, aiohttp_unused_port, socket_enabled):
"""Return aiohttp_unused_port and allow opening sockets."""
+44
View File
@@ -36,6 +36,7 @@ from homeassistant.components.light import (
ATTR_TRANSITION,
ATTR_XY_COLOR,
DOMAIN as LIGHT_DOMAIN,
SERVICE_TURN_ON,
ColorMode,
)
from homeassistant.const import (
@@ -1741,3 +1742,46 @@ async def test_set_hev_cycle_state_fails_for_color_bulb(hass: HomeAssistant) ->
{ATTR_ENTITY_ID: entity_id, ATTR_POWER: True},
blocking=True,
)
async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None:
"""Test a light strip were zones are not populated initially."""
already_migrated_config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
)
already_migrated_config_entry.add_to_hass(hass)
bulb = _mocked_light_strip()
bulb.power_level = 65535
bulb.color_zones = None
bulb.color = [65535, 65535, 65535, 65535]
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
device=bulb
), _patch_device(device=bulb):
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
await hass.async_block_till_done()
entity_id = "light.my_bulb"
state = hass.states.get(entity_id)
assert state.state == "on"
attributes = state.attributes
assert attributes[ATTR_BRIGHTNESS] == 255
assert attributes[ATTR_COLOR_MODE] == ColorMode.HS
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
assert attributes[ATTR_HS_COLOR] == (360.0, 100.0)
assert attributes[ATTR_RGB_COLOR] == (255, 0, 0)
assert attributes[ATTR_XY_COLOR] == (0.701, 0.299)
await hass.services.async_call(
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert bulb.set_power.calls[0][0][0] is True
bulb.set_power.reset_mock()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
+1
View File
@@ -26,6 +26,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test light registered attributes to be excluded."""
now = dt_util.utcnow()
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, light.DOMAIN, {light.DOMAIN: {"platform": "demo"}}
)
@@ -21,6 +21,12 @@ from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import ClientSessionGenerator, WebSocketGenerator
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
async def test_get_image_http(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
) -> None:
@@ -25,6 +25,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test media_player registered attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, media_player.DOMAIN, {media_player.DOMAIN: {"platform": "demo"}}
)
@@ -25,6 +25,12 @@ from tests.common import assert_setup_component, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
def create_group(hass, name):
"""Create a new person group.
@@ -26,6 +26,12 @@ CONFIG = {
ENDPOINT_URL = f"https://westus.{mf.FACE_API_URL}"
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def store_mock():
"""Mock update store."""
@@ -14,6 +14,12 @@ from tests.components.image_processing import common
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def store_mock():
"""Mock update store."""
+1
View File
@@ -18,6 +18,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test number registered attributes to be excluded."""
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, number.DOMAIN, {number.DOMAIN: {"platform": "demo"}}
)
@@ -14,6 +14,12 @@ from tests.components.image_processing import common
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
async def setup_openalpr_cloud(hass):
"""Set up openalpr cloud."""
@@ -14,6 +14,7 @@ from homeassistant.components.rtsp_to_webrtc import CONF_STUN_SERVER, DOMAIN
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup
@@ -27,6 +28,12 @@ OFFER_SDP = "v=0\r\no=carol 28908764872 28908764872 IN IP4 100.3.6.6\r\n..."
ANSWER_SDP = "v=0\r\no=bob 2890844730 2890844730 IN IP4 host.example.com\r\n..."
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
async def test_setup_success(
hass: HomeAssistant, rtsp_to_webrtc_client: Any, setup_integration: ComponentSetup
) -> None:
+1
View File
@@ -19,6 +19,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test select registered attributes to be excluded."""
now = dt_util.utcnow()
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, select.DOMAIN, {select.DOMAIN: {"platform": "demo"}}
)
+1
View File
@@ -1373,6 +1373,7 @@ def test_compile_hourly_sum_statistics_negative_state(
mocksensor._attr_should_poll = False
platform.ENTITIES["custom_sensor"] = mocksensor
setup_component(hass, "homeassistant", {})
setup_component(
hass, "sensor", {"sensor": [{"platform": "demo"}, {"platform": "test"}]}
)
@@ -46,6 +46,12 @@ MOCK_DETECTIONS = {
MOCK_NOW = datetime.datetime(2020, 2, 20, 10, 5, 3)
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def mock_detections():
"""Return a mock detection."""
+8
View File
@@ -1,4 +1,6 @@
"""The tests for the Light Switch platform."""
import pytest
from homeassistant.components.light import (
ATTR_COLOR_MODE,
ATTR_SUPPORTED_COLOR_MODES,
@@ -12,6 +14,12 @@ from . import common as switch_common
from tests.components.light import common
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
async def test_default_state(hass: HomeAssistant) -> None:
"""Test light switch default state."""
await async_setup_component(
+9
View File
@@ -6,6 +6,15 @@ from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
+1
View File
@@ -19,6 +19,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test siren registered attributes to be excluded."""
now = dt_util.utcnow()
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, text.DOMAIN, {text.DOMAIN: {"platform": "demo"}})
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
+2
View File
@@ -72,6 +72,8 @@ async def test_setup_legacy_service(hass: HomeAssistant) -> None:
},
}
await async_setup_component(hass, "homeassistant", {})
with assert_setup_component(1, tts.DOMAIN):
assert await async_setup_component(hass, tts.DOMAIN, config)
@@ -1105,6 +1105,7 @@ async def test_state_template(hass: HomeAssistant) -> None:
async def test_browse_media(hass: HomeAssistant) -> None:
"""Test browse media."""
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, "media_player", {"media_player": {"platform": "demo"}}
)
@@ -1135,6 +1136,7 @@ async def test_browse_media(hass: HomeAssistant) -> None:
async def test_browse_media_override(hass: HomeAssistant) -> None:
"""Test browse media override."""
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, "media_player", {"media_player": {"platform": "demo"}}
)
+21 -2
View File
@@ -4,6 +4,7 @@ import time
from unittest.mock import AsyncMock, Mock, patch
import async_timeout
import pytest
from homeassistant.components import assist_pipeline, voip
from homeassistant.components.voip.devices import VoIPDevice
@@ -88,6 +89,7 @@ async def test_pipeline(
hass.config.language,
voip_device,
Context(),
opus_payload_type=123,
listening_tone_enabled=False,
processing_tone_enabled=False,
error_tone_enabled=False,
@@ -138,6 +140,7 @@ async def test_pipeline_timeout(hass: HomeAssistant, voip_device: VoIPDevice) ->
hass.config.language,
voip_device,
Context(),
opus_payload_type=123,
pipeline_timeout=0.001,
listening_tone_enabled=False,
processing_tone_enabled=False,
@@ -178,6 +181,7 @@ async def test_stt_stream_timeout(hass: HomeAssistant, voip_device: VoIPDevice)
hass.config.language,
voip_device,
Context(),
opus_payload_type=123,
audio_timeout=0.001,
listening_tone_enabled=False,
processing_tone_enabled=False,
@@ -247,6 +251,14 @@ async def test_tts_timeout(
# Block here to force a timeout in _send_tts
time.sleep(2)
async def async_send_audio(audio_bytes, **kwargs):
if audio_bytes == tone_bytes:
# Not TTS
return
# Block here to force a timeout in _send_tts
await asyncio.sleep(2)
async def async_get_media_source_audio(
hass: HomeAssistant,
media_source_id: str,
@@ -269,6 +281,8 @@ async def test_tts_timeout(
hass.config.language,
voip_device,
Context(),
opus_payload_type=123,
tts_extra_timeout=0.001,
listening_tone_enabled=True,
processing_tone_enabled=True,
error_tone_enabled=True,
@@ -277,13 +291,18 @@ async def test_tts_timeout(
rtp_protocol._processing_bytes = tone_bytes
rtp_protocol._error_bytes = tone_bytes
rtp_protocol.transport = Mock()
rtp_protocol.send_audio = Mock(side_effect=send_audio)
rtp_protocol.send_audio = Mock()
original_send_tts = rtp_protocol._send_tts
async def send_tts(*args, **kwargs):
# Call original then end test successfully
rtp_protocol._send_tts(*args, **kwargs)
with pytest.raises(asyncio.TimeoutError):
await original_send_tts(*args, **kwargs)
done.set()
rtp_protocol._async_send_audio = AsyncMock(side_effect=async_send_audio)
rtp_protocol._send_tts = AsyncMock(side_effect=send_tts)
# silence
@@ -18,6 +18,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test weather attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, DOMAIN, {DOMAIN: {"platform": "demo"}})
hass.config.units = METRIC_SYSTEM
await hass.async_block_till_done()