mirror of
https://github.com/home-assistant/core.git
synced 2026-04-29 02:13:44 +02:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bce18bf61a | |||
| eda0731e60 | |||
| 238c87055f | |||
| 4b4464a3de | |||
| a07fbdd61c | |||
| 3126ebe9d6 | |||
| 89aec9d356 | |||
| 0cfa566ff6 | |||
| fffece95f5 | |||
| c61e29709c | |||
| 458fe17a48 | |||
| 15fdefd23b | |||
| 576f9600b5 | |||
| 7a62574360 | |||
| 0251d677d8 | |||
| 2cd9b94ecb | |||
| 3cd2ab2319 | |||
| 4f0d403393 | |||
| b558cf8b59 | |||
| 820c7b77ce | |||
| 9d0fc916fc | |||
| 387f07a97f | |||
| 44968cfc7c | |||
| c6751bed86 | |||
| b87e3860d9 | |||
| 8ef6bd85f5 | |||
| ad4fed4f60 | |||
| 1050895657 | |||
| c31d657206 | |||
| 88343bed77 | |||
| 51a10a84da | |||
| 5f3bbf2804 | |||
| b8eebf085c | |||
| cdfd53e1cc | |||
| ca147dd97e | |||
| 5b1278d885 | |||
| 0db28dcf4d | |||
| 7c651665c5 | |||
| 2f3964e3ce | |||
| eef95fa0d4 |
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"}}
|
||||
)
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}}
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"}}
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user