diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py
index ff3354e5c77..49f7522c03c 100644
--- a/homeassistant/components/html5/notify.py
+++ b/homeassistant/components/html5/notify.py
@@ -4,7 +4,6 @@ from __future__ import annotations
from contextlib import suppress
from datetime import datetime, timedelta
-from functools import partial
from http import HTTPStatus
import json
import logging
@@ -13,7 +12,7 @@ from typing import TYPE_CHECKING, Any, NotRequired, TypedDict, cast
from urllib.parse import urlparse
import uuid
-from aiohttp import web
+from aiohttp import ClientSession, web
from aiohttp.hdrs import AUTHORIZATION
import jwt
from py_vapid import Vapid
@@ -35,6 +34,7 @@ from homeassistant.const import ATTR_NAME, URL_ROOT
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.json import save_json
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import ensure_unique_string
@@ -203,8 +203,9 @@ async def async_get_service(
hass.http.register_view(HTML5PushRegistrationView(registrations, json_path))
hass.http.register_view(HTML5PushCallbackView(registrations))
+ session = async_get_clientsession(hass)
return HTML5NotificationService(
- hass, vapid_prv_key, vapid_email, registrations, json_path
+ hass, session, vapid_prv_key, vapid_email, registrations, json_path
)
@@ -420,12 +421,14 @@ class HTML5NotificationService(BaseNotificationService):
def __init__(
self,
hass: HomeAssistant,
+ session: ClientSession,
vapid_prv: str,
vapid_email: str,
registrations: dict[str, Registration],
json_path: str,
) -> None:
"""Initialize the service."""
+ self.session = session
self._vapid_prv = vapid_prv
self._vapid_email = vapid_email
self.registrations = registrations
@@ -456,22 +459,18 @@ class HTML5NotificationService(BaseNotificationService):
"""Return a dictionary of registered targets."""
return {registration: registration for registration in self.registrations}
- def dismiss(self, **kwargs: Any) -> None:
- """Dismisses a notification."""
- data: dict[str, Any] | None = kwargs.get(ATTR_DATA)
- tag: str = data.get(ATTR_TAG, "") if data else ""
- payload = {ATTR_TAG: tag, ATTR_DISMISS: True, ATTR_DATA: {}}
-
- self._push_message(payload, **kwargs)
-
- async def async_dismiss(self, **kwargs) -> None:
+ async def async_dismiss(self, **kwargs: Any) -> None:
"""Dismisses a notification.
This method must be run in the event loop.
"""
- await self.hass.async_add_executor_job(partial(self.dismiss, **kwargs))
+ data: dict[str, Any] | None = kwargs.get(ATTR_DATA)
+ tag: str = data.get(ATTR_TAG, "") if data else ""
+ payload = {ATTR_TAG: tag, ATTR_DISMISS: True, ATTR_DATA: {}}
- def send_message(self, message: str = "", **kwargs: Any) -> None:
+ await self._push_message(payload, **kwargs)
+
+ async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to a user."""
tag = str(uuid.uuid4())
payload: dict[str, Any] = {
@@ -503,9 +502,9 @@ class HTML5NotificationService(BaseNotificationService):
):
payload[ATTR_DATA][ATTR_URL] = URL_ROOT
- self._push_message(payload, **kwargs)
+ await self._push_message(payload, **kwargs)
- def _push_message(self, payload: dict[str, Any], **kwargs: Any) -> None:
+ async def _push_message(self, payload: dict[str, Any], **kwargs: Any) -> None:
"""Send the message."""
timestamp = int(time.time())
@@ -535,7 +534,9 @@ class HTML5NotificationService(BaseNotificationService):
subscription["keys"]["auth"],
)
- webpusher = WebPusher(cast(dict[str, Any], info["subscription"]))
+ webpusher = WebPusher(
+ cast(dict[str, Any], info["subscription"]), aiohttp_session=self.session
+ )
endpoint = urlparse(subscription["endpoint"])
vapid_claims = {
@@ -545,28 +546,31 @@ class HTML5NotificationService(BaseNotificationService):
}
vapid_headers = Vapid.from_string(self._vapid_prv).sign(vapid_claims)
vapid_headers.update({"urgency": priority, "priority": priority})
- response = webpusher.send(
+
+ response = await webpusher.send_async(
data=json.dumps(payload), headers=vapid_headers, ttl=ttl
)
if TYPE_CHECKING:
assert not isinstance(response, str)
- if response.status_code == HTTPStatus.GONE:
+ if response.status == HTTPStatus.GONE:
_LOGGER.info("Notification channel has expired")
reg = self.registrations.pop(target)
try:
- save_json(self.registrations_json_path, self.registrations)
+ await self.hass.async_add_executor_job(
+ save_json, self.registrations_json_path, self.registrations
+ )
except HomeAssistantError:
self.registrations[target] = reg
_LOGGER.error("Error saving registration")
else:
_LOGGER.info("Configuration saved")
- elif response.status_code >= HTTPStatus.BAD_REQUEST:
+ elif response.status >= HTTPStatus.BAD_REQUEST:
_LOGGER.error(
"There was an issue sending the notification %s: %s",
- response.status_code,
- response.text,
+ response.status,
+ await response.text(),
)
diff --git a/tests/components/html5/conftest.py b/tests/components/html5/conftest.py
index 9c5322b94a6..d24e3102142 100644
--- a/tests/components/html5/conftest.py
+++ b/tests/components/html5/conftest.py
@@ -1,8 +1,9 @@
"""Common fixtures for html5 integration."""
from collections.abc import Generator
-from unittest.mock import MagicMock
+from unittest.mock import AsyncMock, MagicMock
+from aiohttp import ClientResponse
import pytest
from homeassistant.components.html5.const import (
@@ -45,3 +46,58 @@ def mock_load_config() -> Generator[MagicMock]:
"homeassistant.components.html5.notify._load_config", return_value={}
) as mock_load_config:
yield mock_load_config
+
+
+@pytest.fixture
+def mock_wp() -> Generator[AsyncMock]:
+ """Mock WebPusher."""
+
+ with (
+ patch(
+ "homeassistant.components.html5.notify.WebPusher", autospec=True
+ ) as mock_client,
+ ):
+ client = mock_client.return_value
+ client.cls = mock_client
+ client.send_async.return_value = AsyncMock(spec=ClientResponse, status=201)
+ yield client
+
+
+@pytest.fixture
+def mock_jwt() -> Generator[MagicMock]:
+ """Mock JWT."""
+
+ with (
+ patch("homeassistant.components.html5.notify.jwt") as mock_client,
+ ):
+ mock_client.encode.return_value = "JWT"
+ mock_client.decode.return_value = {"target": "device"}
+ yield mock_client
+
+
+@pytest.fixture
+def mock_uuid() -> Generator[MagicMock]:
+ """Mock UUID."""
+
+ with (
+ patch("homeassistant.components.html5.notify.uuid") as mock_client,
+ ):
+ mock_client.uuid4.return_value = "12345678-1234-5678-1234-567812345678"
+ yield mock_client
+
+
+@pytest.fixture
+def mock_vapid() -> Generator[MagicMock]:
+ """Mock VAPID headers."""
+
+ with (
+ patch(
+ "homeassistant.components.html5.notify.Vapid", autospec=True
+ ) as mock_client,
+ ):
+ mock_client.from_string.return_value.sign.return_value = {
+ "Authorization": "vapid t=signed!!!",
+ "urgency": "normal",
+ "priority": "normal",
+ }
+ yield mock_client
diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py
index d1d37cc0e16..3861cca25cd 100644
--- a/tests/components/html5/test_notify.py
+++ b/tests/components/html5/test_notify.py
@@ -2,7 +2,7 @@
from http import HTTPStatus
import json
-from unittest.mock import MagicMock, mock_open, patch
+from unittest.mock import AsyncMock, MagicMock, mock_open, patch
from aiohttp.hdrs import AUTHORIZATION
import pytest
@@ -71,6 +71,12 @@ SUBSCRIPTION_5 = {
REGISTER_URL = "/api/notify.html5"
PUBLISH_URL = "/api/notify.html5/callback"
+VAPID_HEADERS = {
+ "Authorization": "vapid t=signed!!!",
+ "urgency": "normal",
+ "priority": "normal",
+}
+
async def test_get_service_with_no_json(hass: HomeAssistant) -> None:
"""Test empty json file."""
@@ -82,11 +88,11 @@ async def test_get_service_with_no_json(hass: HomeAssistant) -> None:
assert service is not None
-@patch("homeassistant.components.html5.notify.WebPusher")
-async def test_dismissing_message(mock_wp, hass: HomeAssistant) -> None:
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
+async def test_dismissing_message(mock_wp: AsyncMock, hass: HomeAssistant) -> None:
"""Test dismissing message."""
await async_setup_component(hass, "http", {})
- mock_wp().send().status_code = 201
data = {"device": SUBSCRIPTION_1}
@@ -99,23 +105,18 @@ async def test_dismissing_message(mock_wp, hass: HomeAssistant) -> None:
await service.async_dismiss(target=["device", "non_existing"], data={"tag": "test"})
- assert len(mock_wp.mock_calls) == 4
-
- # WebPusher constructor
- assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
-
- # Call to send
- payload = json.loads(mock_wp.mock_calls[3][2]["data"])
-
- assert payload["dismiss"] is True
- assert payload["tag"] == "test"
+ mock_wp.send_async.assert_awaited_once_with(
+ data='{"tag": "test", "dismiss": true, "data": {"jwt": "JWT"}, "timestamp": 1234567890000}',
+ headers=VAPID_HEADERS,
+ ttl=86400,
+ )
-@patch("homeassistant.components.html5.notify.WebPusher")
-async def test_sending_message(mock_wp, hass: HomeAssistant) -> None:
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
+async def test_sending_message(mock_wp: AsyncMock, hass: HomeAssistant) -> None:
"""Test sending message."""
await async_setup_component(hass, "http", {})
- mock_wp().send().status_code = 201
data = {"device": SUBSCRIPTION_1}
@@ -130,23 +131,21 @@ async def test_sending_message(mock_wp, hass: HomeAssistant) -> None:
"Hello", target=["device", "non_existing"], data={"icon": "beer.png"}
)
- assert len(mock_wp.mock_calls) == 4
+ mock_wp.send_async.assert_awaited_once_with(
+ data='{"badge": "/static/images/notification-badge.png", "body": "Hello", "data": {"url": "/", "jwt": "JWT"}, "icon": "beer.png", "tag": "12345678-1234-5678-1234-567812345678", "title": "Home Assistant", "timestamp": 1234567890000}',
+ headers=VAPID_HEADERS,
+ ttl=86400,
+ )
# WebPusher constructor
- assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
-
- # Call to send
- payload = json.loads(mock_wp.mock_calls[3][2]["data"])
-
- assert payload["body"] == "Hello"
- assert payload["icon"] == "beer.png"
+ assert mock_wp.cls.call_args[0][0] == SUBSCRIPTION_1["subscription"]
-@patch("homeassistant.components.html5.notify.WebPusher")
-async def test_fcm_key_include(mock_wp, hass: HomeAssistant) -> None:
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
+async def test_fcm_key_include(mock_wp: AsyncMock, hass: HomeAssistant) -> None:
"""Test if the FCM header is included."""
await async_setup_component(hass, "http", {})
- mock_wp().send().status_code = 201
data = {"chrome": SUBSCRIPTION_5}
@@ -159,19 +158,23 @@ async def test_fcm_key_include(mock_wp, hass: HomeAssistant) -> None:
await service.async_send_message("Hello", target=["chrome"])
- assert len(mock_wp.mock_calls) == 4
+ mock_wp.send_async.assert_awaited_once_with(
+ data='{"badge": "/static/images/notification-badge.png", "body": "Hello", "data": {"url": "/", "jwt": "JWT"}, "icon": "/static/icons/favicon-192x192.png", "tag": "12345678-1234-5678-1234-567812345678", "title": "Home Assistant", "timestamp": 1234567890000}',
+ headers=VAPID_HEADERS,
+ ttl=86400,
+ )
+
# WebPusher constructor
- assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"]
-
- # Get the keys passed to the WebPusher's send method
- assert mock_wp.mock_calls[3][2]["headers"]["Authorization"] is not None
+ assert mock_wp.cls.call_args[0][0] == SUBSCRIPTION_5["subscription"]
-@patch("homeassistant.components.html5.notify.WebPusher")
-async def test_fcm_send_with_unknown_priority(mock_wp, hass: HomeAssistant) -> None:
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
+async def test_fcm_send_with_unknown_priority(
+ mock_wp: AsyncMock, hass: HomeAssistant
+) -> None:
"""Test if the gcm_key is only included for GCM endpoints."""
await async_setup_component(hass, "http", {})
- mock_wp().send().status_code = 201
data = {"chrome": SUBSCRIPTION_5}
@@ -184,19 +187,20 @@ async def test_fcm_send_with_unknown_priority(mock_wp, hass: HomeAssistant) -> N
await service.async_send_message("Hello", target=["chrome"], priority="undefined")
- assert len(mock_wp.mock_calls) == 4
+ mock_wp.send_async.assert_awaited_once_with(
+ data='{"badge": "/static/images/notification-badge.png", "body": "Hello", "data": {"url": "/", "jwt": "JWT"}, "icon": "/static/icons/favicon-192x192.png", "tag": "12345678-1234-5678-1234-567812345678", "title": "Home Assistant", "timestamp": 1234567890000}',
+ headers=VAPID_HEADERS,
+ ttl=86400,
+ )
# WebPusher constructor
- assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"]
-
- # Get the keys passed to the WebPusher's send method
- assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal"
+ assert mock_wp.cls.call_args[0][0] == SUBSCRIPTION_5["subscription"]
-@patch("homeassistant.components.html5.notify.WebPusher")
-async def test_fcm_no_targets(mock_wp, hass: HomeAssistant) -> None:
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
+async def test_fcm_no_targets(mock_wp: AsyncMock, hass: HomeAssistant) -> None:
"""Test if the gcm_key is only included for GCM endpoints."""
await async_setup_component(hass, "http", {})
- mock_wp().send().status_code = 201
data = {"chrome": SUBSCRIPTION_5}
@@ -209,19 +213,20 @@ async def test_fcm_no_targets(mock_wp, hass: HomeAssistant) -> None:
await service.async_send_message("Hello")
- assert len(mock_wp.mock_calls) == 4
+ mock_wp.send_async.assert_awaited_once_with(
+ data='{"badge": "/static/images/notification-badge.png", "body": "Hello", "data": {"url": "/", "jwt": "JWT"}, "icon": "/static/icons/favicon-192x192.png", "tag": "12345678-1234-5678-1234-567812345678", "title": "Home Assistant", "timestamp": 1234567890000}',
+ headers=VAPID_HEADERS,
+ ttl=86400,
+ )
# WebPusher constructor
- assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"]
-
- # Get the keys passed to the WebPusher's send method
- assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal"
+ assert mock_wp.cls.call_args[0][0] == SUBSCRIPTION_5["subscription"]
-@patch("homeassistant.components.html5.notify.WebPusher")
-async def test_fcm_additional_data(mock_wp, hass: HomeAssistant) -> None:
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
+async def test_fcm_additional_data(mock_wp: AsyncMock, hass: HomeAssistant) -> None:
"""Test if the gcm_key is only included for GCM endpoints."""
await async_setup_component(hass, "http", {})
- mock_wp().send().status_code = 201
data = {"chrome": SUBSCRIPTION_5}
@@ -234,12 +239,13 @@ async def test_fcm_additional_data(mock_wp, hass: HomeAssistant) -> None:
await service.async_send_message("Hello", data={"mykey": "myvalue"})
- assert len(mock_wp.mock_calls) == 4
+ mock_wp.send_async.assert_awaited_once_with(
+ data='{"badge": "/static/images/notification-badge.png", "body": "Hello", "data": {"mykey": "myvalue", "url": "/", "jwt": "JWT"}, "icon": "/static/icons/favicon-192x192.png", "tag": "12345678-1234-5678-1234-567812345678", "title": "Home Assistant", "timestamp": 1234567890000}',
+ headers=VAPID_HEADERS,
+ ttl=86400,
+ )
# WebPusher constructor
- assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"]
-
- # Get the keys passed to the WebPusher's send method
- assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal"
+ assert mock_wp.cls.call_args[0][0] == SUBSCRIPTION_5["subscription"]
@pytest.mark.usefixtures("load_config")
@@ -581,11 +587,14 @@ async def test_callback_view_no_jwt(
assert resp.status == HTTPStatus.UNAUTHORIZED
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
async def test_callback_view_with_jwt(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
config_entry: MockConfigEntry,
load_config: MagicMock,
+ mock_wp: AsyncMock,
) -> None:
"""Test that the notification callback view works with JWT."""
load_config.return_value = {"device": SUBSCRIPTION_1}
@@ -599,27 +608,22 @@ async def test_callback_view_with_jwt(
client = await hass_client()
- with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp:
- mock_wp().send().status_code = 201
- await hass.services.async_call(
- "notify",
- "html5",
- {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}},
- blocking=True,
- )
-
- assert len(mock_wp.mock_calls) == 4
+ await hass.services.async_call(
+ "notify",
+ "html5",
+ {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}},
+ blocking=True,
+ )
+ mock_wp.send_async.assert_awaited_once_with(
+ data='{"badge": "/static/images/notification-badge.png", "body": "Hello", "data": {"url": "/", "jwt": "JWT"}, "icon": "beer.png", "tag": "12345678-1234-5678-1234-567812345678", "title": "Home Assistant", "timestamp": 1234567890000}',
+ headers=VAPID_HEADERS,
+ ttl=86400,
+ )
# WebPusher constructor
- assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
+ assert mock_wp.cls.call_args[0][0] == SUBSCRIPTION_1["subscription"]
- # Call to send
- push_payload = json.loads(mock_wp.mock_calls[3][2]["data"])
-
- assert push_payload["body"] == "Hello"
- assert push_payload["icon"] == "beer.png"
-
- bearer_token = f"Bearer {push_payload['data']['jwt']}"
+ bearer_token = "Bearer JWT"
resp = await client.post(
PUBLISH_URL, json={"type": "push"}, headers={AUTHORIZATION: bearer_token}
@@ -630,10 +634,13 @@ async def test_callback_view_with_jwt(
assert body == {"event": "push", "status": "ok"}
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
async def test_send_fcm_without_targets(
hass: HomeAssistant,
config_entry: MockConfigEntry,
load_config: MagicMock,
+ mock_wp: AsyncMock,
) -> None:
"""Test that the notification is send with FCM without targets."""
load_config.return_value = {"device": SUBSCRIPTION_5}
@@ -645,25 +652,29 @@ async def test_send_fcm_without_targets(
assert config_entry.state is ConfigEntryState.LOADED
- with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp:
- mock_wp().send().status_code = 201
- await hass.services.async_call(
- "notify",
- "html5",
- {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}},
- blocking=True,
- )
-
- assert len(mock_wp.mock_calls) == 4
+ await hass.services.async_call(
+ "notify",
+ "html5",
+ {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}},
+ blocking=True,
+ )
+ mock_wp.send_async.assert_awaited_once_with(
+ data='{"badge": "/static/images/notification-badge.png", "body": "Hello", "data": {"url": "/", "jwt": "JWT"}, "icon": "beer.png", "tag": "12345678-1234-5678-1234-567812345678", "title": "Home Assistant", "timestamp": 1234567890000}',
+ headers=VAPID_HEADERS,
+ ttl=86400,
+ )
# WebPusher constructor
- assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"]
+ assert mock_wp.cls.call_args[0][0] == SUBSCRIPTION_5["subscription"]
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
async def test_send_fcm_expired(
hass: HomeAssistant,
config_entry: MockConfigEntry,
load_config: MagicMock,
+ mock_wp: AsyncMock,
) -> None:
"""Test that the FCM target is removed when expired."""
load_config.return_value = {"device": SUBSCRIPTION_5}
@@ -674,12 +685,10 @@ async def test_send_fcm_expired(
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
-
+ mock_wp.send_async.return_value.status = 410
with (
- patch("homeassistant.components.html5.notify.WebPusher") as mock_wp,
patch("homeassistant.components.html5.notify.save_json") as mock_save,
):
- mock_wp().send().status_code = 410
await hass.services.async_call(
"notify",
"html5",
@@ -690,11 +699,14 @@ async def test_send_fcm_expired(
mock_save.assert_called_once_with(hass.config.path(html5.REGISTRATIONS_FILE), {})
+@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
+@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
async def test_send_fcm_expired_save_fails(
hass: HomeAssistant,
config_entry: MockConfigEntry,
load_config: MagicMock,
caplog: pytest.LogCaptureFixture,
+ mock_wp: AsyncMock,
) -> None:
"""Test that the FCM target remains after expiry if save_json fails."""
load_config.return_value = {"device": SUBSCRIPTION_5}
@@ -705,16 +717,13 @@ async def test_send_fcm_expired_save_fails(
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
-
+ mock_wp.send_async.return_value.status = 410
with (
- patch("homeassistant.components.html5.notify.WebPusher") as mock_wp,
patch(
"homeassistant.components.html5.notify.save_json",
side_effect=HomeAssistantError(),
),
):
- mock_wp().send().status_code = 410
-
await hass.services.async_call(
"notify",
"html5",