Compare commits

..

60 Commits

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

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

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

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

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

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

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

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

* Fix test

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

* Tweak

* Fix race, update tests

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

* Fix tests
2023-05-03 18:58:47 +02:00
Erik Montnemery c31d657206 Improve exposed entities tests (#92389) 2023-05-03 18:58:44 +02:00
repaxan 88343bed77 Add ZHA binding for window coverings (#92387) 2023-05-03 18:58:40 +02:00
Artem Draft 51a10a84da Bump pybravia to 0.3.3 (#92378) 2023-05-03 18:58:35 +02:00
Paulus Schoutsen 5f3bbf2804 Bumped version to 2023.5.0b8 2023-05-02 22:39:38 -04:00
Paulus Schoutsen b8eebf085c Fix deserialize bug + add test coverage (#92382) 2023-05-02 22:39:33 -04:00
Franck Nijhof cdfd53e1cc Bumped version to 2023.5.0b7 2023-05-02 22:44:32 +02:00
Bram Kragten ca147dd97e Update frontend to 20230502.0 (#92373) 2023-05-02 22:43:23 +02:00
Erik Montnemery 5b1278d885 Allow exposing entities not in the entity registry to assistants (#92363) 2023-05-02 22:43:19 +02:00
J. Nick Koston 0db28dcf4d Start onvif events later (#92354) 2023-05-02 22:43:15 +02:00
Raman Gupta 7c651665c5 Clean up zwave_js.cover (#92353) 2023-05-02 22:43:12 +02:00
J. Nick Koston 2f3964e3ce Bump ulid-transform to 0.7.2 (#92344) 2023-05-02 22:43:08 +02:00
John Pettitt eef95fa0d4 Increase default timeout in sense (#90556)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-05-02 22:43:03 +02:00
Franck Nijhof 43a1eb043b Bumped version to 2023.5.0b6 2023-05-01 22:55:49 +02:00
Bram Kragten 6b77775ed5 Update frontend to 20230501.0 (#92339) 2023-05-01 22:55:34 +02:00
Michael Hansen 7077d23127 Bump voip-utils to 0.0.6 (#92334) 2023-05-01 22:55:31 +02:00
J. Nick Koston c7eac0ebbb Avoid starting ONVIF PullPoint if the camera reports its unsupported (#92333) 2023-05-01 22:55:27 +02:00
David F. Mulcahey 7f13033f69 Don't poll ZHA electrical measurement sensors unnecessarily (#92330) 2023-05-01 22:55:23 +02:00
Paulus Schoutsen eba201e71b Add voip configuration url (#92326) 2023-05-01 22:55:20 +02:00
G Johansson 1e9d777201 Fix db_url issue in SQL (#92324)
* db_url fix

* Add test

* assert entry.options
2023-05-01 22:55:16 +02:00
J. Nick Koston 030b7f8a37 Bump sqlalchemy to 2.0.12 (#92315)
changelog: https://docs.sqlalchemy.org/en/20/changelog/changelog_20.html#change-2.0.12
2023-05-01 22:55:12 +02:00
J. Nick Koston 8cbc69fc92 Retry onvif setup when it is unexpectedly cancelled (#92313)
* Retry onvif setup when it is unexpectedly cancelled

fixes #92308

* Retry onvif setup when it is unexpectedly cancelled

fixes #92308
2023-05-01 22:55:08 +02:00
J. Nick Koston 2a5f5ea039 Reduce size of migration transactions to accommodate slow/busy systems (#92312)
* Reduce size of migration transactions to accommodate slow/busy systems

related issue #91489

* handle overloaded RPIs better
2023-05-01 22:55:04 +02:00
Michael Hansen 0ba662e7bc Allow configuring SIP port in VoIP (#92210)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-05-01 22:54:59 +02:00
Franck Nijhof 05530d656a Bumped version to 2023.5.0b5 2023-04-30 20:16:39 +02:00
Jan Bouwhuis 2b2be6a333 Fix mqtt not available when starting snips (#92296) 2023-04-30 20:16:28 +02:00
J. Nick Koston 5bd54490ea Ensure onvif webhook can be registered (#92295) 2023-04-30 20:16:25 +02:00
J. Nick Koston 00a28caa6d Bump bleak to 0.20.2 (#92294) 2023-04-30 20:16:21 +02:00
J. Nick Koston c4aa6ba262 Bump beacontools to fix conflict with construct<2.10 and >=2.8.16 (#92293) 2023-04-30 20:16:18 +02:00
J. Nick Koston 7a90db903b Prevent pysnmp from being installed as it does not work with newer python (#92292) 2023-04-30 20:16:14 +02:00
Robert Hillis fe279c8593 Add missing fstrings in Local Calendar (#92288) 2023-04-30 20:16:10 +02:00
Maximilian ddf5a9fbcc Bump pynina to 0.3.0 (#92286) 2023-04-30 20:16:07 +02:00
J. Nick Koston 093d5d6176 Fix august lock state when API reports locking and locked with the same timestamp (#92276) 2023-04-30 20:16:01 +02:00
120 changed files with 1952 additions and 734 deletions
@@ -60,6 +60,7 @@ class AlexaConfig(AbstractConfig):
"""Return an identifier for the user that represents this config."""
return ""
@core.callback
def should_expose(self, entity_id):
"""If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter:
+1 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from typing import Any
from atenpdu import AtenPE, AtenPEError
from atenpdu import AtenPE, AtenPEError # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.switch import (
+5 -5
View File
@@ -3,6 +3,7 @@ import asyncio
import logging
from aiohttp import ClientError
from yalexs.util import get_latest_activity
from homeassistant.core import callback
from homeassistant.helpers.debounce import Debouncer
@@ -169,12 +170,11 @@ class ActivityStream(AugustSubscriberMixin):
device_id = activity.device_id
activity_type = activity.activity_type
device_activities = self._latest_activities.setdefault(device_id, {})
lastest_activity = device_activities.get(activity_type)
# Ignore activities that are older than the latest one
# Ignore activities that are older than the latest one unless it is a non
# locking or unlocking activity with the exact same start time.
if (
lastest_activity
and lastest_activity.activity_start_time >= activity.activity_start_time
get_latest_activity(activity, device_activities.get(activity_type))
!= activity
):
continue
+18 -9
View File
@@ -5,7 +5,7 @@ from typing import Any
from aiohttp import ClientResponseError
from yalexs.activity import SOURCE_PUBNUB, ActivityType
from yalexs.lock import LockStatus
from yalexs.util import update_lock_detail_from_activity
from yalexs.util import get_latest_activity, update_lock_detail_from_activity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity
from homeassistant.config_entries import ConfigEntry
@@ -90,17 +90,26 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
@callback
def _update_from_data(self):
"""Get the latest state of the sensor and update activity."""
lock_activity = self._data.activity_stream.get_latest_device_activity(
self._device_id,
{ActivityType.LOCK_OPERATION, ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR},
activity_stream = self._data.activity_stream
device_id = self._device_id
if lock_activity := activity_stream.get_latest_device_activity(
device_id,
{ActivityType.LOCK_OPERATION},
):
self._attr_changed_by = lock_activity.operated_by
lock_activity_without_operator = activity_stream.get_latest_device_activity(
device_id,
{ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR},
)
if lock_activity is not None:
self._attr_changed_by = lock_activity.operated_by
update_lock_detail_from_activity(self._detail, lock_activity)
# If the source is pubnub the lock must be online since its a live update
if lock_activity.source == SOURCE_PUBNUB:
if latest_activity := get_latest_activity(
lock_activity_without_operator, lock_activity
):
if latest_activity.source == SOURCE_PUBNUB:
# If the source is pubnub the lock must be online since its a live update
self._detail.set_online(True)
update_lock_detail_from_activity(self._detail, latest_activity)
bridge_activity = self._data.activity_stream.get_latest_device_activity(
self._device_id, {ActivityType.BRIDGE_OPERATION}
@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==1.3.2", "yalexs-ble==2.1.16"]
"requirements": ["yalexs==1.3.3", "yalexs-ble==2.1.16"]
}
@@ -15,10 +15,10 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==0.20.1",
"bleak==0.20.2",
"bleak-retry-connector==3.0.2",
"bluetooth-adapters==0.15.3",
"bluetooth-auto-recovery==1.0.3",
"bluetooth-auto-recovery==1.1.2",
"bluetooth-data-tools==0.4.0",
"dbus-fast==1.85.0"
]
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pybravia"],
"requirements": ["pybravia==0.3.2"],
"requirements": ["pybravia==0.3.3"],
"ssdp": [
{
"st": "urn:schemas-sony-com:service:ScalarWebAPI:1",
+34 -18
View File
@@ -22,6 +22,7 @@ from homeassistant.components.alexa import (
)
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
async_get_assistant_settings,
async_listen_entity_updates,
async_should_expose,
@@ -29,6 +30,7 @@ from homeassistant.components.homeassistant.exposed_entities import (
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, start
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.event import async_call_later
@@ -104,7 +106,11 @@ def entity_supported(hass: HomeAssistant, entity_id: str) -> bool:
if domain in SUPPORTED_DOMAINS:
return True
device_class = get_device_class(hass, entity_id)
try:
device_class = get_device_class(hass, entity_id)
except HomeAssistantError:
# The entity no longer exists
return False
if (
domain == "binary_sensor"
and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES
@@ -193,35 +199,44 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
# Don't migrate if there's a YAML config
return
entity_registry = er.async_get(self.hass)
for entity_id, entry in entity_registry.entities.items():
if CLOUD_ALEXA in entry.options:
continue
options = {"should_expose": self._should_expose_legacy(entity_id)}
entity_registry.async_update_entity_options(entity_id, CLOUD_ALEXA, options)
for state in self.hass.states.async_all():
async_expose_entity(
self.hass,
CLOUD_ALEXA,
state.entity_id,
self._should_expose_legacy(state.entity_id),
)
for entity_id in self._prefs.alexa_entity_configs:
async_expose_entity(
self.hass,
CLOUD_ALEXA,
entity_id,
self._should_expose_legacy(entity_id),
)
async def async_initialize(self):
"""Initialize the Alexa config."""
await super().async_initialize()
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
if self._prefs.alexa_settings_version < 2:
self._migrate_alexa_entity_settings_v1()
await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION
async def on_hass_started(hass):
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
if self._prefs.alexa_settings_version < 2:
self._migrate_alexa_entity_settings_v1()
await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION
)
async_listen_entity_updates(
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
)
async def hass_started(hass):
async def on_hass_start(hass):
if self.enabled and ALEXA_DOMAIN not in self.hass.config.components:
await async_setup_component(self.hass, ALEXA_DOMAIN, {})
start.async_at_start(self.hass, hass_started)
start.async_at_start(self.hass, on_hass_start)
start.async_at_started(self.hass, on_hass_started)
self._prefs.async_listen_updates(self._async_prefs_updated)
async_listen_entity_updates(
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
)
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
self._handle_entity_registry_updated,
@@ -257,6 +272,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
and entity_supported(self.hass, entity_id)
)
@callback
def should_expose(self, entity_id):
"""If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter:
+51 -26
View File
@@ -11,7 +11,10 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
from homeassistant.components.google_assistant.helpers import AbstractConfig
from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
async_get_entity_settings,
async_listen_entity_updates,
async_set_assistant_option,
async_should_expose,
)
from homeassistant.components.sensor import SensorDeviceClass
@@ -23,6 +26,7 @@ from homeassistant.core import (
callback,
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er, start
from homeassistant.helpers.entity import get_device_class
from homeassistant.setup import async_setup_component
@@ -171,34 +175,59 @@ class CloudGoogleConfig(AbstractConfig):
# Don't migrate if there's a YAML config
return
entity_registry = er.async_get(self.hass)
for entity_id, entry in entity_registry.entities.items():
if CLOUD_GOOGLE in entry.options:
continue
options = {"should_expose": self._should_expose_legacy(entity_id)}
if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None):
options[PREF_DISABLE_2FA] = _2fa_disabled
entity_registry.async_update_entity_options(
entity_id, CLOUD_GOOGLE, options
for state in self.hass.states.async_all():
entity_id = state.entity_id
async_expose_entity(
self.hass,
CLOUD_GOOGLE,
entity_id,
self._should_expose_legacy(entity_id),
)
if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None):
async_set_assistant_option(
self.hass,
CLOUD_GOOGLE,
entity_id,
PREF_DISABLE_2FA,
_2fa_disabled,
)
for entity_id in self._prefs.google_entity_configs:
async_expose_entity(
self.hass,
CLOUD_GOOGLE,
entity_id,
self._should_expose_legacy(entity_id),
)
if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None):
async_set_assistant_option(
self.hass,
CLOUD_GOOGLE,
entity_id,
PREF_DISABLE_2FA,
_2fa_disabled,
)
async def async_initialize(self):
"""Perform async initialization of config."""
await super().async_initialize()
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
if self._prefs.google_settings_version < 2:
self._migrate_google_entity_settings_v1()
await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION
async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
if self._prefs.google_settings_version < 2:
self._migrate_google_entity_settings_v1()
await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION
)
async_listen_entity_updates(
self.hass, CLOUD_GOOGLE, self._async_exposed_entities_updated
)
async def hass_started(hass):
async def on_hass_start(hass: HomeAssistant) -> None:
if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components:
await async_setup_component(self.hass, GOOGLE_DOMAIN, {})
start.async_at_start(self.hass, hass_started)
start.async_at_start(self.hass, on_hass_start)
start.async_at_started(self.hass, on_hass_started)
# Remove any stored user agent id that is not ours
remove_agent_user_ids = []
@@ -210,9 +239,6 @@ class CloudGoogleConfig(AbstractConfig):
await self.async_disconnect_agent_user(agent_user_id)
self._prefs.async_listen_updates(self._async_prefs_updated)
async_listen_entity_updates(
self.hass, CLOUD_GOOGLE, self._async_exposed_entities_updated
)
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
self._handle_entity_registry_updated,
@@ -289,14 +315,13 @@ class CloudGoogleConfig(AbstractConfig):
def should_2fa(self, state):
"""If an entity should be checked for 2FA."""
entity_registry = er.async_get(self.hass)
registry_entry = entity_registry.async_get(state.entity_id)
if not registry_entry:
try:
settings = async_get_entity_settings(self.hass, state.entity_id)
except HomeAssistantError:
# Handle the entity has been removed
return False
assistant_options = registry_entry.options.get(CLOUD_GOOGLE, {})
assistant_options = settings.get(CLOUD_GOOGLE, {})
return not assistant_options.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
async def async_report_state(self, message, agent_user_id: str):
@@ -382,7 +407,7 @@ class CloudGoogleConfig(AbstractConfig):
self.async_schedule_google_sync_all()
@callback
def _handle_device_registry_updated(self, event: Event) -> None:
async def _handle_device_registry_updated(self, event: Event) -> None:
"""Handle when device registry updated."""
if (
not self.enabled
+18 -34
View File
@@ -1,6 +1,7 @@
"""The HTTP api to control the cloud integration."""
import asyncio
from collections.abc import Mapping
from contextlib import suppress
import dataclasses
from functools import wraps
from http import HTTPStatus
@@ -21,11 +22,12 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
)
from homeassistant.components.google_assistant import helpers as google_helpers
from homeassistant.components.homeassistant import exposed_entities
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.location import async_detect_location_info
@@ -566,15 +568,14 @@ async def google_assistant_get(
"""Get data for a single google assistant entity."""
cloud = hass.data[DOMAIN]
gconf = await cloud.client.get_google_config()
entity_registry = er.async_get(hass)
entity_id: str = msg["entity_id"]
state = hass.states.get(entity_id)
if not entity_registry.async_is_registered(entity_id) or not state:
if not state:
connection.send_error(
msg["id"],
websocket_api.const.ERR_NOT_FOUND,
f"{entity_id} unknown or not in the entity registry",
f"{entity_id} unknown",
)
return
@@ -587,10 +588,16 @@ async def google_assistant_get(
)
return
assistant_options: Mapping[str, Any] = {}
with suppress(HomeAssistantError, KeyError):
settings = exposed_entities.async_get_entity_settings(hass, entity_id)
assistant_options = settings[CLOUD_GOOGLE]
result = {
"entity_id": entity.entity_id,
"traits": [trait.name for trait in entity.traits()],
"might_2fa": entity.might_2fa_traits(),
PREF_DISABLE_2FA: assistant_options.get(PREF_DISABLE_2FA),
}
connection.send_result(msg["id"], result)
@@ -609,14 +616,11 @@ async def google_assistant_list(
"""List all google assistant entities."""
cloud = hass.data[DOMAIN]
gconf = await cloud.client.get_google_config()
entity_registry = er.async_get(hass)
entities = google_helpers.async_get_entities(hass, gconf)
result = []
for entity in entities:
if not entity_registry.async_is_registered(entity.entity_id):
continue
result.append(
{
"entity_id": entity.entity_id,
@@ -645,27 +649,19 @@ async def google_assistant_update(
msg: dict[str, Any],
) -> None:
"""Update google assistant entity config."""
entity_registry = er.async_get(hass)
entity_id: str = msg["entity_id"]
if not (registry_entry := entity_registry.async_get(entity_id)):
connection.send_error(
msg["id"],
websocket_api.const.ERR_NOT_ALLOWED,
f"can't configure {entity_id}",
)
return
assistant_options: Mapping[str, Any] = {}
with suppress(HomeAssistantError, KeyError):
settings = exposed_entities.async_get_entity_settings(hass, entity_id)
assistant_options = settings[CLOUD_GOOGLE]
disable_2fa = msg[PREF_DISABLE_2FA]
assistant_options: Mapping[str, Any]
if (
assistant_options := registry_entry.options.get(CLOUD_GOOGLE, {})
) and assistant_options.get(PREF_DISABLE_2FA) == disable_2fa:
if assistant_options.get(PREF_DISABLE_2FA) == disable_2fa:
return
assistant_options = assistant_options | {PREF_DISABLE_2FA: disable_2fa}
entity_registry.async_update_entity_options(
entity_id, CLOUD_GOOGLE, assistant_options
exposed_entities.async_set_assistant_option(
hass, CLOUD_GOOGLE, entity_id, PREF_DISABLE_2FA, disable_2fa
)
connection.send_result(msg["id"])
@@ -686,17 +682,8 @@ async def alexa_get(
msg: dict[str, Any],
) -> None:
"""Get data for a single alexa entity."""
entity_registry = er.async_get(hass)
entity_id: str = msg["entity_id"]
if not entity_registry.async_is_registered(entity_id):
connection.send_error(
msg["id"],
websocket_api.const.ERR_NOT_FOUND,
f"{entity_id} not in the entity registry",
)
return
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity_supported_by_alexa(
hass, entity_id
):
@@ -723,14 +710,11 @@ async def alexa_list(
"""List all alexa entities."""
cloud = hass.data[DOMAIN]
alexa_config = await cloud.client.get_alexa_config()
entity_registry = er.async_get(hass)
entities = alexa_entities.async_get_entities(hass, alexa_config)
result = []
for entity in entities:
if not entity_registry.async_is_registered(entity.entity_id):
continue
result.append(
{
"entity_id": entity.entity_id,
@@ -1,4 +1,5 @@
"""Const for conversation integration."""
DOMAIN = "conversation"
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
HOME_ASSISTANT_AGENT = "homeassistant"
@@ -21,19 +21,21 @@ from homeassistant.components.homeassistant.exposed_entities import (
async_listen_entity_updates,
async_should_expose,
)
from homeassistant.const import ATTR_DEVICE_CLASS
from homeassistant.const import MATCH_ALL
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity_registry as er,
intent,
start,
template,
translation,
)
from homeassistant.helpers.event import async_track_state_change
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent import AbstractConversationAgent, ConversationInput, ConversationResult
from .const import DOMAIN
from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN
_LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
@@ -81,16 +83,24 @@ def async_setup(hass: core.HomeAssistant) -> None:
async_should_expose(hass, DOMAIN, entity_id)
@core.callback
def async_handle_entity_registry_changed(event: core.Event) -> None:
"""Set expose flag on newly created entities."""
if event.data["action"] == "create":
async_should_expose(hass, DOMAIN, event.data["entity_id"])
def async_entity_state_listener(
changed_entity: str,
old_state: core.State | None,
new_state: core.State | None,
):
"""Set expose flag on new entities."""
if old_state is not None or new_state is None:
return
async_should_expose(hass, DOMAIN, changed_entity)
hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
async_handle_entity_registry_changed,
run_immediately=True,
)
@core.callback
def async_hass_started(hass: core.HomeAssistant) -> None:
"""Set expose flag on all entities."""
for state in hass.states.async_all():
async_should_expose(hass, DOMAIN, state.entity_id)
async_track_state_change(hass, MATCH_ALL, async_entity_state_listener)
start.async_at_started(hass, async_hass_started)
class DefaultAgent(AbstractConversationAgent):
@@ -130,6 +140,11 @@ class DefaultAgent(AbstractConversationAgent):
self._async_handle_entity_registry_changed,
run_immediately=True,
)
self.hass.bus.async_listen(
core.EVENT_STATE_CHANGED,
self._async_handle_state_changed,
run_immediately=True,
)
async_listen_entity_updates(
self.hass, DOMAIN, self._async_exposed_entities_updated
)
@@ -186,6 +201,7 @@ class DefaultAgent(AbstractConversationAgent):
user_input.text,
user_input.context,
language,
assistant=DOMAIN,
)
except intent.IntentHandleError:
_LOGGER.exception("Intent handling error")
@@ -475,12 +491,19 @@ class DefaultAgent(AbstractConversationAgent):
@core.callback
def _async_handle_entity_registry_changed(self, event: core.Event) -> None:
"""Clear names list cache when an entity registry entry has changed."""
if event.data["action"] == "update" and not any(
if event.data["action"] != "update" or not any(
field in event.data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
):
return
self._slot_lists = None
@core.callback
def _async_handle_state_changed(self, event: core.Event) -> None:
"""Clear names list cache when a state is added or removed from the state machine."""
if event.data.get("old_state") and event.data.get("new_state"):
return
self._slot_lists = None
@core.callback
def _async_exposed_entities_updated(self) -> None:
"""Handle updated preferences."""
@@ -493,30 +516,38 @@ class DefaultAgent(AbstractConversationAgent):
area_ids_with_entities: set[str] = set()
entity_registry = er.async_get(self.hass)
entities = [
entity
for entity in entity_registry.entities.values()
if async_should_expose(self.hass, DOMAIN, entity.entity_id)
states = [
state
for state in self.hass.states.async_all()
if async_should_expose(self.hass, DOMAIN, state.entity_id)
]
devices = dr.async_get(self.hass)
# Gather exposed entity names
entity_names = []
for entity in entities:
for state in states:
# Checked against "requires_context" and "excludes_context" in hassil
context = {"domain": entity.domain}
if entity.device_class:
context[ATTR_DEVICE_CLASS] = entity.device_class
context = {"domain": state.domain}
if state.attributes:
# Include some attributes
for attr in DEFAULT_EXPOSED_ATTRIBUTES:
if attr not in state.attributes:
continue
context[attr] = state.attributes[attr]
entity = entity_registry.async_get(state.entity_id)
if not entity:
# Default name
entity_names.append((state.name, state.name, context))
continue
if entity.aliases:
for alias in entity.aliases:
entity_names.append((alias, alias, context))
# Default name
name = entity.async_friendly_name(self.hass) or entity.entity_id.replace(
"_", " "
)
entity_names.append((name, name, context))
entity_names.append((state.name, state.name, context))
if entity.area_id:
# Expose area too
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/eddystone_temperature",
"iot_class": "local_polling",
"loggers": ["beacontools"],
"requirements": ["beacontools[scan]==1.2.3", "construct==2.10.56"]
"requirements": ["beacontools[scan]==2.1.0", "construct==2.10.56"]
}
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230428.0"]
"requirements": ["home-assistant-frontend==20230503.2"]
}
@@ -3,7 +3,8 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
import dataclasses
from typing import Any
from itertools import chain
from typing import Any, TypedDict
import voluptuous as vol
@@ -77,16 +78,41 @@ class AssistantPreferences:
return {"expose_new": self.expose_new}
@dataclasses.dataclass(frozen=True)
class ExposedEntity:
"""An exposed entity without a unique_id."""
assistants: dict[str, dict[str, Any]]
def to_json(self) -> dict[str, Any]:
"""Return a JSON serializable representation for storage."""
return {
"assistants": self.assistants,
}
class SerializedExposedEntities(TypedDict):
"""Serialized exposed entities storage storage collection."""
assistants: dict[str, dict[str, Any]]
exposed_entities: dict[str, dict[str, Any]]
class ExposedEntities:
"""Control assistant settings."""
"""Control assistant settings.
Settings for entities without a unique_id are stored in the store.
Settings for entities with a unique_id are stored in the entity registry.
"""
_assistants: dict[str, AssistantPreferences]
entities: dict[str, ExposedEntity]
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
self._hass = hass
self._listeners: dict[str, list[Callable[[], None]]] = {}
self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
self._store: Store[SerializedExposedEntities] = Store(
hass, STORAGE_VERSION, STORAGE_KEY
)
@@ -95,7 +121,8 @@ class ExposedEntities:
websocket_api.async_register_command(self._hass, ws_expose_entity)
websocket_api.async_register_command(self._hass, ws_expose_new_entities_get)
websocket_api.async_register_command(self._hass, ws_expose_new_entities_set)
await self.async_load()
websocket_api.async_register_command(self._hass, ws_list_exposed_entities)
await self._async_load_data()
@callback
def async_listen_entity_updates(
@@ -105,30 +132,57 @@ class ExposedEntities:
self._listeners.setdefault(assistant, []).append(listener)
@callback
def async_expose_entity(
self, assistant: str, entity_id: str, should_expose: bool
def async_set_assistant_option(
self, assistant: str, entity_id: str, key: str, value: Any
) -> None:
"""Expose an entity to an assistant.
"""Set an option for an assistant.
Notify listeners if expose flag was changed.
"""
entity_registry = er.async_get(self._hass)
if not (registry_entry := entity_registry.async_get(entity_id)):
raise HomeAssistantError("Unknown entity")
return self._async_set_legacy_assistant_option(
assistant, entity_id, key, value
)
assistant_options: Mapping[str, Any]
if (
assistant_options := registry_entry.options.get(assistant, {})
) and assistant_options.get("should_expose") == should_expose:
) and assistant_options.get(key) == value:
return
assistant_options = assistant_options | {"should_expose": should_expose}
assistant_options = assistant_options | {key: value}
entity_registry.async_update_entity_options(
entity_id, assistant, assistant_options
)
for listener in self._listeners.get(assistant, []):
listener()
def _async_set_legacy_assistant_option(
self, assistant: str, entity_id: str, key: str, value: Any
) -> None:
"""Set an option for an assistant.
Notify listeners if expose flag was changed.
"""
if (
(exposed_entity := self.entities.get(entity_id))
and (assistant_options := exposed_entity.assistants.get(assistant, {}))
and assistant_options.get(key) == value
):
return
if exposed_entity:
new_exposed_entity = self._update_exposed_entity(
assistant, entity_id, key, value
)
else:
new_exposed_entity = self._new_exposed_entity(assistant, key, value)
self.entities[entity_id] = new_exposed_entity
self._async_schedule_save()
for listener in self._listeners.get(assistant, []):
listener()
@callback
def async_get_expose_new_entities(self, assistant: str) -> bool:
"""Check if new entities are exposed to an assistant."""
@@ -150,6 +204,11 @@ class ExposedEntities:
entity_registry = er.async_get(self._hass)
result: dict[str, Mapping[str, Any]] = {}
options: Mapping | None
for entity_id, exposed_entity in self.entities.items():
if options := exposed_entity.assistants.get(assistant):
result[entity_id] = options
for entity_id, entry in entity_registry.entities.items():
if options := entry.options.get(assistant):
result[entity_id] = options
@@ -162,11 +221,16 @@ class ExposedEntities:
entity_registry = er.async_get(self._hass)
result: dict[str, Mapping[str, Any]] = {}
if not (registry_entry := entity_registry.async_get(entity_id)):
assistant_settings: Mapping
if registry_entry := entity_registry.async_get(entity_id):
assistant_settings = registry_entry.options
elif exposed_entity := self.entities.get(entity_id):
assistant_settings = exposed_entity.assistants
else:
raise HomeAssistantError("Unknown entity")
for assistant in KNOWN_ASSISTANTS:
if options := registry_entry.options.get(assistant):
if options := assistant_settings.get(assistant):
result[assistant] = options
return result
@@ -181,9 +245,7 @@ class ExposedEntities:
entity_registry = er.async_get(self._hass)
if not (registry_entry := entity_registry.async_get(entity_id)):
# Entities which are not in the entity registry are not exposed
return False
return self._async_should_expose_legacy_entity(assistant, entity_id)
if assistant in registry_entry.options:
if "should_expose" in registry_entry.options[assistant]:
should_expose = registry_entry.options[assistant]["should_expose"]
@@ -202,11 +264,42 @@ class ExposedEntities:
return should_expose
def _async_should_expose_legacy_entity(
self, assistant: str, entity_id: str
) -> bool:
"""Return True if an entity should be exposed to an assistant."""
should_expose: bool
if (
exposed_entity := self.entities.get(entity_id)
) and assistant in exposed_entity.assistants:
if "should_expose" in exposed_entity.assistants[assistant]:
should_expose = exposed_entity.assistants[assistant]["should_expose"]
return should_expose
if self.async_get_expose_new_entities(assistant):
should_expose = self._is_default_exposed(entity_id, None)
else:
should_expose = False
if exposed_entity:
new_exposed_entity = self._update_exposed_entity(
assistant, entity_id, "should_expose", should_expose
)
else:
new_exposed_entity = self._new_exposed_entity(
assistant, "should_expose", should_expose
)
self.entities[entity_id] = new_exposed_entity
self._async_schedule_save()
return should_expose
def _is_default_exposed(
self, entity_id: str, registry_entry: er.RegistryEntry
self, entity_id: str, registry_entry: er.RegistryEntry | None
) -> bool:
"""Return True if an entity is exposed by default."""
if (
if registry_entry and (
registry_entry.entity_category is not None
or registry_entry.hidden_by is not None
):
@@ -216,7 +309,11 @@ class ExposedEntities:
if domain in DEFAULT_EXPOSED_DOMAINS:
return True
device_class = get_device_class(self._hass, entity_id)
try:
device_class = get_device_class(self._hass, entity_id)
except HomeAssistantError:
# The entity no longer exists
return False
if (
domain == "binary_sensor"
and device_class in DEFAULT_EXPOSED_BINARY_SENSOR_DEVICE_CLASSES
@@ -228,17 +325,43 @@ class ExposedEntities:
return False
async def async_load(self) -> None:
def _update_exposed_entity(
self, assistant: str, entity_id: str, key: str, value: Any
) -> ExposedEntity:
"""Update an exposed entity."""
entity = self.entities[entity_id]
assistants = dict(entity.assistants)
old_settings = assistants.get(assistant, {})
assistants[assistant] = old_settings | {key: value}
return ExposedEntity(assistants)
def _new_exposed_entity(
self, assistant: str, key: str, value: Any
) -> ExposedEntity:
"""Create a new exposed entity."""
return ExposedEntity(
assistants={assistant: {key: value}},
)
async def _async_load_data(self) -> SerializedExposedEntities | None:
"""Load from the store."""
data = await self._store.async_load()
assistants: dict[str, AssistantPreferences] = {}
exposed_entities: dict[str, ExposedEntity] = {}
if data:
for domain, preferences in data["assistants"].items():
assistants[domain] = AssistantPreferences(**preferences)
if data and "exposed_entities" in data:
for entity_id, preferences in data["exposed_entities"].items():
exposed_entities[entity_id] = ExposedEntity(**preferences)
self._assistants = assistants
self.entities = exposed_entities
return data
@callback
def _async_schedule_save(self) -> None:
@@ -246,17 +369,19 @@ class ExposedEntities:
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@callback
def _data_to_save(self) -> dict[str, dict[str, dict[str, Any]]]:
"""Return data to store in a file."""
data = {}
data["assistants"] = {
domain: preferences.to_json()
for domain, preferences in self._assistants.items()
def _data_to_save(self) -> SerializedExposedEntities:
"""Return JSON-compatible date for storing to file."""
return {
"assistants": {
domain: preferences.to_json()
for domain, preferences in self._assistants.items()
},
"exposed_entities": {
entity_id: entity.to_json()
for entity_id, entity in self.entities.items()
},
}
return data
@callback
@websocket_api.require_admin
@@ -272,7 +397,6 @@ def ws_expose_entity(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Expose an entity to an assistant."""
entity_registry = er.async_get(hass)
entity_ids: str = msg["entity_ids"]
if blocked := next(
@@ -288,28 +412,37 @@ def ws_expose_entity(
)
return
if unknown := next(
(
entity_id
for entity_id in entity_ids
if entity_id not in entity_registry.entities
),
None,
):
connection.send_error(
msg["id"], websocket_api.const.ERR_NOT_FOUND, f"can't expose '{unknown}'"
)
return
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
for entity_id in entity_ids:
for assistant in msg["assistants"]:
exposed_entities.async_expose_entity(
assistant, entity_id, msg["should_expose"]
)
async_expose_entity(hass, assistant, entity_id, msg["should_expose"])
connection.send_result(msg["id"])
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "homeassistant/expose_entity/list",
}
)
def ws_list_exposed_entities(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Expose an entity to an assistant."""
result: dict[str, Any] = {}
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
entity_registry = er.async_get(hass)
for entity_id in chain(exposed_entities.entities, entity_registry.entities):
result[entity_id] = {}
entity_settings = async_get_entity_settings(hass, entity_id)
for assistant, settings in entity_settings.items():
if "should_expose" not in settings:
continue
result[entity_id][assistant] = settings["should_expose"]
connection.send_result(msg["id"], {"exposed_entities": result})
@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
@@ -380,8 +513,9 @@ def async_expose_entity(
should_expose: bool,
) -> None:
"""Get assistant expose settings for an entity."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity(assistant, entity_id, should_expose)
async_set_assistant_option(
hass, assistant, entity_id, "should_expose", should_expose
)
@callback
@@ -389,3 +523,15 @@ def async_should_expose(hass: HomeAssistant, assistant: str, entity_id: str) ->
"""Return True if an entity should be exposed to an assistant."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
return exposed_entities.async_should_expose(assistant, entity_id)
@callback
def async_set_assistant_option(
hass: HomeAssistant, assistant: str, entity_id: str, option: str, value: Any
) -> None:
"""Set an option for an assistant.
Notify listeners if expose flag was changed.
"""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_set_assistant_option(assistant, entity_id, option, value)
+3 -1
View File
@@ -140,16 +140,18 @@ class GetStateIntentHandler(intent.IntentHandler):
area=area,
domains=domains,
device_classes=device_classes,
assistant=intent_obj.assistant,
)
)
_LOGGER.debug(
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s",
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s, assistant=%s",
len(states),
name,
area,
domains,
device_classes,
intent_obj.assistant,
)
# Create response
+10 -3
View File
@@ -205,13 +205,20 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
methods, DEFAULT_ATTEMPTS, OVERALL_TIMEOUT
)
def get_number_of_zones(self) -> int:
"""Return the number of zones.
If the number of zones is not yet populated, return 0
"""
return len(self.device.color_zones) if self.device.color_zones else 0
@callback
def _async_build_color_zones_update_requests(self) -> list[Callable]:
"""Build a color zones update request."""
device = self.device
return [
partial(device.get_color_zones, start_index=zone)
for zone in range(0, len(device.color_zones), 8)
for zone in range(0, self.get_number_of_zones(), 8)
]
async def _async_update_data(self) -> None:
@@ -224,7 +231,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
):
await self._async_populate_device_info()
num_zones = len(device.color_zones) if device.color_zones is not None else 0
num_zones = self.get_number_of_zones()
features = lifx_features(self.device)
is_extended_multizone = features["extended_multizone"]
is_legacy_multizone = not is_extended_multizone and features["multizone"]
@@ -256,7 +263,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
if is_extended_multizone or is_legacy_multizone:
self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")]
if is_legacy_multizone and num_zones != len(device.color_zones):
if is_legacy_multizone and num_zones != self.get_number_of_zones():
# The number of zones has changed so we need
# to update the zones again. This happens rarely.
await self.async_get_color_zones()
+1 -1
View File
@@ -382,7 +382,7 @@ class LIFXMultiZone(LIFXColor):
"""Send a color change to the bulb."""
bulb = self.bulb
color_zones = bulb.color_zones
num_zones = len(color_zones)
num_zones = self.coordinator.get_number_of_zones()
# Zone brightness is not reported when powered off
if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None:
@@ -129,7 +129,7 @@ class LocalCalendarEntity(CalendarEntity):
recurrence_range=range_value,
)
except EventStoreError as err:
raise HomeAssistantError("Error while deleting event: {err}") from err
raise HomeAssistantError(f"Error while deleting event: {err}") from err
await self._async_store()
await self.async_update_ha_state(force_refresh=True)
@@ -153,7 +153,7 @@ class LocalCalendarEntity(CalendarEntity):
recurrence_range=range_value,
)
except EventStoreError as err:
raise HomeAssistantError("Error while updating event: {err}") from err
raise HomeAssistantError(f"Error while updating event: {err}") from err
await self._async_store()
await self.async_update_ha_state(force_refresh=True)
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/nina",
"iot_class": "cloud_polling",
"loggers": ["pynina"],
"requirements": ["pynina==0.2.0"]
"requirements": ["pynina==0.3.0"]
}
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aionotion"],
"requirements": ["aionotion==2023.04.2"]
"requirements": ["aionotion==2023.05.0"]
}
@@ -1,4 +1,5 @@
"""The ONVIF integration."""
import asyncio
import logging
from httpx import RequestError
@@ -57,6 +58,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady(
f"Could not setup camera {device.device.host}:{device.device.port}: {err}"
) from err
except asyncio.CancelledError as err:
# After https://github.com/agronholm/anyio/issues/374 is resolved
# this may be able to be removed
await device.device.close()
raise ConfigEntryNotReady(f"Setup was unexpectedly canceled: {err}") from err
if not device.available:
raise ConfigEntryNotReady()
+138 -75
View File
@@ -6,12 +6,13 @@ from contextlib import suppress
import datetime as dt
import os
import time
from typing import Any
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 (
@@ -55,6 +56,7 @@ class ONVIFDevice:
self.info: DeviceInfo = DeviceInfo()
self.capabilities: Capabilities = Capabilities()
self.onvif_capabilities: dict[str, Any] | None = None
self.profiles: list[Profile] = []
self.max_resolution: int = 0
self.platforms: list[Platform] = []
@@ -98,6 +100,11 @@ 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()
await self.async_check_date_and_time()
# Create event manager
@@ -106,9 +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)
#
# 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("Camera %s capabilities = %s", self.name, self.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)
@@ -117,6 +135,7 @@ class ONVIFDevice:
raise ONVIFError("No camera profiles found")
if self.capabilities.ptz:
LOGGER.debug("%s: creating PTZ service", self.name)
self.device.create_ptz_service()
# Determine max resolution from profiles
@@ -126,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:
@@ -178,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
@@ -272,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,
)
@@ -297,16 +345,31 @@ class ONVIFDevice:
self.device.create_imaging_service()
imaging = True
events = False
with suppress(*GET_CAPABILITIES_EXCEPTIONS, XMLParseError):
events = await self.events.async_start()
return Capabilities(snapshot=snapshot, ptz=ptz, imaging=imaging)
return Capabilities(snapshot, events, ptz, 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") or {}).get(
"WSPullPointSupport"
)
LOGGER.debug("%s: WSPullPointSupport: %s", self.name, pull_point_support)
return await self.events.async_start(pull_point_support is not False, True)
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):
+29 -8
View File
@@ -11,7 +11,7 @@ from httpx import RemoteProtocolError, RequestError, TransportError
from onvif import ONVIFCamera, ONVIFService
from onvif.client import NotificationManager
from onvif.exceptions import ONVIFError
from zeep.exceptions import Fault, XMLParseError
from zeep.exceptions import Fault, ValidationError, XMLParseError
from homeassistant.components import webhook
from homeassistant.config_entries import ConfigEntry
@@ -35,7 +35,7 @@ from .util import stringify_onvif_error
UNHANDLED_TOPICS: set[str] = {"tns1:MediaControl/VideoEncoderConfiguration"}
SUBSCRIPTION_ERRORS = (Fault, asyncio.TimeoutError, TransportError)
CREATE_ERRORS = (ONVIFError, Fault, RequestError, XMLParseError)
CREATE_ERRORS = (ONVIFError, Fault, RequestError, XMLParseError, ValidationError)
SET_SYNCHRONIZATION_POINT_ERRORS = (*SUBSCRIPTION_ERRORS, TypeError)
UNSUBSCRIBE_ERRORS = (XMLParseError, *SUBSCRIPTION_ERRORS)
RENEW_ERRORS = (ONVIFError, RequestError, XMLParseError, *SUBSCRIPTION_ERRORS)
@@ -123,11 +123,13 @@ class EventManager:
if not self._listeners:
self.pullpoint_manager.async_cancel_pull_messages()
async def async_start(self) -> bool:
async def async_start(self, try_pullpoint: bool, try_webhook: bool) -> bool:
"""Start polling events."""
# Always start pull point first, since it will populate the event list
event_via_pull_point = await self.pullpoint_manager.async_start()
events_via_webhook = await self.webhook_manager.async_start()
event_via_pull_point = (
try_pullpoint and await self.pullpoint_manager.async_start()
)
events_via_webhook = try_webhook and await self.webhook_manager.async_start()
return events_via_webhook or event_via_pull_point
async def async_stop(self) -> None:
@@ -655,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."""
@@ -769,6 +789,7 @@ class WebHookManager:
return
webhook_id = self._webhook_unique_id
self._async_unregister_webhook()
webhook.async_register(
self._hass, DOMAIN, webhook_id, webhook_id, self._async_handle_webhook
)
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/onvif",
"iot_class": "local_push",
"loggers": ["onvif", "wsdiscovery", "zeep"],
"requirements": ["onvif-zeep-async==1.3.0", "WSDiscovery==2.0.0"]
"requirements": ["onvif-zeep-async==1.3.1", "WSDiscovery==2.0.0"]
}
@@ -7,7 +7,7 @@
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": [
"sqlalchemy==2.0.11",
"sqlalchemy==2.0.12",
"fnv-hash-fast==0.3.1",
"psutil-home-assistant==0.0.1"
]
+10 -10
View File
@@ -1158,23 +1158,23 @@ def _wipe_old_string_time_columns(
elif engine.dialect.name == SupportedDialect.MYSQL:
#
# Since this is only to save space we limit the number of rows we update
# to 10,000,000 per table since we do not want to block the database for too long
# to 100,000 per table since we do not want to block the database for too long
# or run out of innodb_buffer_pool_size on MySQL. The old data will eventually
# be cleaned up by the recorder purge if we do not do it now.
#
session.execute(text("UPDATE events set time_fired=NULL LIMIT 10000000;"))
session.execute(text("UPDATE events set time_fired=NULL LIMIT 100000;"))
session.commit()
session.execute(
text(
"UPDATE states set last_updated=NULL, last_changed=NULL "
" LIMIT 10000000;"
" LIMIT 100000;"
)
)
session.commit()
elif engine.dialect.name == SupportedDialect.POSTGRESQL:
#
# Since this is only to save space we limit the number of rows we update
# to 250,000 per table since we do not want to block the database for too long
# to 100,000 per table since we do not want to block the database for too long
# or run out ram with postgresql. The old data will eventually
# be cleaned up by the recorder purge if we do not do it now.
#
@@ -1182,7 +1182,7 @@ def _wipe_old_string_time_columns(
text(
"UPDATE events set time_fired=NULL "
"where event_id in "
"(select event_id from events where time_fired_ts is NOT NULL LIMIT 250000);"
"(select event_id from events where time_fired_ts is NOT NULL LIMIT 100000);"
)
)
session.commit()
@@ -1190,7 +1190,7 @@ def _wipe_old_string_time_columns(
text(
"UPDATE states set last_updated=NULL, last_changed=NULL "
"where state_id in "
"(select state_id from states where last_updated_ts is NOT NULL LIMIT 250000);"
"(select state_id from states where last_updated_ts is NOT NULL LIMIT 100000);"
)
)
session.commit()
@@ -1236,7 +1236,7 @@ def _migrate_columns_to_timestamp(
"UNIX_TIMESTAMP(time_fired)"
") "
"where time_fired_ts is NULL "
"LIMIT 250000;"
"LIMIT 100000;"
)
)
result = None
@@ -1251,7 +1251,7 @@ def _migrate_columns_to_timestamp(
"last_changed_ts="
"UNIX_TIMESTAMP(last_changed) "
"where last_updated_ts is NULL "
"LIMIT 250000;"
"LIMIT 100000;"
)
)
elif engine.dialect.name == SupportedDialect.POSTGRESQL:
@@ -1266,7 +1266,7 @@ def _migrate_columns_to_timestamp(
"time_fired_ts= "
"(case when time_fired is NULL then 0 else EXTRACT(EPOCH FROM time_fired::timestamptz) end) "
"WHERE event_id IN ( "
"SELECT event_id FROM events where time_fired_ts is NULL LIMIT 250000 "
"SELECT event_id FROM events where time_fired_ts is NULL LIMIT 100000 "
" );"
)
)
@@ -1279,7 +1279,7 @@ def _migrate_columns_to_timestamp(
"(case when last_updated is NULL then 0 else EXTRACT(EPOCH FROM last_updated::timestamptz) end), "
"last_changed_ts=EXTRACT(EPOCH FROM last_changed::timestamptz) "
"where state_id IN ( "
"SELECT state_id FROM states where last_updated_ts is NULL LIMIT 250000 "
"SELECT state_id FROM states where last_updated_ts is NULL LIMIT 100000 "
" );"
)
)
@@ -2314,7 +2314,7 @@ def cleanup_statistics_timestamp_migration(instance: Recorder) -> bool:
session.connection()
.execute(
text(
f"UPDATE {table} set start=NULL, created=NULL, last_reset=NULL where start is not NULL LIMIT 250000;"
f"UPDATE {table} set start=NULL, created=NULL, last_reset=NULL where start is not NULL LIMIT 100000;"
)
)
.rowcount
@@ -2330,7 +2330,7 @@ def cleanup_statistics_timestamp_migration(instance: Recorder) -> bool:
.execute(
text(
f"UPDATE {table} set start=NULL, created=NULL, last_reset=NULL " # nosec
f"where id in (select id from {table} where start is not NULL LIMIT 250000)"
f"where id in (select id from {table} where start is not NULL LIMIT 100000)"
)
)
.rowcount
+1 -1
View File
@@ -10,7 +10,7 @@ from sense_energy import (
)
DOMAIN = "sense"
DEFAULT_TIMEOUT = 10
DEFAULT_TIMEOUT = 30
ACTIVE_UPDATE_RATE = 60
DEFAULT_NAME = "Sense"
SENSE_DATA = "sense_data"
@@ -91,6 +91,11 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Activate Snips component."""
# Make sure MQTT integration is enabled and the client is available
if not await mqtt.async_wait_for_mqtt_client(hass):
_LOGGER.error("MQTT integration is not available")
return False
async def async_set_feedback(site_ids, state):
"""Set Feedback sound state."""
site_ids = site_ids if site_ids else config[DOMAIN].get(CONF_SITE_IDS)
+10
View File
@@ -1,6 +1,8 @@
"""The sql component."""
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant.components.recorder import CONF_DB_URL, get_instance
@@ -24,6 +26,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN, PLATFORMS
from .util import redact_credentials
_LOGGER = logging.getLogger(__name__)
def validate_sql_select(value: str) -> str:
@@ -85,6 +90,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SQL from a config entry."""
_LOGGER.debug(
"Comparing %s and %s",
redact_credentials(entry.options.get(CONF_DB_URL)),
redact_credentials(get_instance(hass).db_url),
)
if entry.options.get(CONF_DB_URL) == get_instance(hass).db_url:
remove_configured_db_url_if_not_needed(hass, entry)
+14 -13
View File
@@ -11,7 +11,7 @@ from sqlalchemy.orm import Session, scoped_session, sessionmaker
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.recorder import CONF_DB_URL
from homeassistant.components.recorder import CONF_DB_URL, get_instance
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
@@ -159,13 +159,9 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
class SQLOptionsFlowHandler(config_entries.OptionsFlow):
class SQLOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
"""Handle SQL options."""
def __init__(self, entry: config_entries.ConfigEntry) -> None:
"""Initialize SQL options flow."""
self.entry = entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -177,7 +173,7 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow):
db_url = user_input.get(CONF_DB_URL)
query = user_input[CONF_QUERY]
column = user_input[CONF_COLUMN_NAME]
name = self.entry.options.get(CONF_NAME, self.entry.title)
name = self.options.get(CONF_NAME, self.config_entry.title)
try:
validate_sql_select(query)
@@ -193,21 +189,26 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow):
except ValueError:
errors["query"] = "query_invalid"
else:
new_user_input = user_input
if new_user_input.get(CONF_DB_URL) and db_url == db_url_for_validation:
new_user_input.pop(CONF_DB_URL)
recorder_db = get_instance(self.hass).db_url
_LOGGER.debug(
"db_url: %s, resolved db_url: %s, recorder: %s",
db_url,
db_url_for_validation,
recorder_db,
)
if db_url and db_url_for_validation == recorder_db:
user_input.pop(CONF_DB_URL)
return self.async_create_entry(
title="",
data={
CONF_NAME: name,
**new_user_input,
**user_input,
},
)
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
OPTIONS_SCHEMA, user_input or self.entry.options
OPTIONS_SCHEMA, user_input or self.options
),
errors=errors,
description_placeholders=description_placeholders,
+1 -1
View File
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sql",
"iot_class": "local_polling",
"requirements": ["sqlalchemy==2.0.11"]
"requirements": ["sqlalchemy==2.0.12"]
}
+2 -7
View File
@@ -42,20 +42,15 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_COLUMN_NAME, CONF_QUERY, DB_URL_RE, DOMAIN
from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .models import SQLData
from .util import resolve_db_url
from .util import redact_credentials, resolve_db_url
_LOGGER = logging.getLogger(__name__)
_SQL_LAMBDA_CACHE: LRUCache = LRUCache(1000)
def redact_credentials(data: str) -> str:
"""Redact credentials from string data."""
return DB_URL_RE.sub("//****:****@", data)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
+14
View File
@@ -1,12 +1,26 @@
"""Utils for sql."""
from __future__ import annotations
import logging
from homeassistant.components.recorder import get_instance
from homeassistant.core import HomeAssistant
from .const import DB_URL_RE
_LOGGER = logging.getLogger(__name__)
def redact_credentials(data: str | None) -> str:
"""Redact credentials from string data."""
if not data:
return "none"
return DB_URL_RE.sub("//****:****@", data)
def resolve_db_url(hass: HomeAssistant, db_url: str | None) -> str:
"""Return the db_url provided if not empty, otherwise return the recorder db_url."""
_LOGGER.debug("db_url: %s", redact_credentials(db_url))
if db_url and not db_url.isspace():
return db_url
return get_instance(hass).db_url
+27 -10
View File
@@ -14,7 +14,7 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN
from .const import CONF_SIP_PORT, DOMAIN
from .devices import VoIPDevices
from .voip import HassVoipDatagramProtocol
@@ -39,6 +39,7 @@ class DomainData:
"""Domain data."""
transport: asyncio.DatagramTransport
protocol: HassVoipDatagramProtocol
devices: VoIPDevices
@@ -56,41 +57,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, data={**entry.data, "user": voip_user.id}
)
sip_port = entry.options.get(CONF_SIP_PORT, SIP_PORT)
devices = VoIPDevices(hass, entry)
devices.async_setup()
transport = await _create_sip_server(
transport, protocol = await _create_sip_server(
hass,
lambda: HassVoipDatagramProtocol(hass, devices),
sip_port,
)
_LOGGER.debug("Listening for VoIP calls on port %s", SIP_PORT)
_LOGGER.debug("Listening for VoIP calls on port %s", sip_port)
hass.data[DOMAIN] = DomainData(transport, devices)
hass.data[DOMAIN] = DomainData(transport, protocol, devices)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def _create_sip_server(
hass: HomeAssistant,
protocol_factory: Callable[
[],
asyncio.DatagramProtocol,
],
) -> asyncio.DatagramTransport:
transport, _protocol = await hass.loop.create_datagram_endpoint(
sip_port: int,
) -> tuple[asyncio.DatagramTransport, HassVoipDatagramProtocol]:
transport, protocol = await hass.loop.create_datagram_endpoint(
protocol_factory,
local_addr=(_IP_WILDCARD, SIP_PORT),
local_addr=(_IP_WILDCARD, sip_port),
)
return transport
if not isinstance(protocol, HassVoipDatagramProtocol):
raise TypeError(f"Expected HassVoipDatagramProtocol, got {protocol}")
return transport, protocol
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload VoIP."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
_LOGGER.debug("Shut down VoIP server")
hass.data.pop(DOMAIN).transport.close()
_LOGGER.debug("Shutting down VoIP server")
data = hass.data.pop(DOMAIN)
data.transport.close()
await data.protocol.wait_closed()
_LOGGER.debug("VoIP server shut down successfully")
return unload_ok
+49 -4
View File
@@ -3,10 +3,15 @@ from __future__ import annotations
from typing import Any
from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from voip_utils import SIP_PORT
import voluptuous as vol
from .const import DOMAIN
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv
from .const import CONF_SIP_PORT, DOMAIN
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -22,9 +27,49 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="single_instance_allowed")
if user_input is None:
return self.async_show_form(step_id="user")
return self.async_show_form(
step_id="user",
)
return self.async_create_entry(
title="Voice over IP",
data=user_input,
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return VoipOptionsFlowHandler(config_entry)
class VoipOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle VoIP options."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_SIP_PORT,
default=self.config_entry.options.get(
CONF_SIP_PORT,
SIP_PORT,
),
): cv.port
}
),
)
+2
View File
@@ -11,3 +11,5 @@ RTP_AUDIO_SETTINGS = {
"channels": CHANNELS,
"sleep_ratio": 0.99,
}
CONF_SIP_PORT = "sip_port"
+1
View File
@@ -139,6 +139,7 @@ class VoIPDevices:
manufacturer=manuf,
model=model,
sw_version=fw_version,
configuration_url=f"http://{call_info.caller_ip}",
)
voip_device = self.devices[voip_id] = VoIPDevice(
voip_id=voip_id,
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/voip",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["voip-utils==0.0.5"]
"requirements": ["voip-utils==0.0.7"]
}
@@ -28,5 +28,14 @@
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"sip_port": "SIP port"
}
}
}
}
}
+44 -28
View File
@@ -52,7 +52,11 @@ def make_protocol(
or (pipeline.tts_engine is None)
):
# Play pre-recorded message instead of failing
return PreRecordMessageProtocol(hass, "problem.pcm")
return PreRecordMessageProtocol(
hass,
"problem.pcm",
opus_payload_type=call_info.opus_payload_type,
)
# Pipeline is properly configured
return PipelineRtpDatagramProtocol(
@@ -60,6 +64,7 @@ def make_protocol(
hass.config.language,
voip_device,
Context(user_id=devices.config_entry.data["user"]),
opus_payload_type=call_info.opus_payload_type,
)
@@ -79,17 +84,28 @@ 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
self.devices = devices
self._closed_event = asyncio.Event()
def is_valid_call(self, call_info: CallInfo) -> bool:
"""Filter calls."""
device = self.devices.async_get_or_create(call_info)
return device.async_allow_call(self.hass)
def connection_lost(self, exc):
"""Signal wait_closed when transport is completely closed."""
self.hass.loop.call_soon_threadsafe(self._closed_event.set)
async def wait_closed(self) -> None:
"""Wait for connection_lost to be called."""
await self._closed_event.wait()
class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
"""Run a voice assistant pipeline in a loop for a VoIP call."""
@@ -100,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,
@@ -110,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
@@ -341,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
@@ -351,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:
@@ -360,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:
@@ -378,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."""
@@ -395,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)."""
@@ -415,11 +425,17 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
self,
hass: HomeAssistant,
file_name: str,
opus_payload_type: int,
message_delay: float = 1.0,
loop_delay: float = 2.0,
) -> None:
"""Set up RTP server."""
super().__init__(rate=RATE, width=WIDTH, channels=CHANNELS)
super().__init__(
rate=RATE,
width=WIDTH,
channels=CHANNELS,
opus_payload_type=opus_payload_type,
)
self.hass = hass
self.file_name = file_name
self.message_delay = message_delay
@@ -117,6 +117,7 @@ class WindowCoveringClient(ClientClusterHandler):
"""Window client cluster handler."""
@registries.BINDABLE_CLUSTERS.register(closures.WindowCovering.cluster_id)
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(closures.WindowCovering.cluster_id)
class WindowCovering(ClusterHandler):
"""Window cluster handler."""
+1 -1
View File
@@ -23,7 +23,7 @@
"bellows==0.35.2",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.98",
"zha-quirks==0.0.99",
"zigpy-deconz==0.21.0",
"zigpy==0.55.0",
"zigpy-xbee==0.18.0",
+15 -7
View File
@@ -248,13 +248,16 @@ class Battery(Sensor):
return state_attrs
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
stop_on_match_group=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
models={"VZM31-SN", "SP 234", "outletv4"},
)
class ElectricalMeasurement(Sensor):
"""Active power measurement."""
SENSOR_ATTR = "active_power"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER
_attr_should_poll = True # BaseZhaEntity defaults to False
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_name: str = "Active power"
_attr_native_unit_of_measurement: str = UnitOfPower.WATT
@@ -284,6 +287,16 @@ class ElectricalMeasurement(Sensor):
return round(value, self._decimals)
return round(value)
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
stop_on_match_group=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
)
class PolledElectricalMeasurement(ElectricalMeasurement):
"""Polled active power measurement."""
_attr_should_poll = True # BaseZhaEntity defaults to False
async def async_update(self) -> None:
"""Retrieve latest state."""
if not self.available:
@@ -299,7 +312,6 @@ class ElectricalMeasurementApparentPower(
SENSOR_ATTR = "apparent_power"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.APPARENT_POWER
_attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor
_attr_name: str = "Apparent power"
_attr_native_unit_of_measurement = UnitOfApparentPower.VOLT_AMPERE
_div_mul_prefix = "ac_power"
@@ -311,7 +323,6 @@ class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_curr
SENSOR_ATTR = "rms_current"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT
_attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor
_attr_name: str = "RMS current"
_attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
_div_mul_prefix = "ac_current"
@@ -323,7 +334,6 @@ class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_volt
SENSOR_ATTR = "rms_voltage"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLTAGE
_attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor
_attr_name: str = "RMS voltage"
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
_div_mul_prefix = "ac_voltage"
@@ -335,7 +345,6 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_freque
SENSOR_ATTR = "ac_frequency"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY
_attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor
_attr_name: str = "AC frequency"
_attr_native_unit_of_measurement = UnitOfFrequency.HERTZ
_div_mul_prefix = "ac_frequency"
@@ -347,7 +356,6 @@ class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_f
SENSOR_ATTR = "power_factor"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR
_attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor
_attr_name: str = "Power factor"
_attr_native_unit_of_measurement = PERCENTAGE
+36 -30
View File
@@ -51,7 +51,7 @@ async def async_setup_entry(
entities: list[ZWaveBaseEntity] = []
if info.platform_hint == "motorized_barrier":
entities.append(ZwaveMotorizedBarrier(config_entry, driver, info))
elif info.platform_hint == "window_shutter_tilt":
elif info.platform_hint and info.platform_hint.endswith("tilt"):
entities.append(ZWaveTiltCover(config_entry, driver, info))
else:
entities.append(ZWaveCover(config_entry, driver, info))
@@ -99,6 +99,12 @@ def zwave_tilt_to_percent(value: int) -> int:
class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave Cover device."""
_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)
def __init__(
self,
config_entry: ConfigEntry,
@@ -108,11 +114,20 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Initialize a ZWaveCover entity."""
super().__init__(config_entry, driver, info)
self._stop_cover_value = (
self.get_zwave_value(COVER_OPEN_PROPERTY)
or self.get_zwave_value(COVER_UP_PROPERTY)
or self.get_zwave_value(COVER_ON_PROPERTY)
)
if self._stop_cover_value:
self._attr_supported_features |= CoverEntityFeature.STOP
# Entity class attributes
self._attr_device_class = CoverDeviceClass.WINDOW
if self.info.platform_hint in ("window_shutter", "window_shutter_tilt"):
if self.info.platform_hint and self.info.platform_hint.startswith("shutter"):
self._attr_device_class = CoverDeviceClass.SHUTTER
if self.info.platform_hint == "window_blind":
if self.info.platform_hint and self.info.platform_hint.startswith("blind"):
self._attr_device_class = CoverDeviceClass.BLIND
@property
@@ -153,28 +168,13 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop cover."""
cover_property = (
self.get_zwave_value(COVER_OPEN_PROPERTY)
or self.get_zwave_value(COVER_UP_PROPERTY)
or self.get_zwave_value(COVER_ON_PROPERTY)
)
if cover_property:
# Stop the cover, will stop regardless of the actual direction of travel.
await self.info.node.async_set_value(cover_property, False)
assert self._stop_cover_value
# Stop the cover, will stop regardless of the actual direction of travel.
await self.info.node.async_set_value(self._stop_cover_value, False)
class ZWaveTiltCover(ZWaveCover):
"""Representation of a Z-Wave Cover device with tilt."""
_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.STOP
| CoverEntityFeature.SET_POSITION
| CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
"""Representation of a Z-Wave cover device with tilt."""
def __init__(
self,
@@ -184,8 +184,15 @@ class ZWaveTiltCover(ZWaveCover):
) -> None:
"""Initialize a ZWaveCover entity."""
super().__init__(config_entry, driver, info)
self.data_template = cast(
self._current_tilt_value = cast(
CoverTiltDataTemplate, self.info.platform_data_template
).current_tilt_value(self.info.platform_data)
self._attr_supported_features |= (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
@property
@@ -194,19 +201,18 @@ class ZWaveTiltCover(ZWaveCover):
None is unknown, 0 is closed, 100 is fully open.
"""
value = self.data_template.current_tilt_value(self.info.platform_data)
value = self._current_tilt_value
if value is None or value.value is None:
return None
return zwave_tilt_to_percent(int(value.value))
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
tilt_value = self.data_template.current_tilt_value(self.info.platform_data)
if tilt_value:
await self.info.node.async_set_value(
tilt_value,
percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]),
)
assert self._current_tilt_value
await self.info.node.async_set_value(
self._current_tilt_value,
percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]),
)
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
@@ -347,7 +347,7 @@ DISCOVERY_SCHEMAS = [
# Fibaro Shutter Fibaro FGR222
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_shutter_tilt",
hint="shutter_tilt",
manufacturer_id={0x010F},
product_id={0x1000, 0x1001},
product_type={0x0301, 0x0302},
@@ -371,7 +371,7 @@ DISCOVERY_SCHEMAS = [
# Qubino flush shutter
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_shutter",
hint="shutter",
manufacturer_id={0x0159},
product_id={0x0052, 0x0053},
product_type={0x0003},
@@ -380,7 +380,7 @@ DISCOVERY_SCHEMAS = [
# Graber/Bali/Spring Fashion Covers
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_blind",
hint="blind",
manufacturer_id={0x026E},
product_id={0x5A31},
product_type={0x4353},
@@ -389,7 +389,7 @@ DISCOVERY_SCHEMAS = [
# iBlinds v2 window blind motor
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_blind",
hint="blind",
manufacturer_id={0x0287},
product_id={0x000D},
product_type={0x0003},
@@ -398,7 +398,7 @@ DISCOVERY_SCHEMAS = [
# Merten 507801 Connect Roller Shutter
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_shutter",
hint="shutter",
manufacturer_id={0x007A},
product_id={0x0001},
product_type={0x8003},
@@ -414,7 +414,7 @@ DISCOVERY_SCHEMAS = [
# Disable endpoint 2, as it has no practical function. CC: Switch_Multilevel
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_shutter",
hint="shutter",
manufacturer_id={0x007A},
product_id={0x0001},
product_type={0x8003},
@@ -807,7 +807,7 @@ DISCOVERY_SCHEMAS = [
# window coverings
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="window_cover",
hint="cover",
device_class_generic={"Multilevel Switch"},
device_class_specific={
"Motor Control Class A",
+1 -1
View File
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "0b4"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
-20
View File
@@ -307,26 +307,6 @@ class RegistryEntry:
hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs)
def async_friendly_name(self, hass: HomeAssistant) -> str | None:
"""Return the friendly name.
If self.name is not None, this returns self.name
If has_entity_name is False, self.original_name
If has_entity_name is True, this returns device.name + self.original_name
"""
if not self.has_entity_name or self.name is not None:
return self.name or self.original_name
device_registry = dr.async_get(hass)
if not (device_id := self.device_id) or not (
device_entry := device_registry.async_get(device_id)
):
return self.original_name
if not (original_name := self.original_name):
return device_entry.name_by_user or device_entry.name
return f"{device_entry.name_by_user or device_entry.name} {original_name}"
class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
"""Store entity registry data."""
+23 -1
View File
@@ -11,6 +11,7 @@ from typing import Any, TypeVar
import voluptuous as vol
from homeassistant.components.homeassistant.exposed_entities import async_should_expose
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
@@ -65,6 +66,7 @@ async def async_handle(
text_input: str | None = None,
context: Context | None = None,
language: str | None = None,
assistant: str | None = None,
) -> IntentResponse:
"""Handle an intent."""
handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type)
@@ -79,7 +81,14 @@ async def async_handle(
language = hass.config.language
intent = Intent(
hass, platform, intent_type, slots or {}, text_input, context, language
hass,
platform=platform,
intent_type=intent_type,
slots=slots or {},
text_input=text_input,
context=context,
language=language,
assistant=assistant,
)
try:
@@ -208,6 +217,7 @@ def async_match_states(
entities: entity_registry.EntityRegistry | None = None,
areas: area_registry.AreaRegistry | None = None,
devices: device_registry.DeviceRegistry | None = None,
assistant: str | None = None,
) -> Iterable[State]:
"""Find states that match the constraints."""
if states is None:
@@ -258,6 +268,14 @@ def async_match_states(
states_and_entities = list(_filter_by_area(states_and_entities, area, devices))
if assistant is not None:
# Filter by exposure
states_and_entities = [
(state, entity)
for state, entity in states_and_entities
if async_should_expose(hass, assistant, state.entity_id)
]
if name is not None:
if devices is None:
devices = device_registry.async_get(hass)
@@ -387,6 +405,7 @@ class ServiceIntentHandler(IntentHandler):
area=area,
domains=domains,
device_classes=device_classes,
assistant=intent_obj.assistant,
)
)
@@ -496,6 +515,7 @@ class Intent:
"context",
"language",
"category",
"assistant",
]
def __init__(
@@ -508,6 +528,7 @@ class Intent:
context: Context,
language: str,
category: IntentCategory | None = None,
assistant: str | None = None,
) -> None:
"""Initialize an intent."""
self.hass = hass
@@ -518,6 +539,7 @@ class Intent:
self.context = context
self.language = language
self.category = category
self.assistant = assistant
@callback
def create_response(self) -> IntentResponse:
+8 -5
View File
@@ -12,9 +12,9 @@ attrs==22.2.0
awesomeversion==22.9.0
bcrypt==4.0.1
bleak-retry-connector==3.0.2
bleak==0.20.1
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==20230428.0
home-assistant-frontend==20230503.2
home-assistant-intents==2023.4.26
httpx==0.24.0
ifaddr==0.1.7
@@ -45,9 +45,9 @@ pyudev==0.23.2
pyyaml==6.0
requests==2.28.2
scapy==2.5.0
sqlalchemy==2.0.11
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
@@ -176,3 +176,6 @@ websockets>=11.0.1
# https://github.com/pysnmp/pysnmp/issues/51
pyasn1==0.4.8
pysnmplib==5.0.21
# pysnmp is no longer maintained and does not work with newer
# python
pysnmp==1000000000.0.0
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.5.0b4"
version = "2023.5.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@@ -50,7 +50,7 @@ dependencies = [
"pyyaml==6.0",
"requests==2.28.2",
"typing-extensions>=4.5.0,<5.0",
"ulid-transform==0.7.0",
"ulid-transform==0.7.2",
"voluptuous==0.13.1",
"voluptuous-serialize==2.6.0",
"yarl==1.9.2",
+1 -1
View File
@@ -24,7 +24,7 @@ python-slugify==4.0.1
pyyaml==6.0
requests==2.28.2
typing-extensions>=4.5.0,<5.0
ulid-transform==0.7.0
ulid-transform==0.7.2
voluptuous==0.13.1
voluptuous-serialize==2.6.0
yarl==1.9.2
+13 -13
View File
@@ -223,7 +223,7 @@ aionanoleaf==0.2.1
aionotify==0.2.0
# homeassistant.components.notion
aionotion==2023.04.2
aionotion==2023.05.0
# homeassistant.components.oncue
aiooncue==0.3.4
@@ -386,7 +386,7 @@ asyncpysupla==0.0.5
asyncsleepiq==1.3.4
# homeassistant.components.aten_pe
atenpdu==0.3.2
# atenpdu==0.3.2
# homeassistant.components.aurora
auroranoaa==0.0.3
@@ -419,7 +419,7 @@ base36==0.1.1
batinfo==0.4.2
# homeassistant.components.eddystone_temperature
# beacontools[scan]==1.2.3
# beacontools[scan]==2.1.0
# homeassistant.components.scrape
beautifulsoup4==4.11.1
@@ -440,7 +440,7 @@ bizkaibus==0.1.1
bleak-retry-connector==3.0.2
# homeassistant.components.bluetooth
bleak==0.20.1
bleak==0.20.2
# homeassistant.components.blebox
blebox_uniapi==2.1.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==20230428.0
home-assistant-frontend==20230503.2
# homeassistant.components.conversation
home-assistant-intents==2023.4.26
@@ -1264,7 +1264,7 @@ ondilo==0.2.0
onkyo-eiscp==1.2.7
# homeassistant.components.onvif
onvif-zeep-async==1.3.0
onvif-zeep-async==1.3.1
# homeassistant.components.opengarage
open-garage==0.2.0
@@ -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
@@ -1812,7 +1812,7 @@ pynetgear==0.10.9
pynetio==0.1.9.1
# homeassistant.components.nina
pynina==0.2.0
pynina==0.3.0
# homeassistant.components.nobo_hub
pynobo==1.6.0
@@ -2406,7 +2406,7 @@ spotipy==2.23.0
# homeassistant.components.recorder
# homeassistant.components.sql
sqlalchemy==2.0.11
sqlalchemy==2.0.12
# homeassistant.components.srp_energy
srpenergy==1.3.6
@@ -2594,7 +2594,7 @@ venstarcolortouch==0.19
vilfo-api-client==0.3.2
# homeassistant.components.voip
voip-utils==0.0.5
voip-utils==0.0.7
# homeassistant.components.volkszaehler
volkszaehler==0.4.0
@@ -2688,7 +2688,7 @@ yalesmartalarmclient==0.3.9
yalexs-ble==2.1.16
# homeassistant.components.august
yalexs==1.3.2
yalexs==1.3.3
# homeassistant.components.yeelight
yeelight==0.7.10
@@ -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
+11 -11
View File
@@ -204,7 +204,7 @@ aiomusiccast==0.14.8
aionanoleaf==0.2.1
# homeassistant.components.notion
aionotion==2023.04.2
aionotion==2023.05.0
# homeassistant.components.oncue
aiooncue==0.3.4
@@ -370,7 +370,7 @@ bimmer_connected==0.13.2
bleak-retry-connector==3.0.2
# homeassistant.components.bluetooth
bleak==0.20.1
bleak==0.20.2
# homeassistant.components.blebox
blebox_uniapi==2.1.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==20230428.0
home-assistant-frontend==20230503.2
# homeassistant.components.conversation
home-assistant-intents==2023.4.26
@@ -945,7 +945,7 @@ omnilogic==0.4.5
ondilo==0.2.0
# homeassistant.components.onvif
onvif-zeep-async==1.3.0
onvif-zeep-async==1.3.1
# homeassistant.components.opengarage
open-garage==0.2.0
@@ -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
@@ -1316,7 +1316,7 @@ pymysensors==0.24.0
pynetgear==0.10.9
# homeassistant.components.nina
pynina==0.2.0
pynina==0.3.0
# homeassistant.components.nobo_hub
pynobo==1.6.0
@@ -1730,7 +1730,7 @@ spotipy==2.23.0
# homeassistant.components.recorder
# homeassistant.components.sql
sqlalchemy==2.0.11
sqlalchemy==2.0.12
# homeassistant.components.srp_energy
srpenergy==1.3.6
@@ -1870,7 +1870,7 @@ venstarcolortouch==0.19
vilfo-api-client==0.3.2
# homeassistant.components.voip
voip-utils==0.0.5
voip-utils==0.0.7
# homeassistant.components.volvooncall
volvooncall==0.10.2
@@ -1943,7 +1943,7 @@ yalesmartalarmclient==0.3.9
yalexs-ble==2.1.16
# homeassistant.components.august
yalexs==1.3.2
yalexs==1.3.3
# homeassistant.components.yeelight
yeelight==0.7.10
@@ -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
+4
View File
@@ -21,6 +21,7 @@ else:
COMMENT_REQUIREMENTS = (
"Adafruit_BBIO",
"atenpdu", # depends on pysnmp which is not maintained at this time
"avea", # depends on bluepy
"avion",
"azure-servicebus", # depends on uamqp, which requires OpenSSL 1.1
@@ -180,6 +181,9 @@ websockets>=11.0.1
# https://github.com/pysnmp/pysnmp/issues/51
pyasn1==0.4.8
pysnmplib==5.0.21
# pysnmp is no longer maintained and does not work with newer
# python
pysnmp==1000000000.0.0
"""
IGNORE_PRE_COMMIT_HOOK_ID = (
@@ -1,4 +1,6 @@
"""The tests for the Air Quality component."""
import pytest
from homeassistant.components.air_quality import ATTR_N2O, ATTR_OZONE, ATTR_PM_10
from homeassistant.const import (
ATTR_ATTRIBUTION,
@@ -9,6 +11,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
async def test_state(hass: HomeAssistant) -> None:
"""Test Air Quality state."""
config = {"air_quality": {"platform": "demo"}}
@@ -39,6 +39,7 @@ def events(hass: HomeAssistant) -> list[Event]:
@pytest.fixture
async def mock_camera(hass: HomeAssistant) -> None:
"""Initialize a demo camera platform."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
)
+1
View File
@@ -1539,6 +1539,7 @@ async def test_automation_restore_last_triggered_with_initial_state(
async def test_extraction_functions(hass: HomeAssistant) -> None:
"""Test extraction functions."""
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
assert await async_setup_component(
hass,
+11
View File
@@ -0,0 +1,11 @@
"""Test fixtures for calendar sensor platforms."""
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@@ -1,6 +1,8 @@
"""The tests for calendar recorder."""
from datetime import timedelta
import pytest
from homeassistant.components.recorder import Recorder
from homeassistant.components.recorder.history import get_significant_states
from homeassistant.const import ATTR_FRIENDLY_NAME
@@ -12,9 +14,15 @@ from tests.common import async_fire_time_changed
from tests.components.recorder.common import async_wait_recording_done
@pytest.fixture(autouse=True)
async def setup_homeassistant():
"""Override the fixture in calendar.conftest."""
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test sensor attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
await hass.async_block_till_done()
+7
View File
@@ -5,11 +5,18 @@ import pytest
from homeassistant.components import camera
from homeassistant.components.camera.const import StreamType
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .common import WEBRTC_ANSWER
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture(name="mock_camera")
async def mock_camera_fixture(hass):
"""Initialize a demo camera platform."""
+1
View File
@@ -370,6 +370,7 @@ async def test_websocket_update_orientation_prefs(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera
) -> None:
"""Test updating camera preferences."""
await async_setup_component(hass, "homeassistant", {})
client = await hass_ws_client(hass)
+8
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from datetime import timedelta
import pytest
from homeassistant.components import camera
from homeassistant.components.recorder import Recorder
from homeassistant.components.recorder.history import get_significant_states
@@ -20,9 +22,15 @@ from tests.common import async_fire_time_changed
from tests.components.recorder.common import async_wait_recording_done
@pytest.fixture(autouse=True)
async def setup_homeassistant():
"""Override the fixture in calendar.conftest."""
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test camera registered attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}}
)
@@ -1888,6 +1888,7 @@ async def test_failed_cast_other_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test warning when casting from internal_url fails."""
await async_setup_component(hass, "homeassistant", {})
with assert_setup_component(1, tts.DOMAIN):
assert await async_setup_component(
hass,
@@ -1911,6 +1912,7 @@ async def test_failed_cast_internal_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test warning when casting from internal_url fails."""
await async_setup_component(hass, "homeassistant", {})
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
@@ -1939,6 +1941,7 @@ async def test_failed_cast_external_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test warning when casting from external_url fails."""
await async_setup_component(hass, "homeassistant", {})
await async_process_ha_core_config(
hass,
{"external_url": "http://example.com:8123"},
@@ -1969,6 +1972,7 @@ async def test_failed_cast_tts_base_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test warning when casting from tts.base_url fails."""
await async_setup_component(hass, "homeassistant", {})
with assert_setup_component(1, tts.DOMAIN):
assert await async_setup_component(
hass,
@@ -29,6 +29,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test climate registered attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, climate.DOMAIN, {climate.DOMAIN: {"platform": "demo"}}
)
+73 -46
View File
@@ -15,10 +15,15 @@ from homeassistant.components.cloud.prefs import CloudPreferences
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
async_expose_entity,
async_get_entity_settings,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED,
EntityCategory,
)
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.setup import async_setup_component
@@ -41,8 +46,7 @@ def expose_new(hass, expose_new):
def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to Alexa."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity("cloud.alexa", entity_id, should_expose)
async_expose_entity(hass, "cloud.alexa", entity_id, should_expose)
async def test_alexa_config_expose_entity_prefs(
@@ -102,10 +106,9 @@ async def test_alexa_config_expose_entity_prefs(
)
await conf.async_initialize()
# can't expose an entity which is not in the entity registry
with pytest.raises(HomeAssistantError):
expose_entity(hass, "light.kitchen", True)
assert not conf.should_expose("light.kitchen")
# an entity which is not in the entity registry can be exposed
expose_entity(hass, "light.kitchen", True)
assert conf.should_expose("light.kitchen")
# categorized and hidden entities should not be exposed
assert not conf.should_expose(entity_entry1.entity_id)
assert not conf.should_expose(entity_entry2.entity_id)
@@ -368,6 +371,8 @@ async def test_alexa_update_expose_trigger_sync(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
with patch_sync_helper() as (to_update, to_remove):
expose_entity(hass, light_entry.entity_id, True)
@@ -544,8 +549,10 @@ async def test_alexa_config_migrate_expose_entity_prefs(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.state_only", "on")
entity_exposed = entity_registry.async_get_or_create(
"light",
"test",
@@ -593,6 +600,9 @@ async def test_alexa_config_migrate_expose_entity_prefs(
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.unknown"] = {
PREF_SHOULD_EXPOSE: True
}
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.state_only"] = {
PREF_SHOULD_EXPOSE: False
}
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_exposed.entity_id] = {
PREF_SHOULD_EXPOSE: True
}
@@ -603,21 +613,32 @@ async def test_alexa_config_migrate_expose_entity_prefs(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
entity_exposed = entity_registry.async_get(entity_exposed.entity_id)
assert entity_exposed.options == {"cloud.alexa": {"should_expose": True}}
entity_migrated = entity_registry.async_get(entity_migrated.entity_id)
assert entity_migrated.options == {"cloud.alexa": {"should_expose": False}}
entity_config = entity_registry.async_get(entity_config.entity_id)
assert entity_config.options == {"cloud.alexa": {"should_expose": False}}
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.alexa": {"should_expose": True}}
entity_blocked = entity_registry.async_get(entity_blocked.entity_id)
assert entity_blocked.options == {"cloud.alexa": {"should_expose": False}}
assert async_get_entity_settings(hass, "light.unknown") == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, "light.state_only") == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_exposed.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_migrated.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_config.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_default.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_blocked.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
async def test_alexa_config_migrate_expose_entity_prefs_default_none(
@@ -627,6 +648,7 @@ async def test_alexa_config_migrate_expose_entity_prefs_default_none(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
entity_default = entity_registry.async_get_or_create(
@@ -647,9 +669,14 @@ async def test_alexa_config_migrate_expose_entity_prefs_default_none(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.alexa": {"should_expose": True}}
assert async_get_entity_settings(hass, entity_default.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
async def test_alexa_config_migrate_expose_entity_prefs_default(
@@ -659,6 +686,7 @@ async def test_alexa_config_migrate_expose_entity_prefs_default(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
@@ -724,27 +752,26 @@ async def test_alexa_config_migrate_expose_entity_prefs_default(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
binary_sensor_supported = entity_registry.async_get(
binary_sensor_supported.entity_id
)
assert binary_sensor_supported.options == {"cloud.alexa": {"should_expose": True}}
binary_sensor_unsupported = entity_registry.async_get(
binary_sensor_unsupported.entity_id
)
assert binary_sensor_unsupported.options == {
assert async_get_entity_settings(hass, binary_sensor_supported.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, binary_sensor_unsupported.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, light.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, sensor_supported.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, sensor_unsupported.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, water_heater.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
light = entity_registry.async_get(light.entity_id)
assert light.options == {"cloud.alexa": {"should_expose": True}}
sensor_supported = entity_registry.async_get(sensor_supported.entity_id)
assert sensor_supported.options == {"cloud.alexa": {"should_expose": True}}
sensor_unsupported = entity_registry.async_get(sensor_unsupported.entity_id)
assert sensor_unsupported.options == {"cloud.alexa": {"should_expose": False}}
water_heater = entity_registry.async_get(water_heater.entity_id)
assert water_heater.options == {"cloud.alexa": {"should_expose": False}}
+2 -3
View File
@@ -16,6 +16,7 @@ from homeassistant.components.cloud.const import (
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
async_expose_entity,
)
from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant, State
@@ -267,9 +268,7 @@ async def test_google_config_expose_entity(
assert gconf.should_expose(state)
exposed_entities.async_expose_entity(
"cloud.google_assistant", entity_entry.entity_id, False
)
async_expose_entity(hass, "cloud.google_assistant", entity_entry.entity_id, False)
assert not gconf.should_expose(state)
+67 -53
View File
@@ -18,10 +18,15 @@ from homeassistant.components.google_assistant import helpers as ga_helpers
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
async_expose_entity,
async_get_entity_settings,
)
from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED,
EntityCategory,
)
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EntityCategory
from homeassistant.core import CoreState, HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -49,10 +54,7 @@ def expose_new(hass, expose_new):
def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to Google."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity(
"cloud.google_assistant", entity_id, should_expose
)
async_expose_entity(hass, "cloud.google_assistant", entity_id, should_expose)
async def test_google_update_report_state(
@@ -146,6 +148,8 @@ async def test_google_update_expose_trigger_sync(
Mock(claims={"cognito:username": "abcdefghjkl"}),
)
await config.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
await config.async_connect_agent_user("mock-user-id")
with patch.object(config, "async_sync_entities") as mock_sync, patch.object(
@@ -395,10 +399,9 @@ async def test_google_config_expose_entity_prefs(
state_not_exposed = State(entity_entry5.entity_id, "on")
state_exposed_default = State(entity_entry6.entity_id, "on")
# can't expose an entity which is not in the entity registry
with pytest.raises(HomeAssistantError):
expose_entity(hass, "light.kitchen", True)
assert not mock_conf.should_expose(state)
# an entity which is not in the entity registry can be exposed
expose_entity(hass, "light.kitchen", True)
assert mock_conf.should_expose(state)
# categorized and hidden entities should not be exposed
assert not mock_conf.should_expose(state_config)
assert not mock_conf.should_expose(state_diagnostic)
@@ -486,8 +489,10 @@ async def test_google_config_migrate_expose_entity_prefs(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Google entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.state_only", "on")
entity_exposed = entity_registry.async_get_or_create(
"light",
"test",
@@ -540,7 +545,11 @@ async def test_google_config_migrate_expose_entity_prefs(
expose_entity(hass, entity_migrated.entity_id, False)
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.unknown"] = {
PREF_SHOULD_EXPOSE: True
PREF_SHOULD_EXPOSE: True,
PREF_DISABLE_2FA: True,
}
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.state_only"] = {
PREF_SHOULD_EXPOSE: False
}
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_exposed.entity_id] = {
PREF_SHOULD_EXPOSE: True
@@ -556,28 +565,33 @@ async def test_google_config_migrate_expose_entity_prefs(
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
entity_exposed = entity_registry.async_get(entity_exposed.entity_id)
assert entity_exposed.options == {"cloud.google_assistant": {"should_expose": True}}
entity_migrated = entity_registry.async_get(entity_migrated.entity_id)
assert entity_migrated.options == {
"cloud.google_assistant": {"should_expose": False}
}
entity_no_2fa_exposed = entity_registry.async_get(entity_no_2fa_exposed.entity_id)
assert entity_no_2fa_exposed.options == {
assert async_get_entity_settings(hass, "light.unknown") == {
"cloud.google_assistant": {"disable_2fa": True, "should_expose": True}
}
entity_config = entity_registry.async_get(entity_config.entity_id)
assert entity_config.options == {"cloud.google_assistant": {"should_expose": False}}
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.google_assistant": {"should_expose": True}}
entity_blocked = entity_registry.async_get(entity_blocked.entity_id)
assert entity_blocked.options == {
assert async_get_entity_settings(hass, "light.state_only") == {
"cloud.google_assistant": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_exposed.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_migrated.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_no_2fa_exposed.entity_id) == {
"cloud.google_assistant": {"disable_2fa": True, "should_expose": True}
}
assert async_get_entity_settings(hass, entity_config.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_default.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_blocked.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
@@ -588,6 +602,7 @@ async def test_google_config_migrate_expose_entity_prefs_default_none(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Google entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
entity_default = entity_registry.async_get_or_create(
@@ -608,9 +623,14 @@ async def test_google_config_migrate_expose_entity_prefs_default_none(
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.google_assistant": {"should_expose": True}}
assert async_get_entity_settings(hass, entity_default.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
async def test_google_config_migrate_expose_entity_prefs_default(
@@ -619,6 +639,7 @@ async def test_google_config_migrate_expose_entity_prefs_default(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Google entity config."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
@@ -684,33 +705,26 @@ async def test_google_config_migrate_expose_entity_prefs_default(
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
binary_sensor_supported = entity_registry.async_get(
binary_sensor_supported.entity_id
)
assert binary_sensor_supported.options == {
assert async_get_entity_settings(hass, binary_sensor_supported.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
binary_sensor_unsupported = entity_registry.async_get(
binary_sensor_unsupported.entity_id
)
assert binary_sensor_unsupported.options == {
assert async_get_entity_settings(hass, binary_sensor_unsupported.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
light = entity_registry.async_get(light.entity_id)
assert light.options == {"cloud.google_assistant": {"should_expose": True}}
sensor_supported = entity_registry.async_get(sensor_supported.entity_id)
assert sensor_supported.options == {
assert async_get_entity_settings(hass, light.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
sensor_unsupported = entity_registry.async_get(sensor_unsupported.entity_id)
assert sensor_unsupported.options == {
assert async_get_entity_settings(hass, sensor_supported.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, sensor_unsupported.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
assert async_get_entity_settings(hass, water_heater.entity_id) == {
"cloud.google_assistant": {"should_expose": False}
}
water_heater = entity_registry.async_get(water_heater.entity_id)
assert water_heater.options == {"cloud.google_assistant": {"should_expose": False}}
+60 -17
View File
@@ -14,6 +14,7 @@ from homeassistant.components.alexa import errors as alexa_errors
from homeassistant.components.alexa.entities import LightCapabilities
from homeassistant.components.cloud.const import DOMAIN
from homeassistant.components.google_assistant.helpers import GoogleEntity
from homeassistant.components.homeassistant import exposed_entities
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
@@ -761,7 +762,17 @@ async def test_list_google_entities(
await client.send_json_auto_id({"type": "cloud/google_assistant/entities"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]) == 0
assert len(response["result"]) == 2
assert response["result"][0] == {
"entity_id": "light.kitchen",
"might_2fa": False,
"traits": ["action.devices.traits.OnOff"],
}
assert response["result"][1] == {
"entity_id": "cover.garage",
"might_2fa": True,
"traits": ["action.devices.traits.OpenClose"],
}
# Add the entities to the entity registry
entity_registry.async_get_or_create(
@@ -809,7 +820,7 @@ async def test_get_google_entity(
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "light.kitchen unknown or not in the entity registry",
"message": "light.kitchen unknown",
}
# Test getting a blocked entity
@@ -830,9 +841,6 @@ async def test_get_google_entity(
entity_registry.async_get_or_create(
"light", "test", "unique", suggested_object_id="kitchen"
)
entity_registry.async_get_or_create(
"cover", "test", "unique", suggested_object_id="garage"
)
hass.states.async_set("light.kitchen", "on")
hass.states.async_set("cover.garage", "open", {"device_class": "garage"})
@@ -842,6 +850,7 @@ async def test_get_google_entity(
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"disable_2fa": None,
"entity_id": "light.kitchen",
"might_2fa": False,
"traits": ["action.devices.traits.OnOff"],
@@ -853,6 +862,30 @@ async def test_get_google_entity(
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"disable_2fa": None,
"entity_id": "cover.garage",
"might_2fa": True,
"traits": ["action.devices.traits.OpenClose"],
}
# Set the disable 2fa flag
await client.send_json_auto_id(
{
"type": "cloud/google_assistant/entities/update",
"entity_id": "cover.garage",
"disable_2fa": True,
}
)
response = await client.receive_json()
assert response["success"]
await client.send_json_auto_id(
{"type": "cloud/google_assistant/entities/get", "entity_id": "cover.garage"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"disable_2fa": True,
"entity_id": "cover.garage",
"might_2fa": True,
"traits": ["action.devices.traits.OpenClose"],
@@ -867,9 +900,6 @@ async def test_update_google_entity(
mock_cloud_login,
) -> None:
"""Test that we can update config of a Google entity."""
entry = entity_registry.async_get_or_create(
"light", "test", "unique", suggested_object_id="kitchen"
)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
@@ -885,16 +915,16 @@ async def test_update_google_entity(
{
"type": "homeassistant/expose_entity",
"assistants": ["cloud.google_assistant"],
"entity_ids": [entry.entity_id],
"entity_ids": ["light.kitchen"],
"should_expose": False,
}
)
response = await client.receive_json()
assert response["success"]
assert entity_registry.async_get(entry.entity_id).options[
"cloud.google_assistant"
] == {"disable_2fa": False, "should_expose": False}
assert exposed_entities.async_get_entity_settings(hass, "light.kitchen") == {
"cloud.google_assistant": {"disable_2fa": False, "should_expose": False}
}
async def test_list_alexa_entities(
@@ -916,7 +946,12 @@ async def test_list_alexa_entities(
await client.send_json_auto_id({"id": 5, "type": "cloud/alexa/entities"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]) == 0
assert len(response["result"]) == 1
assert response["result"][0] == {
"entity_id": "light.kitchen",
"display_categories": ["LIGHT"],
"interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"],
}
# Add the entity to the entity registry
entity_registry.async_get_or_create(
@@ -953,10 +988,18 @@ async def test_get_alexa_entity(
{"type": "cloud/alexa/entities/get", "entity_id": "light.kitchen"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] is None
# Test getting an unknown sensor
await client.send_json_auto_id(
{"type": "cloud/alexa/entities/get", "entity_id": "sensor.temperature"}
)
response = await client.receive_json()
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "light.kitchen not in the entity registry",
"code": "not_supported",
"message": "sensor.temperature not supported by Alexa",
}
# Test getting a blocked entity
@@ -1022,8 +1065,8 @@ async def test_update_alexa_entity(
response = await client.receive_json()
assert response["success"]
assert entity_registry.async_get(entry.entity_id).options["cloud.alexa"] == {
"should_expose": False
assert exposed_entities.async_get_entity_settings(hass, entry.entity_id) == {
"cloud.alexa": {"should_expose": False}
}
@@ -32,6 +32,12 @@ LIGHT_ENTITY = "light.kitchen_lights"
CLOSE_THRESHOLD = 10
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
def _close_enough(actual_rgb, testing_rgb):
"""Validate the given RGB value is in acceptable tolerance."""
# Convert the given RGB values to hue / saturation and then back again
+2 -2
View File
@@ -7,6 +7,7 @@ from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
async_expose_entity,
)
from homeassistant.helpers import intent
@@ -53,5 +54,4 @@ def expose_new(hass, expose_new):
def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to the default agent."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity(conversation.DOMAIN, entity_id, should_expose)
async_expose_entity(hass, conversation.DOMAIN, entity_id, should_expose)
@@ -91,21 +91,21 @@ async def test_exposed_areas(
)
device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id)
kitchen_light = entity_registry.async_get_or_create(
"light", "demo", "1234", original_name="kitchen light"
)
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
entity_registry.async_update_entity(
kitchen_light.entity_id, device_id=kitchen_device.id
)
hass.states.async_set(kitchen_light.entity_id, "on")
bedroom_light = entity_registry.async_get_or_create(
"light", "demo", "5678", original_name="bedroom light"
hass.states.async_set(
kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
)
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
entity_registry.async_update_entity(
bedroom_light.entity_id, area_id=area_bedroom.id
)
hass.states.async_set(bedroom_light.entity_id, "on")
hass.states.async_set(
bedroom_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"}
)
# Hide the bedroom light
expose_entity(hass, bedroom_light.entity_id, False)
@@ -156,6 +156,8 @@ async def test_expose_flag_automatically_set(
assert await async_setup_component(hass, "conversation", {})
await hass.async_block_till_done()
with patch("homeassistant.components.http.start_http_server_and_save_config"):
await hass.async_start()
# After setting up conversation, the expose flag should now be set on all entities
assert async_get_assistant_settings(hass, conversation.DOMAIN) == {
@@ -164,10 +166,60 @@ async def test_expose_flag_automatically_set(
}
# New entities will automatically have the expose flag set
new_light = entity_registry.async_get_or_create("light", "demo", "2345")
new_light = "light.demo_2345"
hass.states.async_set(new_light, "test")
await hass.async_block_till_done()
assert async_get_assistant_settings(hass, conversation.DOMAIN) == {
light.entity_id: {"should_expose": True},
new_light.entity_id: {"should_expose": True},
new_light: {"should_expose": True},
test.entity_id: {"should_expose": False},
}
async def test_unexposed_entities_skipped(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that unexposed entities are skipped in exposed areas."""
area_kitchen = area_registry.async_get_or_create("kitchen")
# Both lights are in the kitchen
exposed_light = entity_registry.async_get_or_create("light", "demo", "1234")
entity_registry.async_update_entity(
exposed_light.entity_id,
area_id=area_kitchen.id,
)
hass.states.async_set(exposed_light.entity_id, "off")
unexposed_light = entity_registry.async_get_or_create("light", "demo", "5678")
entity_registry.async_update_entity(
unexposed_light.entity_id,
area_id=area_kitchen.id,
)
hass.states.async_set(unexposed_light.entity_id, "off")
# On light is exposed, the other is not
expose_entity(hass, exposed_light.entity_id, True)
expose_entity(hass, unexposed_light.entity_id, False)
# Only one light should be turned on
calls = async_mock_service(hass, "light", "turn_on")
result = await conversation.async_converse(
hass, "turn on kitchen lights", None, Context(), None
)
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
# Only one light should be returned
hass.states.async_set(exposed_light.entity_id, "on")
hass.states.async_set(unexposed_light.entity_id, "on")
result = await conversation.async_converse(
hass, "how many lights are on in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == exposed_light.entity_id
+8 -73
View File
@@ -202,11 +202,7 @@ async def test_http_processing_intent_entity_added_removed(
# Add an entity
entity_registry.async_get_or_create(
"light",
"demo",
"5678",
suggested_object_id="late",
original_name="friendly light",
"light", "demo", "5678", suggested_object_id="late"
)
hass.states.async_set("light.late", "off", {"friendly_name": "friendly light"})
@@ -274,7 +270,7 @@ async def test_http_processing_intent_entity_added_removed(
}
# Now delete the entity
entity_registry.async_remove("light.late")
hass.states.async_remove("light.late")
client = await hass_client()
resp = await client.post(
@@ -313,11 +309,7 @@ async def test_http_processing_intent_alias_added_removed(
so that the new alias is available.
"""
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen light",
"light", "demo", "1234", suggested_object_id="kitchen"
)
hass.states.async_set("light.kitchen", "off", {"friendly_name": "kitchen light"})
@@ -438,7 +430,6 @@ async def test_http_processing_intent_entity_renamed(
LIGHT_DOMAIN,
{LIGHT_DOMAIN: [{"platform": "test"}]},
)
await hass.async_block_till_done()
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
@@ -882,20 +873,9 @@ async def test_http_processing_intent_conversion_not_expose_new(
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
@pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on"))
async def test_turn_on_intent(
hass: HomeAssistant,
init_components,
entity_registry: er.EntityRegistry,
sentence,
agent_id,
hass: HomeAssistant, init_components, sentence, agent_id
) -> None:
"""Test calling the turn on intent."""
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
@@ -913,17 +893,8 @@ async def test_turn_on_intent(
@pytest.mark.parametrize("sentence", ("turn off kitchen", "turn kitchen off"))
async def test_turn_off_intent(
hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry, sentence
) -> None:
async def test_turn_off_intent(hass: HomeAssistant, init_components, sentence) -> None:
"""Test calling the turn on intent."""
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "on")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_off")
@@ -969,21 +940,11 @@ async def test_http_api_no_match(
async def test_http_api_handle_failure(
hass: HomeAssistant,
init_components,
entity_registry: er.EntityRegistry,
hass_client: ClientSessionGenerator,
hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator
) -> None:
"""Test the HTTP conversation API with an error during handling."""
client = await hass_client()
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "off")
# Raise an error during intent handling
@@ -1020,19 +981,11 @@ async def test_http_api_handle_failure(
async def test_http_api_unexpected_failure(
hass: HomeAssistant,
init_components,
entity_registry: er.EntityRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test the HTTP conversation API with an unexpected error during handling."""
client = await hass_client()
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "off")
# Raise an "unexpected" error during intent handling
@@ -1351,17 +1304,8 @@ async def test_prepare_fail(hass: HomeAssistant) -> None:
assert not agent._lang_intents.get("not-a-language")
async def test_language_region(
hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry
) -> None:
async def test_language_region(hass: HomeAssistant, init_components) -> None:
"""Test calling the turn on intent."""
entity_registry.async_get_or_create(
"light",
"demo",
"1234",
suggested_object_id="kitchen",
original_name="kitchen",
)
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
@@ -1409,17 +1353,8 @@ async def test_reload_on_new_component(hass: HomeAssistant) -> None:
assert {"light"} == (lang_intents.loaded_components - loaded_components)
async def test_non_default_response(
hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry
) -> None:
async def test_non_default_response(hass: HomeAssistant, init_components) -> None:
"""Test intent response that is not the default."""
entity_registry.async_get_or_create(
"cover",
"demo",
"1234",
suggested_object_id="front_door",
original_name="front door",
)
hass.states.async_set("cover.front_door", "closed")
calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
+11
View File
@@ -1,3 +1,14 @@
"""demo conftest."""
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@@ -218,6 +218,7 @@ async def test_discover_platform(
mock_demo_setup_scanner, mock_see, hass: HomeAssistant
) -> None:
"""Test discovery of device_tracker demo platform."""
await async_setup_component(hass, "homeassistant", {})
with patch("homeassistant.components.device_tracker.legacy.update_config"):
await discovery.async_load_platform(
hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}}
@@ -2,6 +2,8 @@
import math
from unittest.mock import AsyncMock, Mock, patch
import pytest
from homeassistant.components import emulated_kasa
from homeassistant.components.emulated_kasa.const import (
CONF_POWER,
@@ -132,6 +134,12 @@ CONFIG_SENSOR = {
}
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
def nested_value(ndict, *keys):
"""Return a nested dict value or None if it doesn't exist."""
if len(keys) == 0:
@@ -75,6 +75,12 @@ VALID_CONFIG = {
}
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def mock_healthybox():
"""Mock fb.check_box_health."""
+1
View File
@@ -19,6 +19,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test fan registered attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "demo"}})
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
@@ -20,6 +20,7 @@ async def test_diagnostics(
await setup.async_setup_component(
hass, switch.DOMAIN, {"switch": [{"platform": "demo"}]}
)
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass,
@@ -451,6 +451,7 @@ async def test_execute(
hass: HomeAssistant, report_state, on, brightness, value
) -> None:
"""Test an execute command."""
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, "light", {"light": {"platform": "demo"}})
await hass.async_block_till_done()
@@ -635,6 +636,7 @@ async def test_execute_times_out(
orig_execute_limit = sh.EXECUTE_LIMIT
sh.EXECUTE_LIMIT = 0.02 # Decrease timeout to 20ms
await async_setup_component(hass, "light", {"light": {"platform": "demo"}})
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
await hass.services.async_call(
@@ -209,6 +209,7 @@ async def test_send_text_command_expired_token_refresh_failure(
requires_reauth: ConfigEntryState,
) -> None:
"""Test failure refreshing token in send_text_command."""
await async_setup_component(hass, "homeassistant", {})
await setup_integration()
entries = hass.config_entries.async_entries(DOMAIN)
+11
View File
@@ -1,2 +1,13 @@
"""group conftest."""
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
+7
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from datetime import timedelta
import pytest
from homeassistant.components import group
from homeassistant.components.group import ATTR_AUTO, ATTR_ENTITY_ID, ATTR_ORDER
from homeassistant.components.recorder import Recorder
@@ -16,6 +18,11 @@ from tests.common import async_fire_time_changed
from tests.components.recorder.common import async_wait_recording_done
@pytest.fixture(autouse=True)
async def setup_homeassistant():
"""Override the fixture in group.conftest."""
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test number registered attributes to be excluded."""
now = dt_util.utcnow()
@@ -0,0 +1,25 @@
# serializer version: 1
# name: test_get_assistant_settings
dict({
'climate.test_unique1': mappingproxy({
'should_expose': True,
}),
'light.not_in_registry': dict({
'should_expose': True,
}),
})
# ---
# name: test_get_assistant_settings.1
dict({
})
# ---
# name: test_listeners
dict({
'light.kitchen': dict({
'should_expose': True,
}),
'switch.test_unique1': mappingproxy({
'should_expose': True,
}),
})
# ---
@@ -1,10 +1,14 @@
"""Test Home Assistant exposed entities helper."""
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
ExposedEntity,
async_expose_entity,
async_get_assistant_settings,
async_get_entity_settings,
async_listen_entity_updates,
async_should_expose,
)
@@ -18,6 +22,76 @@ from tests.common import flush_store
from tests.typing import WebSocketGenerator
@pytest.fixture(name="entities")
def entities_fixture(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
request: pytest.FixtureRequest,
) -> dict[str, str]:
"""Set up the test environment."""
if request.param == "entities_unique_id":
return entities_unique_id(entity_registry)
elif request.param == "entities_no_unique_id":
return entities_no_unique_id(hass)
else:
raise RuntimeError("Invalid setup fixture")
def entities_unique_id(entity_registry: er.EntityRegistry) -> dict[str, str]:
"""Create some entities in the entity registry."""
entry_blocked = entity_registry.async_get_or_create(
"group", "test", "unique", suggested_object_id="all_locks"
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
entry_lock = entity_registry.async_get_or_create("lock", "test", "unique1")
entry_binary_sensor = entity_registry.async_get_or_create(
"binary_sensor", "test", "unique1"
)
entry_binary_sensor_door = entity_registry.async_get_or_create(
"binary_sensor",
"test",
"unique2",
original_device_class="door",
)
entry_sensor = entity_registry.async_get_or_create("sensor", "test", "unique1")
entry_sensor_temperature = entity_registry.async_get_or_create(
"sensor",
"test",
"unique2",
original_device_class="temperature",
)
return {
"blocked": entry_blocked.entity_id,
"lock": entry_lock.entity_id,
"binary_sensor": entry_binary_sensor.entity_id,
"door_sensor": entry_binary_sensor_door.entity_id,
"sensor": entry_sensor.entity_id,
"temperature_sensor": entry_sensor_temperature.entity_id,
}
def entities_no_unique_id(hass: HomeAssistant) -> dict[str, str]:
"""Create some entities not in the entity registry."""
blocked = CLOUD_NEVER_EXPOSED_ENTITIES[0]
lock = "lock.test"
binary_sensor = "binary_sensor.test"
door_sensor = "binary_sensor.door"
sensor = "sensor.test"
sensor_temperature = "sensor.temperature"
hass.states.async_set(binary_sensor, "on", {})
hass.states.async_set(door_sensor, "on", {"device_class": "door"})
hass.states.async_set(sensor, "on", {})
hass.states.async_set(sensor_temperature, "on", {"device_class": "temperature"})
return {
"blocked": blocked,
"lock": lock,
"binary_sensor": binary_sensor,
"door_sensor": door_sensor,
"sensor": sensor,
"temperature_sensor": sensor_temperature,
}
async def test_load_preferences(hass: HomeAssistant) -> None:
"""Make sure that we can load/save data correctly."""
assert await async_setup_component(hass, "homeassistant", {})
@@ -28,13 +102,21 @@ async def test_load_preferences(hass: HomeAssistant) -> None:
exposed_entities.async_set_expose_new_entities("test1", True)
exposed_entities.async_set_expose_new_entities("test2", False)
async_expose_entity(hass, "test1", "light.kitchen", True)
async_expose_entity(hass, "test1", "light.living_room", True)
async_expose_entity(hass, "test2", "light.kitchen", True)
async_expose_entity(hass, "test2", "light.kitchen", True)
assert list(exposed_entities._assistants) == ["test1", "test2"]
assert list(exposed_entities.entities) == ["light.kitchen", "light.living_room"]
await flush_store(exposed_entities._store)
exposed_entities2 = ExposedEntities(hass)
await flush_store(exposed_entities._store)
await exposed_entities2.async_load()
await exposed_entities2.async_initialize()
assert exposed_entities._assistants == exposed_entities2._assistants
assert exposed_entities.entities == exposed_entities2.entities
async def test_expose_entity(
@@ -50,6 +132,9 @@ async def test_expose_entity(
entry1 = entity_registry.async_get_or_create("test", "test", "unique1")
entry2 = entity_registry.async_get_or_create("test", "test", "unique2")
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
assert len(exposed_entities.entities) == 0
# Set options
await ws_client.send_json_auto_id(
{
@@ -67,6 +152,7 @@ async def test_expose_entity(
assert entry1.options == {"cloud.alexa": {"should_expose": True}}
entry2 = entity_registry.async_get(entry2.entity_id)
assert entry2.options == {}
assert len(exposed_entities.entities) == 0
# Update options
await ws_client.send_json_auto_id(
@@ -91,6 +177,7 @@ async def test_expose_entity(
"cloud.alexa": {"should_expose": False},
"cloud.google_assistant": {"should_expose": False},
}
assert len(exposed_entities.entities) == 0
async def test_expose_entity_unknown(
@@ -103,6 +190,7 @@ async def test_expose_entity_unknown(
await hass.async_block_till_done()
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
assert len(exposed_entities.entities) == 0
# Set options
await ws_client.send_json_auto_id(
@@ -115,14 +203,41 @@ async def test_expose_entity_unknown(
)
response = await ws_client.receive_json()
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "can't expose 'test.test'",
assert response["success"]
assert len(exposed_entities.entities) == 1
assert exposed_entities.entities == {
"test.test": ExposedEntity({"cloud.alexa": {"should_expose": True}})
}
with pytest.raises(HomeAssistantError):
exposed_entities.async_expose_entity("cloud.alexa", "test.test", True)
# Update options
await ws_client.send_json_auto_id(
{
"type": "homeassistant/expose_entity",
"assistants": ["cloud.alexa", "cloud.google_assistant"],
"entity_ids": ["test.test", "test.test2"],
"should_expose": False,
}
)
response = await ws_client.receive_json()
assert response["success"]
assert len(exposed_entities.entities) == 2
assert exposed_entities.entities == {
"test.test": ExposedEntity(
{
"cloud.alexa": {"should_expose": False},
"cloud.google_assistant": {"should_expose": False},
}
),
"test.test2": ExposedEntity(
{
"cloud.alexa": {"should_expose": False},
"cloud.google_assistant": {"should_expose": False},
}
),
}
async def test_expose_entity_blocked(
@@ -220,55 +335,57 @@ async def test_listen_updates(
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
async_listen_entity_updates(hass, "cloud.alexa", listener)
entry = entity_registry.async_get_or_create("climate", "test", "unique1")
# Call for another assistant - listener not called
exposed_entities.async_expose_entity(
"cloud.google_assistant", entry.entity_id, True
)
async_expose_entity(hass, "cloud.google_assistant", entry.entity_id, True)
assert len(calls) == 0
# Call for our assistant - listener called
exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
async_expose_entity(hass, "cloud.alexa", entry.entity_id, True)
assert len(calls) == 1
# Settings not changed - listener not called
exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
async_expose_entity(hass, "cloud.alexa", entry.entity_id, True)
assert len(calls) == 1
# Settings changed - listener called
exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, False)
async_expose_entity(hass, "cloud.alexa", entry.entity_id, False)
assert len(calls) == 2
async def test_get_assistant_settings(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test get assistant settings."""
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
entry = entity_registry.async_get_or_create("climate", "test", "unique1")
assert async_get_assistant_settings(hass, "cloud.alexa") == {}
exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
assert async_get_assistant_settings(hass, "cloud.alexa") == {
"climate.test_unique1": {"should_expose": True}
}
assert async_get_assistant_settings(hass, "cloud.google_assistant") == {}
async_expose_entity(hass, "cloud.alexa", entry.entity_id, True)
async_expose_entity(hass, "cloud.alexa", "light.not_in_registry", True)
assert async_get_assistant_settings(hass, "cloud.alexa") == snapshot
assert async_get_assistant_settings(hass, "cloud.google_assistant") == snapshot
with pytest.raises(HomeAssistantError):
async_get_entity_settings(hass, "light.unknown")
@pytest.mark.parametrize(
"entities", ["entities_unique_id", "entities_no_unique_id"], indirect=True
)
async def test_should_expose(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
hass_ws_client: WebSocketGenerator,
entities: dict[str, str],
) -> None:
"""Test expose entity."""
ws_client = await hass_ws_client(hass)
@@ -290,59 +407,141 @@ async def test_should_expose(
assert async_should_expose(hass, "test.test", "test.test") is False
# Blocked entity is not exposed
entry_blocked = entity_registry.async_get_or_create(
"group", "test", "unique", suggested_object_id="all_locks"
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", entry_blocked.entity_id) is False
assert async_should_expose(hass, "cloud.alexa", entities["blocked"]) is False
# Lock is exposed
lock1 = entity_registry.async_get_or_create("lock", "test", "unique1")
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", lock1.entity_id) is True
# Hidden entity is not exposed
lock2 = entity_registry.async_get_or_create(
"lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", lock2.entity_id) is False
# Entity with category is not exposed
lock3 = entity_registry.async_get_or_create(
"lock", "test", "unique3", entity_category=EntityCategory.CONFIG
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", lock3.entity_id) is False
assert async_should_expose(hass, "cloud.alexa", entities["lock"]) is True
# Binary sensor without device class is not exposed
binarysensor1 = entity_registry.async_get_or_create(
"binary_sensor", "test", "unique1"
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", binarysensor1.entity_id) is False
assert async_should_expose(hass, "cloud.alexa", entities["binary_sensor"]) is False
# Binary sensor with certain device class is exposed
binarysensor2 = entity_registry.async_get_or_create(
"binary_sensor",
"test",
"unique2",
original_device_class="door",
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", binarysensor2.entity_id) is True
assert async_should_expose(hass, "cloud.alexa", entities["door_sensor"]) is True
# Sensor without device class is not exposed
sensor1 = entity_registry.async_get_or_create("sensor", "test", "unique1")
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", sensor1.entity_id) is False
assert async_should_expose(hass, "cloud.alexa", entities["sensor"]) is False
# Sensor with certain device class is exposed
sensor2 = entity_registry.async_get_or_create(
"sensor",
"test",
"unique2",
original_device_class="temperature",
assert (
async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True
)
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0]
assert async_should_expose(hass, "cloud.alexa", sensor2.entity_id) is True
# The second time we check, it should load it from storage
assert (
async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True
)
# Check with a different assistant
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_set_expose_new_entities("cloud.no_default_expose", False)
assert (
async_should_expose(
hass, "cloud.no_default_expose", entities["temperature_sensor"]
)
is False
)
async def test_should_expose_hidden_categorized(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test expose entity."""
ws_client = await hass_ws_client(hass)
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
# Expose new entities to Alexa
await ws_client.send_json_auto_id(
{
"type": "homeassistant/expose_new_entities/set",
"assistant": "cloud.alexa",
"expose_new": True,
}
)
response = await ws_client.receive_json()
assert response["success"]
entity_registry.async_get_or_create(
"lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER
)
assert async_should_expose(hass, "cloud.alexa", "lock.test_unique2") is False
# Entity with category is not exposed
entity_registry.async_get_or_create(
"lock", "test", "unique3", entity_category=EntityCategory.CONFIG
)
assert async_should_expose(hass, "cloud.alexa", "lock.test_unique3") is False
async def test_list_exposed_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test list exposed entities."""
ws_client = await hass_ws_client(hass)
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
entry1 = entity_registry.async_get_or_create("test", "test", "unique1")
entry2 = entity_registry.async_get_or_create("test", "test", "unique2")
# Set options for registered entities
await ws_client.send_json_auto_id(
{
"type": "homeassistant/expose_entity",
"assistants": ["cloud.alexa", "cloud.google_assistant"],
"entity_ids": [entry1.entity_id, entry2.entity_id],
"should_expose": True,
}
)
response = await ws_client.receive_json()
assert response["success"]
# Set options for entities not in the entity registry
await ws_client.send_json_auto_id(
{
"type": "homeassistant/expose_entity",
"assistants": ["cloud.alexa", "cloud.google_assistant"],
"entity_ids": [
"test.test",
"test.test2",
],
"should_expose": False,
}
)
response = await ws_client.receive_json()
assert response["success"]
# List exposed entities
await ws_client.send_json_auto_id({"type": "homeassistant/expose_entity/list"})
response = await ws_client.receive_json()
assert response["success"]
assert response["result"] == {
"exposed_entities": {
"test.test": {"cloud.alexa": False, "cloud.google_assistant": False},
"test.test2": {"cloud.alexa": False, "cloud.google_assistant": False},
"test.test_unique1": {"cloud.alexa": True, "cloud.google_assistant": True},
"test.test_unique2": {"cloud.alexa": True, "cloud.google_assistant": True},
},
}
async def test_listeners(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Make sure we call entity listeners."""
assert await async_setup_component(hass, "homeassistant", {})
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
callbacks = []
exposed_entities.async_listen_entity_updates("test1", lambda: callbacks.append(1))
async_expose_entity(hass, "test1", "light.kitchen", True)
assert len(callbacks) == 1
entry1 = entity_registry.async_get_or_create("switch", "test", "unique1")
async_expose_entity(hass, "test1", entry1.entity_id, True)
@@ -427,6 +427,7 @@ async def test_options_flow_devices(
demo_config_entry = MockConfigEntry(domain="domain")
demo_config_entry.add_to_hass(hass)
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
assert await async_setup_component(hass, "homekit", {"homekit": {}})
@@ -319,6 +319,7 @@ async def test_config_entry_with_trigger_accessory(
entity_registry: er.EntityRegistry,
) -> None:
"""Test generating diagnostics for a bridge config entry with a trigger accessory."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
hk_driver.publish = MagicMock()
+1
View File
@@ -747,6 +747,7 @@ async def test_homekit_start_with_a_device(
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
await hass.async_block_till_done()
+1
View File
@@ -81,6 +81,7 @@ async def test_bridge_with_triggers(
an above or below additional configuration which we have no way
to input, we ignore them.
"""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
await hass.async_block_till_done()
@@ -43,6 +43,12 @@ MOCK_START_STREAM_SESSION_UUID = UUID("3303d503-17cc-469a-b672-92436a71a2f6")
PID_THAT_WILL_NEVER_BE_ALIVE = 2147483647
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
async def _async_start_streaming(hass, acc):
"""Start streaming a camera."""
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
@@ -25,6 +25,7 @@ async def test_programmable_switch_button_fires_on_trigger(
demo_config_entry = MockConfigEntry(domain="domain")
demo_config_entry.add_to_hass(hass)
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}})
await hass.async_block_till_done()
hass.states.async_set("light.ceiling_lights", STATE_OFF)
@@ -16,6 +16,12 @@ from tests.common import assert_setup_component, async_capture_events
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def aiohttp_unused_port(event_loop, aiohttp_unused_port, socket_enabled):
"""Return aiohttp_unused_port and allow opening sockets."""
+44
View File
@@ -36,6 +36,7 @@ from homeassistant.components.light import (
ATTR_TRANSITION,
ATTR_XY_COLOR,
DOMAIN as LIGHT_DOMAIN,
SERVICE_TURN_ON,
ColorMode,
)
from homeassistant.const import (
@@ -1741,3 +1742,46 @@ async def test_set_hev_cycle_state_fails_for_color_bulb(hass: HomeAssistant) ->
{ATTR_ENTITY_ID: entity_id, ATTR_POWER: True},
blocking=True,
)
async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None:
"""Test a light strip were zones are not populated initially."""
already_migrated_config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
)
already_migrated_config_entry.add_to_hass(hass)
bulb = _mocked_light_strip()
bulb.power_level = 65535
bulb.color_zones = None
bulb.color = [65535, 65535, 65535, 65535]
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
device=bulb
), _patch_device(device=bulb):
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
await hass.async_block_till_done()
entity_id = "light.my_bulb"
state = hass.states.get(entity_id)
assert state.state == "on"
attributes = state.attributes
assert attributes[ATTR_BRIGHTNESS] == 255
assert attributes[ATTR_COLOR_MODE] == ColorMode.HS
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
assert attributes[ATTR_HS_COLOR] == (360.0, 100.0)
assert attributes[ATTR_RGB_COLOR] == (255, 0, 0)
assert attributes[ATTR_XY_COLOR] == (0.701, 0.299)
await hass.services.async_call(
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert bulb.set_power.calls[0][0][0] is True
bulb.set_power.reset_mock()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
+1
View File
@@ -26,6 +26,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test light registered attributes to be excluded."""
now = dt_util.utcnow()
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, light.DOMAIN, {light.DOMAIN: {"platform": "demo"}}
)
@@ -21,6 +21,12 @@ from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import ClientSessionGenerator, WebSocketGenerator
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
async def test_get_image_http(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
) -> None:
@@ -25,6 +25,7 @@ from tests.components.recorder.common import async_wait_recording_done
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test media_player registered attributes to be excluded."""
now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, media_player.DOMAIN, {media_player.DOMAIN: {"platform": "demo"}}
)
@@ -25,6 +25,12 @@ from tests.common import assert_setup_component, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
def create_group(hass, name):
"""Create a new person group.
@@ -26,6 +26,12 @@ CONFIG = {
ENDPOINT_URL = f"https://westus.{mf.FACE_API_URL}"
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def store_mock():
"""Mock update store."""
@@ -14,6 +14,12 @@ from tests.components.image_processing import common
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def store_mock():
"""Mock update store."""

Some files were not shown because too many files have changed in this diff Show More