Add doorbell event to google_assistant (#97123)
* First attempt async_report_state_all * Move notificationSupportedByAgent to SYNC response * Make notificationSupportedByAgent conditional * Add generic sync_options method * Report event * Add event_type as ID * User UUID, imlement query_notifications * Refactor query_notifications * Add test * MyPy * Unreachable code * Tweak * Correct notification message * Timestamp was wrong unit, it should be in seconds * Can only allow doorbell class, since it's the only type * Fix test * Remove unrelated changes - improve coverage * Additional tests --------- Co-authored-by: Joakim Plate <elupus@ecce.se>
This commit is contained in:
parent
6387263007
commit
c5b32d6307
11 changed files with 390 additions and 13 deletions
|
@ -345,14 +345,16 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
assistant_options = settings.get(CLOUD_GOOGLE, {})
|
||||
return not assistant_options.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
||||
|
||||
async def async_report_state(self, message: Any, agent_user_id: str) -> None:
|
||||
async def async_report_state(
|
||||
self, message: Any, agent_user_id: str, event_id: str | None = None
|
||||
) -> None:
|
||||
"""Send a state report to Google."""
|
||||
try:
|
||||
await self._cloud.google_report_state.async_send_message(message)
|
||||
except ErrorResponse as err:
|
||||
_LOGGER.warning("Error reporting state - %s: %s", err.code, err.message)
|
||||
|
||||
async def _async_request_sync_devices(self, agent_user_id: str) -> int:
|
||||
async def _async_request_sync_devices(self, agent_user_id: str) -> HTTPStatus | int:
|
||||
"""Trigger a sync with Google."""
|
||||
if self._sync_entities_lock.locked():
|
||||
return HTTPStatus.OK
|
||||
|
|
|
@ -6,6 +6,7 @@ from homeassistant.components import (
|
|||
camera,
|
||||
climate,
|
||||
cover,
|
||||
event,
|
||||
fan,
|
||||
group,
|
||||
humidifier,
|
||||
|
@ -48,6 +49,7 @@ DEFAULT_EXPOSED_DOMAINS = [
|
|||
"binary_sensor",
|
||||
"climate",
|
||||
"cover",
|
||||
"event",
|
||||
"fan",
|
||||
"group",
|
||||
"humidifier",
|
||||
|
@ -73,6 +75,7 @@ TYPE_CAMERA = f"{PREFIX_TYPES}CAMERA"
|
|||
TYPE_CURTAIN = f"{PREFIX_TYPES}CURTAIN"
|
||||
TYPE_DEHUMIDIFIER = f"{PREFIX_TYPES}DEHUMIDIFIER"
|
||||
TYPE_DOOR = f"{PREFIX_TYPES}DOOR"
|
||||
TYPE_DOORBELL = f"{PREFIX_TYPES}DOORBELL"
|
||||
TYPE_FAN = f"{PREFIX_TYPES}FAN"
|
||||
TYPE_GARAGE = f"{PREFIX_TYPES}GARAGE"
|
||||
TYPE_HUMIDIFIER = f"{PREFIX_TYPES}HUMIDIFIER"
|
||||
|
@ -162,6 +165,7 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
|
|||
(cover.DOMAIN, cover.CoverDeviceClass.GATE): TYPE_GARAGE,
|
||||
(cover.DOMAIN, cover.CoverDeviceClass.SHUTTER): TYPE_SHUTTER,
|
||||
(cover.DOMAIN, cover.CoverDeviceClass.WINDOW): TYPE_WINDOW,
|
||||
(event.DOMAIN, event.EventDeviceClass.DOORBELL): TYPE_DOORBELL,
|
||||
(
|
||||
humidifier.DOMAIN,
|
||||
humidifier.HumidifierDeviceClass.DEHUMIDIFIER,
|
||||
|
|
|
@ -9,6 +9,7 @@ from functools import lru_cache
|
|||
from http import HTTPStatus
|
||||
import logging
|
||||
import pprint
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.web import json_response
|
||||
from awesomeversion import AwesomeVersion
|
||||
|
@ -183,7 +184,9 @@ class AbstractConfig(ABC):
|
|||
"""If an entity should have 2FA checked."""
|
||||
return True
|
||||
|
||||
async def async_report_state(self, message, agent_user_id: str):
|
||||
async def async_report_state(
|
||||
self, message: dict[str, Any], agent_user_id: str, event_id: str | None = None
|
||||
) -> HTTPStatus | None:
|
||||
"""Send a state report to Google."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -234,6 +237,33 @@ class AbstractConfig(ABC):
|
|||
)
|
||||
return max(res, default=204)
|
||||
|
||||
async def async_sync_notification(
|
||||
self, agent_user_id: str, event_id: str, payload: dict[str, Any]
|
||||
) -> HTTPStatus:
|
||||
"""Sync notification to Google."""
|
||||
# Remove any pending sync
|
||||
self._google_sync_unsub.pop(agent_user_id, lambda: None)()
|
||||
status = await self.async_report_state(payload, agent_user_id, event_id)
|
||||
assert status is not None
|
||||
if status == HTTPStatus.NOT_FOUND:
|
||||
await self.async_disconnect_agent_user(agent_user_id)
|
||||
return status
|
||||
|
||||
async def async_sync_notification_all(
|
||||
self, event_id: str, payload: dict[str, Any]
|
||||
) -> HTTPStatus:
|
||||
"""Sync notification to Google for all registered agents."""
|
||||
if not self._store.agent_user_ids:
|
||||
return HTTPStatus.NO_CONTENT
|
||||
|
||||
res = await gather(
|
||||
*(
|
||||
self.async_sync_notification(agent_user_id, event_id, payload)
|
||||
for agent_user_id in self._store.agent_user_ids
|
||||
)
|
||||
)
|
||||
return max(res, default=HTTPStatus.NO_CONTENT)
|
||||
|
||||
@callback
|
||||
def async_schedule_google_sync(self, agent_user_id: str):
|
||||
"""Schedule a sync."""
|
||||
|
@ -617,7 +647,6 @@ class GoogleEntity:
|
|||
state.domain, state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
),
|
||||
}
|
||||
|
||||
# Add aliases
|
||||
if (config_aliases := entity_config.get(CONF_ALIASES, [])) or (
|
||||
entity_entry and entity_entry.aliases
|
||||
|
@ -639,6 +668,10 @@ class GoogleEntity:
|
|||
for trt in traits:
|
||||
device["attributes"].update(trt.sync_attributes())
|
||||
|
||||
# Add trait options
|
||||
for trt in traits:
|
||||
device.update(trt.sync_options())
|
||||
|
||||
# Add roomhint
|
||||
if room := entity_config.get(CONF_ROOM_HINT):
|
||||
device["roomHint"] = room
|
||||
|
@ -681,6 +714,16 @@ class GoogleEntity:
|
|||
|
||||
return attrs
|
||||
|
||||
@callback
|
||||
def notifications_serialize(self) -> dict[str, Any] | None:
|
||||
"""Serialize the payload for notifications to be sent."""
|
||||
notifications: dict[str, Any] = {}
|
||||
|
||||
for trt in self.traits():
|
||||
deep_update(notifications, trt.query_notifications() or {})
|
||||
|
||||
return notifications or None
|
||||
|
||||
@callback
|
||||
def reachable_device_serialize(self):
|
||||
"""Serialize entity for a REACHABLE_DEVICE response."""
|
||||
|
|
|
@ -158,7 +158,7 @@ class GoogleConfig(AbstractConfig):
|
|||
"""If an entity should have 2FA checked."""
|
||||
return True
|
||||
|
||||
async def _async_request_sync_devices(self, agent_user_id: str):
|
||||
async def _async_request_sync_devices(self, agent_user_id: str) -> HTTPStatus:
|
||||
if CONF_SERVICE_ACCOUNT in self._config:
|
||||
return await self.async_call_homegraph_api(
|
||||
REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id}
|
||||
|
@ -220,14 +220,18 @@ class GoogleConfig(AbstractConfig):
|
|||
_LOGGER.error("Could not contact %s", url)
|
||||
return HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
async def async_report_state(self, message, agent_user_id: str):
|
||||
async def async_report_state(
|
||||
self, message: dict[str, Any], agent_user_id: str, event_id: str | None = None
|
||||
) -> HTTPStatus:
|
||||
"""Send a state report to Google."""
|
||||
data = {
|
||||
"requestId": uuid4().hex,
|
||||
"agentUserId": agent_user_id,
|
||||
"payload": message,
|
||||
}
|
||||
await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data)
|
||||
if event_id is not None:
|
||||
data["eventId"] = event_id
|
||||
return await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data)
|
||||
|
||||
|
||||
class GoogleAssistantView(HomeAssistantView):
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
from collections import deque
|
||||
import logging
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.const import MATCH_ALL
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback
|
||||
|
@ -30,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
@callback
|
||||
def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig):
|
||||
"""Enable state reporting."""
|
||||
"""Enable state and notification reporting."""
|
||||
checker = None
|
||||
unsub_pending: CALLBACK_TYPE | None = None
|
||||
pending: deque[dict[str, Any]] = deque([{}])
|
||||
|
@ -79,6 +80,23 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig
|
|||
):
|
||||
return
|
||||
|
||||
if (notifications := entity.notifications_serialize()) is not None:
|
||||
event_id = uuid4().hex
|
||||
payload = {
|
||||
"devices": {"notifications": {entity.state.entity_id: notifications}}
|
||||
}
|
||||
_LOGGER.info(
|
||||
"Sending event notification for entity %s",
|
||||
entity.state.entity_id,
|
||||
)
|
||||
result = await google_config.async_sync_notification_all(event_id, payload)
|
||||
if result != 200:
|
||||
_LOGGER.error(
|
||||
"Unable to send notification with result code: %s, check log for more"
|
||||
" info",
|
||||
result,
|
||||
)
|
||||
|
||||
try:
|
||||
entity_data = entity.query_serialize()
|
||||
except SmartHomeError as err:
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, TypeVar
|
||||
|
||||
|
@ -12,6 +13,7 @@ from homeassistant.components import (
|
|||
camera,
|
||||
climate,
|
||||
cover,
|
||||
event,
|
||||
fan,
|
||||
group,
|
||||
humidifier,
|
||||
|
@ -74,9 +76,10 @@ from homeassistant.const import (
|
|||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.util import color as color_util, dt as dt_util
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.percentage import (
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item,
|
||||
|
@ -115,6 +118,7 @@ TRAIT_LOCKUNLOCK = f"{PREFIX_TRAITS}LockUnlock"
|
|||
TRAIT_FANSPEED = f"{PREFIX_TRAITS}FanSpeed"
|
||||
TRAIT_MODES = f"{PREFIX_TRAITS}Modes"
|
||||
TRAIT_INPUTSELECTOR = f"{PREFIX_TRAITS}InputSelector"
|
||||
TRAIT_OBJECTDETECTION = f"{PREFIX_TRAITS}ObjectDetection"
|
||||
TRAIT_OPENCLOSE = f"{PREFIX_TRAITS}OpenClose"
|
||||
TRAIT_VOLUME = f"{PREFIX_TRAITS}Volume"
|
||||
TRAIT_ARMDISARM = f"{PREFIX_TRAITS}ArmDisarm"
|
||||
|
@ -221,7 +225,7 @@ class _Trait(ABC):
|
|||
def supported(domain, features, device_class, attributes):
|
||||
"""Test if state is supported."""
|
||||
|
||||
def __init__(self, hass, state, config):
|
||||
def __init__(self, hass: HomeAssistant, state, config) -> None:
|
||||
"""Initialize a trait for a state."""
|
||||
self.hass = hass
|
||||
self.state = state
|
||||
|
@ -231,10 +235,17 @@ class _Trait(ABC):
|
|||
"""Return attributes for a sync request."""
|
||||
raise NotImplementedError
|
||||
|
||||
def sync_options(self) -> dict[str, Any]:
|
||||
"""Add options for the sync request."""
|
||||
return {}
|
||||
|
||||
def query_attributes(self):
|
||||
"""Return the attributes of this trait for this entity."""
|
||||
raise NotImplementedError
|
||||
|
||||
def query_notifications(self) -> dict[str, Any] | None:
|
||||
"""Return notifications payload."""
|
||||
|
||||
def can_execute(self, command, params):
|
||||
"""Test if command can be executed."""
|
||||
return command in self.commands
|
||||
|
@ -335,6 +346,60 @@ class CameraStreamTrait(_Trait):
|
|||
}
|
||||
|
||||
|
||||
@register_trait
|
||||
class ObjectDetection(_Trait):
|
||||
"""Trait to object detection.
|
||||
|
||||
https://developers.google.com/actions/smarthome/traits/objectdetection
|
||||
"""
|
||||
|
||||
name = TRAIT_OBJECTDETECTION
|
||||
commands = []
|
||||
|
||||
@staticmethod
|
||||
def supported(domain, features, device_class, _) -> bool:
|
||||
"""Test if state is supported."""
|
||||
return (
|
||||
domain == event.DOMAIN and device_class == event.EventDeviceClass.DOORBELL
|
||||
)
|
||||
|
||||
def sync_attributes(self):
|
||||
"""Return ObjectDetection attributes for a sync request."""
|
||||
return {}
|
||||
|
||||
def sync_options(self) -> dict[str, Any]:
|
||||
"""Add options for the sync request."""
|
||||
return {"notificationSupportedByAgent": True}
|
||||
|
||||
def query_attributes(self):
|
||||
"""Return ObjectDetection query attributes."""
|
||||
return {}
|
||||
|
||||
def query_notifications(self) -> dict[str, Any] | None:
|
||||
"""Return notifications payload."""
|
||||
|
||||
if self.state.state in {STATE_UNKNOWN, STATE_UNAVAILABLE}:
|
||||
return None
|
||||
|
||||
# Only notify if last event was less then 30 seconds ago
|
||||
time_stamp = datetime.fromisoformat(self.state.state)
|
||||
if (utcnow() - time_stamp) > timedelta(seconds=30):
|
||||
return None
|
||||
|
||||
return {
|
||||
"ObjectDetection": {
|
||||
"objects": {
|
||||
"unclassified": 1,
|
||||
},
|
||||
"priority": 0,
|
||||
"detectionTimestamp": int(time_stamp.timestamp() * 1000),
|
||||
},
|
||||
}
|
||||
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute an ObjectDetection command."""
|
||||
|
||||
|
||||
@register_trait
|
||||
class OnOffTrait(_Trait):
|
||||
"""Trait to offer basic on and off functionality.
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
'binary_sensor',
|
||||
'climate',
|
||||
'cover',
|
||||
'event',
|
||||
'fan',
|
||||
'group',
|
||||
'humidifier',
|
||||
|
|
|
@ -306,7 +306,7 @@ async def test_agent_user_id_connect() -> None:
|
|||
|
||||
@pytest.mark.parametrize("agents", [{}, {"1"}, {"1", "2"}])
|
||||
async def test_report_state_all(agents) -> None:
|
||||
"""Test a disconnect message."""
|
||||
"""Test sync of all states."""
|
||||
config = MockConfig(agent_user_ids=agents)
|
||||
data = {}
|
||||
with patch.object(config, "async_report_state") as mock:
|
||||
|
@ -314,6 +314,28 @@ async def test_report_state_all(agents) -> None:
|
|||
assert sorted(mock.mock_calls) == sorted(call(data, agent) for agent in agents)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agents", [{}, {"1"}, {"1", "2"}])
|
||||
async def test_sync_entities(agents) -> None:
|
||||
"""Test sync of all entities."""
|
||||
config = MockConfig(agent_user_ids=agents)
|
||||
with patch.object(
|
||||
config, "async_sync_entities", return_value=HTTPStatus.NO_CONTENT
|
||||
) as mock:
|
||||
await config.async_sync_entities_all()
|
||||
assert sorted(mock.mock_calls) == sorted(call(agent) for agent in agents)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agents", [{}, {"1"}, {"1", "2"}])
|
||||
async def test_sync_notifications(agents) -> None:
|
||||
"""Test sync of notifications."""
|
||||
config = MockConfig(agent_user_ids=agents)
|
||||
with patch.object(
|
||||
config, "async_sync_notification", return_value=HTTPStatus.NO_CONTENT
|
||||
) as mock:
|
||||
await config.async_sync_notification_all("1234", {})
|
||||
assert not agents or bool(mock.mock_calls) and agents
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("agents", "result"),
|
||||
[({}, 204), ({"1": 200}, 200), ({"1": 200, "2": 300}, 300)],
|
||||
|
|
|
@ -3,6 +3,7 @@ from datetime import UTC, datetime, timedelta
|
|||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -195,6 +196,38 @@ async def test_report_state(
|
|||
)
|
||||
|
||||
|
||||
async def test_report_event(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the report event function."""
|
||||
agent_user_id = "user"
|
||||
config = GoogleConfig(hass, DUMMY_CONFIG)
|
||||
await config.async_initialize()
|
||||
|
||||
await config.async_connect_agent_user(agent_user_id)
|
||||
message = {"devices": {}}
|
||||
|
||||
with patch.object(config, "async_call_homegraph_api"):
|
||||
# Wait for google_assistant.helpers.async_initialize.sync_google to be called
|
||||
await hass.async_block_till_done()
|
||||
|
||||
event_id = uuid4().hex
|
||||
with patch.object(config, "async_call_homegraph_api") as mock_call:
|
||||
# Wait for google_assistant.helpers.async_initialize.sync_google to be called
|
||||
await config.async_report_state(message, agent_user_id, event_id=event_id)
|
||||
mock_call.assert_called_once_with(
|
||||
REPORT_STATE_BASE_URL,
|
||||
{
|
||||
"requestId": ANY,
|
||||
"agentUserId": agent_user_id,
|
||||
"payload": message,
|
||||
"eventId": event_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_google_config_local_fulfillment(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""Test Google report state."""
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
from time import mktime
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
@ -9,7 +11,7 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import BASIC_CONFIG
|
||||
from . import BASIC_CONFIG, MockConfig
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
@ -21,6 +23,9 @@ async def test_report_state(
|
|||
assert await async_setup_component(hass, "switch", {})
|
||||
hass.states.async_set("light.ceiling", "off")
|
||||
hass.states.async_set("switch.ac", "on")
|
||||
hass.states.async_set(
|
||||
"event.doorbell", "unknown", attributes={"device_class": "doorbell"}
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
BASIC_CONFIG, "async_report_state_all", AsyncMock()
|
||||
|
@ -37,6 +42,7 @@ async def test_report_state(
|
|||
"states": {
|
||||
"light.ceiling": {"on": False, "online": True},
|
||||
"switch.ac": {"on": True, "online": True},
|
||||
"event.doorbell": {"online": True},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,3 +134,145 @@ async def test_report_state(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_report.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-08-01 00:00:00")
|
||||
async def test_report_notifications(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test report state works."""
|
||||
config = MockConfig(agent_user_ids={"1"})
|
||||
|
||||
assert await async_setup_component(hass, "event", {})
|
||||
hass.states.async_set(
|
||||
"event.doorbell", "unknown", attributes={"device_class": "doorbell"}
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
config, "async_report_state_all", AsyncMock()
|
||||
) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0):
|
||||
report_state.async_enable_report_state(hass, config)
|
||||
|
||||
async_fire_time_changed(
|
||||
hass, datetime.fromisoformat("2023-08-01T00:01:00+00:00")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test that enabling report state does a report on event entities
|
||||
assert len(mock_report.mock_calls) == 1
|
||||
assert mock_report.mock_calls[0][1][0] == {
|
||||
"devices": {
|
||||
"states": {
|
||||
"event.doorbell": {"online": True},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
with patch.object(
|
||||
config, "async_report_state", return_value=HTTPStatus(200)
|
||||
) as mock_report_state:
|
||||
event_time = datetime.fromisoformat("2023-08-01T00:02:57+00:00")
|
||||
epoc_event_time = int(mktime(event_time.timetuple()))
|
||||
hass.states.async_set(
|
||||
"event.doorbell",
|
||||
"2023-08-01T00:02:57+00:00",
|
||||
attributes={"device_class": "doorbell"},
|
||||
)
|
||||
async_fire_time_changed(
|
||||
hass, datetime.fromisoformat("2023-08-01T00:03:00+00:00")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_report_state.mock_calls) == 1
|
||||
notifications_payload = mock_report_state.mock_calls[0][1][0]["devices"][
|
||||
"notifications"
|
||||
]["event.doorbell"]
|
||||
assert notifications_payload == {
|
||||
"ObjectDetection": {
|
||||
"objects": {"unclassified": 1},
|
||||
"priority": 0,
|
||||
"detectionTimestamp": epoc_event_time * 1000,
|
||||
}
|
||||
}
|
||||
assert "Sending event notification for entity event.doorbell" in caplog.text
|
||||
assert "Unable to send notification with result code" not in caplog.text
|
||||
|
||||
hass.states.async_set(
|
||||
"event.doorbell", "unknown", attributes={"device_class": "doorbell"}
|
||||
)
|
||||
async_fire_time_changed(
|
||||
hass, datetime.fromisoformat("2023-08-01T01:01:00+00:00")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test the notification request failed
|
||||
caplog.clear()
|
||||
with patch.object(
|
||||
config, "async_report_state", return_value=HTTPStatus(500)
|
||||
) as mock_report_state:
|
||||
event_time = datetime.fromisoformat("2023-08-01T01:02:57+00:00")
|
||||
epoc_event_time = int(mktime(event_time.timetuple()))
|
||||
hass.states.async_set(
|
||||
"event.doorbell",
|
||||
"2023-08-01T01:02:57+00:00",
|
||||
attributes={"device_class": "doorbell"},
|
||||
)
|
||||
async_fire_time_changed(
|
||||
hass, datetime.fromisoformat("2023-08-01T01:03:00+00:00")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_report_state.mock_calls) == 2
|
||||
for call in mock_report_state.mock_calls:
|
||||
if "notifications" in call[1][0]["devices"]:
|
||||
notifications = call[1][0]["devices"]["notifications"]
|
||||
elif "states" in call[1][0]["devices"]:
|
||||
states = call[1][0]["devices"]["states"]
|
||||
assert notifications["event.doorbell"] == {
|
||||
"ObjectDetection": {
|
||||
"objects": {"unclassified": 1},
|
||||
"priority": 0,
|
||||
"detectionTimestamp": epoc_event_time * 1000,
|
||||
}
|
||||
}
|
||||
assert states["event.doorbell"] == {"online": True}
|
||||
assert "Sending event notification for entity event.doorbell" in caplog.text
|
||||
assert (
|
||||
"Unable to send notification with result code: 500, check log for more info"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
# Test disconnecting agent user
|
||||
caplog.clear()
|
||||
with patch.object(
|
||||
config, "async_report_state", return_value=HTTPStatus.NOT_FOUND
|
||||
) as mock_report_state, patch.object(config, "async_disconnect_agent_user"):
|
||||
event_time = datetime.fromisoformat("2023-08-01T01:03:57+00:00")
|
||||
epoc_event_time = int(mktime(event_time.timetuple()))
|
||||
hass.states.async_set(
|
||||
"event.doorbell",
|
||||
"2023-08-01T01:03:57+00:00",
|
||||
attributes={"device_class": "doorbell"},
|
||||
)
|
||||
async_fire_time_changed(
|
||||
hass, datetime.fromisoformat("2023-08-01T01:04:00+00:00")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_report_state.mock_calls) == 2
|
||||
for call in mock_report_state.mock_calls:
|
||||
if "notifications" in call[1][0]["devices"]:
|
||||
notifications = call[1][0]["devices"]["notifications"]
|
||||
elif "states" in call[1][0]["devices"]:
|
||||
states = call[1][0]["devices"]["states"]
|
||||
assert notifications["event.doorbell"] == {
|
||||
"ObjectDetection": {
|
||||
"objects": {"unclassified": 1},
|
||||
"priority": 0,
|
||||
"detectionTimestamp": epoc_event_time * 1000,
|
||||
}
|
||||
}
|
||||
assert states["event.doorbell"] == {"online": True}
|
||||
assert "Sending event notification for entity event.doorbell" in caplog.text
|
||||
assert (
|
||||
"Unable to send notification with result code: 404, check log for more info"
|
||||
in caplog.text
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant.components import (
|
|||
camera,
|
||||
climate,
|
||||
cover,
|
||||
event,
|
||||
fan,
|
||||
group,
|
||||
humidifier,
|
||||
|
@ -220,6 +221,42 @@ async def test_onoff_input_boolean(hass: HomeAssistant) -> None:
|
|||
assert off_calls[0].data == {ATTR_ENTITY_ID: "input_boolean.bla"}
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-08-01T00:02:57+00:00")
|
||||
async def test_doorbell_event(hass: HomeAssistant) -> None:
|
||||
"""Test doorbell event trait support for input_boolean domain."""
|
||||
assert trait.ObjectDetection.supported(event.DOMAIN, 0, "doorbell", None)
|
||||
|
||||
state = State(
|
||||
"event.bla",
|
||||
"2023-08-01T00:02:57+00:00",
|
||||
attributes={"device_class": "doorbell"},
|
||||
)
|
||||
trt_od = trait.ObjectDetection(hass, state, BASIC_CONFIG)
|
||||
|
||||
assert not trt_od.sync_attributes()
|
||||
assert trt_od.sync_options() == {"notificationSupportedByAgent": True}
|
||||
assert not trt_od.query_attributes()
|
||||
time_stamp = datetime.fromisoformat(state.state)
|
||||
assert trt_od.query_notifications() == {
|
||||
"ObjectDetection": {
|
||||
"objects": {
|
||||
"unclassified": 1,
|
||||
},
|
||||
"priority": 0,
|
||||
"detectionTimestamp": int(time_stamp.timestamp() * 1000),
|
||||
}
|
||||
}
|
||||
|
||||
# Test that stale notifications (older than 30 s) are dropped
|
||||
state = State(
|
||||
"event.bla",
|
||||
"2023-08-01T00:02:22+00:00",
|
||||
attributes={"device_class": "doorbell"},
|
||||
)
|
||||
trt_od = trait.ObjectDetection(hass, state, BASIC_CONFIG)
|
||||
assert trt_od.query_notifications() is None
|
||||
|
||||
|
||||
async def test_onoff_switch(hass: HomeAssistant) -> None:
|
||||
"""Test OnOff trait support for switch domain."""
|
||||
assert helpers.get_google_type(switch.DOMAIN, None) is not None
|
||||
|
|
Loading…
Add table
Reference in a new issue