1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Add entity action html5.send_message to HTML5 integration (#166349)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Manu
2026-03-30 19:49:59 +02:00
committed by GitHub
parent dc111a475e
commit 52af74c3b6
9 changed files with 641 additions and 38 deletions

View File

@@ -4,14 +4,23 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.EVENT, Platform.NOTIFY]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the HTML5 services."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HTML5 from a config entry."""
hass.async_create_task(

View File

@@ -11,5 +11,18 @@ ATTR_VAPID_EMAIL = "vapid_email"
REGISTRATIONS_FILE = "html5_push_registrations.conf"
ATTR_ACTION = "action"
ATTR_ACTIONS = "actions"
ATTR_BADGE = "badge"
ATTR_DATA = "data"
ATTR_DIR = "dir"
ATTR_ICON = "icon"
ATTR_IMAGE = "image"
ATTR_LANG = "lang"
ATTR_RENOTIFY = "renotify"
ATTR_REQUIRE_INTERACTION = "require_interaction"
ATTR_SILENT = "silent"
ATTR_TAG = "tag"
ATTR_TIMESTAMP = "timestamp"
ATTR_TTL = "ttl"
ATTR_URGENCY = "urgency"
ATTR_VIBRATE = "vibrate"

View File

@@ -9,6 +9,9 @@
"services": {
"dismiss": {
"service": "mdi:bell-off"
},
"send_message": {
"service": "mdi:message-arrow-right"
}
}
}

View File

@@ -0,0 +1,31 @@
"""Issues for HTML5 integration."""
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.util import slugify
from .const import DOMAIN
@callback
def deprecated_notify_action_call(
hass: HomeAssistant, target: list[str] | None
) -> None:
"""Deprecated action call."""
action = (
f"notify.html5_{slugify(target[0])}"
if target and len(target) == 1
else "notify.html5"
)
async_create_issue(
hass,
DOMAIN,
f"deprecated_notify_action_{action}",
breaks_in_ha_version="2026.11.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_notify_action",
translation_placeholders={"action": action},
)

View File

@@ -47,7 +47,11 @@ from homeassistant.util.json import load_json_object
from .const import (
ATTR_ACTION,
ATTR_ACTIONS,
ATTR_REQUIRE_INTERACTION,
ATTR_TAG,
ATTR_TIMESTAMP,
ATTR_TTL,
ATTR_VAPID_EMAIL,
ATTR_VAPID_PRV_KEY,
ATTR_VAPID_PUB_KEY,
@@ -56,6 +60,7 @@ from .const import (
SERVICE_DISMISS,
)
from .entity import HTML5Entity, Registration
from .issue import deprecated_notify_action_call
_LOGGER = logging.getLogger(__name__)
@@ -69,13 +74,11 @@ ATTR_AUTH = "auth"
ATTR_P256DH = "p256dh"
ATTR_EXPIRATIONTIME = "expirationTime"
ATTR_ACTIONS = "actions"
ATTR_TYPE = "type"
ATTR_URL = "url"
ATTR_DISMISS = "dismiss"
ATTR_PRIORITY = "priority"
DEFAULT_PRIORITY = "normal"
ATTR_TTL = "ttl"
DEFAULT_TTL = 86400
DEFAULT_BADGE = "/static/images/notification-badge.png"
@@ -465,6 +468,9 @@ class HTML5NotificationService(BaseNotificationService):
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to a user."""
deprecated_notify_action_call(self.hass, kwargs.get(ATTR_TARGET))
tag = str(uuid.uuid4())
payload: dict[str, Any] = {
"badge": DEFAULT_BADGE,
@@ -605,32 +611,53 @@ class HTML5NotifyEntity(HTML5Entity, NotifyEntity):
_key = "device"
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to a device."""
timestamp = int(time.time())
tag = str(uuid.uuid4())
"""Send a message to a device via notify.send_message action."""
await self._webpush(
title=title or ATTR_TITLE_DEFAULT,
message=message,
badge=DEFAULT_BADGE,
icon=DEFAULT_ICON,
)
payload: dict[str, Any] = {
"badge": DEFAULT_BADGE,
"body": message,
"icon": DEFAULT_ICON,
ATTR_TAG: tag,
ATTR_TITLE: title or ATTR_TITLE_DEFAULT,
"timestamp": timestamp * 1000,
ATTR_DATA: {
ATTR_JWT: add_jwt(
timestamp,
async def send_push_notification(self, **kwargs: Any) -> None:
"""Send a message to a device via html5.send_message action."""
await self._webpush(**kwargs)
self._async_record_notification()
async def _webpush(
self,
message: str | None = None,
timestamp: datetime | None = None,
ttl: timedelta | None = None,
urgency: str | None = None,
**kwargs: Any,
) -> None:
"""Shared internal helper to push messages."""
payload: dict[str, Any] = kwargs
if message is not None:
payload["body"] = message
payload.setdefault(ATTR_TAG, str(uuid.uuid4()))
ts = int(timestamp.timestamp()) if timestamp else int(time.time())
payload[ATTR_TIMESTAMP] = ts * 1000
if ATTR_REQUIRE_INTERACTION in payload:
payload["requireInteraction"] = payload.pop(ATTR_REQUIRE_INTERACTION)
payload.setdefault(ATTR_DATA, {})
payload[ATTR_DATA][ATTR_JWT] = add_jwt(
ts,
self.target,
tag,
payload[ATTR_TAG],
self.registration["subscription"]["keys"]["auth"],
)
},
}
endpoint = urlparse(self.registration["subscription"]["endpoint"])
vapid_claims = {
"sub": f"mailto:{self.config_entry.data[ATTR_VAPID_EMAIL]}",
"aud": f"{endpoint.scheme}://{endpoint.netloc}",
"exp": timestamp + (VAPID_CLAIM_VALID_HOURS * 60 * 60),
"exp": ts + (VAPID_CLAIM_VALID_HOURS * 60 * 60),
}
try:
@@ -639,6 +666,8 @@ class HTML5NotifyEntity(HTML5Entity, NotifyEntity):
json.dumps(payload),
self.config_entry.data[ATTR_VAPID_PRV_KEY],
vapid_claims,
ttl=int(ttl.total_seconds()) if ttl is not None else DEFAULT_TTL,
headers={"Urgency": urgency} if urgency else None,
aiohttp_session=self.session,
)
cast(ClientResponse, response).raise_for_status()

View File

@@ -0,0 +1,82 @@
"""Service registration for HTML5 integration."""
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_MESSAGE,
ATTR_TITLE,
ATTR_TITLE_DEFAULT,
DOMAIN as NOTIFY_DOMAIN,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import (
ATTR_ACTION,
ATTR_ACTIONS,
ATTR_BADGE,
ATTR_DIR,
ATTR_ICON,
ATTR_IMAGE,
ATTR_LANG,
ATTR_RENOTIFY,
ATTR_REQUIRE_INTERACTION,
ATTR_SILENT,
ATTR_TAG,
ATTR_TIMESTAMP,
ATTR_TTL,
ATTR_URGENCY,
ATTR_VIBRATE,
DOMAIN,
)
SERVICE_SEND_MESSAGE = "send_message"
SERVICE_SEND_MESSAGE_SCHEMA = cv.make_entity_service_schema(
{
vol.Required(ATTR_TITLE, default=ATTR_TITLE_DEFAULT): cv.string,
vol.Optional(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_DIR): vol.In({"auto", "ltr", "rtl"}),
vol.Optional(ATTR_ICON): cv.string,
vol.Optional(ATTR_BADGE): cv.string,
vol.Optional(ATTR_IMAGE): cv.string,
vol.Optional(ATTR_TAG): cv.string,
vol.Exclusive(ATTR_VIBRATE, "silent_xor_vibrate"): vol.All(
cv.ensure_list,
[vol.All(vol.Coerce(int), vol.Range(min=0))],
),
vol.Optional(ATTR_TIMESTAMP): cv.datetime,
vol.Optional(ATTR_LANG): cv.language,
vol.Exclusive(ATTR_SILENT, "silent_xor_vibrate"): cv.boolean,
vol.Optional(ATTR_RENOTIFY): cv.boolean,
vol.Optional(ATTR_REQUIRE_INTERACTION): cv.boolean,
vol.Optional(ATTR_URGENCY): vol.In({"normal", "high", "low"}),
vol.Optional(ATTR_TTL): vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(ATTR_ACTIONS): vol.All(
cv.ensure_list,
[
{
vol.Required(ATTR_ACTION): cv.string,
vol.Required(ATTR_TITLE): cv.string,
vol.Optional(ATTR_ICON): cv.string,
}
],
),
vol.Optional(ATTR_DATA): dict,
}
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for HTML5 integration."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SEND_MESSAGE,
entity_domain=NOTIFY_DOMAIN,
schema=SERVICE_SEND_MESSAGE_SCHEMA,
func="send_push_notification",
)

View File

@@ -8,3 +8,137 @@ dismiss:
example: '{ "tag": "tagname" }'
selector:
object:
send_message:
target:
entity:
domain: notify
integration: html5
fields:
title:
required: true
selector:
text:
example: Home Assistant
default: Home Assistant
message:
required: false
selector:
text:
multiline: true
example: Hello World
icon:
required: false
selector:
text:
type: url
example: /static/icons/favicon-192x192.png
badge:
required: false
selector:
text:
type: url
example: /static/images/notification-badge.png
image:
required: false
selector:
text:
type: url
example: /static/images/image.jpg
tag:
required: false
selector:
text:
example: message-group-1
actions:
selector:
object:
label_field: "action"
description_field: "title"
multiple: true
translation_key: actions
fields:
action:
required: true
selector:
text:
title:
required: true
selector:
text:
icon:
selector:
text:
type: url
example: '[{"action": "test-action", "title": "🆗 Click here!", "icon": "/images/action-1-128x128.png"}]'
dir:
required: false
selector:
select:
options:
- auto
- ltr
- rtl
mode: dropdown
translation_key: dir
example: auto
renotify:
required: false
selector:
constant:
value: true
label: ""
example: true
silent:
required: false
selector:
constant:
value: true
label: ""
example: true
require_interaction:
required: false
selector:
constant:
value: true
label: ""
example: true
vibrate:
required: false
selector:
text:
multiple: true
type: number
suffix: ms
example: "[125,75,125,275,200,275,125,75,125,275,200,600,200,600]"
lang:
required: false
selector:
language:
example: es-419
timestamp:
required: false
selector:
datetime:
example: "1970-01-01 00:00:00"
ttl:
required: false
selector:
duration:
enable_day: true
example: "{'days': 28}"
urgency:
required: false
selector:
select:
options:
- low
- normal
- high
mode: dropdown
translation_key: urgency
example: normal
data:
required: false
selector:
object:
example: "{'customKey': 'customValue'}"

View File

@@ -48,6 +48,44 @@
"message": "Sending notification to {target} failed due to a request error"
}
},
"issues": {
"deprecated_notify_action": {
"description": "The action `{action}` is deprecated and will be removed in a future release.\n\nPlease update your automations and scripts to use the notify entities with the `notify.send_message` or `html5.send_message` actions instead.",
"title": "Detected use of deprecated action {action}"
}
},
"selector": {
"actions": {
"fields": {
"action": {
"description": "The identifier of the action. This will be sent back to Home Assistant when the user clicks the button.",
"name": "Action identifier"
},
"icon": {
"description": "URL of an image displayed as the icon for this button.",
"name": "Icon"
},
"title": {
"description": "The label of the button displayed to the user.",
"name": "Title"
}
}
},
"dir": {
"options": {
"auto": "[%key:common::state::auto%]",
"ltr": "Left-to-right",
"rtl": "Right-to-left"
}
},
"urgency": {
"options": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"normal": "[%key:common::state::normal%]"
}
}
},
"services": {
"dismiss": {
"description": "Dismisses an HTML5 notification.",
@@ -62,6 +100,80 @@
}
},
"name": "Dismiss"
},
"send_message": {
"description": "Sends a message via HTML5 Push Notifications",
"fields": {
"actions": {
"description": "Adds action buttons to the notification. When the user clicks a button, an event is sent back to Home Assistant. Amount of actions supported may vary between platforms.",
"name": "Action buttons"
},
"badge": {
"description": "URL or relative path of a small image to replace the browser icon on mobile platforms. Maximum size is 96px by 96px",
"name": "Badge"
},
"data": {
"description": "Additional custom key-value pairs to include in the payload of the push message. This can be used to include extra information that can be accessed in the notification events.",
"name": "Extra data"
},
"dir": {
"description": "The direction of the notification's text. Adopts the browser's language setting behavior by default.",
"name": "Text direction"
},
"icon": {
"description": "URL or relative path of an image to display as the main icon in the notification. Maximum size is 320px by 320px.",
"name": "Icon"
},
"image": {
"description": "URL or relative path of a larger image to display in the main body of the notification. Experimental support, may not be displayed on all platforms.",
"name": "Image"
},
"lang": {
"description": "The language of the notification's content.",
"name": "Language"
},
"message": {
"description": "The message body of the notification.",
"name": "Message"
},
"renotify": {
"description": "If enabled, the user will be alerted again (sound/vibration) when a notification with the same tag replaces a previous one.",
"name": "Renotify"
},
"require_interaction": {
"description": "If enabled, the notification will remain active until the user clicks or dismisses it, rather than automatically closing after a few seconds. This provides the same behavior on desktop as on mobile platforms.",
"name": "Require interaction"
},
"silent": {
"description": "If enabled, the notification will not play sounds or trigger vibration, regardless of the device's notification settings.",
"name": "Silent"
},
"tag": {
"description": "The identifier of the notification. Sending a new notification with the same tag will replace the existing one. If not specified, a unique tag will be generated for each notification.",
"name": "Tag"
},
"timestamp": {
"description": "The timestamp of the notification. By default, it uses the time when the notification is sent.",
"name": "Timestamp"
},
"title": {
"description": "Title for your notification message.",
"name": "Title"
},
"ttl": {
"description": "Specifies how long the push service should retain the message if the user's browser or device is offline. After this period, the notification expires. A value of 0 means the notification is discarded immediately if the target is not connected. Defaults to 1 day.",
"name": "Time to live"
},
"urgency": {
"description": "Whether the push service should try to deliver the notification immediately or defer it in accordance with the user's power saving preferences.",
"name": "Urgency"
},
"vibrate": {
"description": "A vibration pattern to run with the notification. An array of integers representing alternating periods of vibration and silence in milliseconds. For example, [200, 100, 200] would vibrate for 200ms, pause for 100ms, then vibrate for another 200ms.",
"name": "Vibration pattern"
}
},
"name": "Send message"
}
}
}

View File

@@ -3,6 +3,7 @@
from collections.abc import Generator
from http import HTTPStatus
import json
from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch
from aiohttp import ClientError
@@ -11,9 +12,28 @@ import pytest
from pywebpush import WebPushException
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.html5 import notify as html5
from homeassistant.components.html5 import DOMAIN, notify as html5
from homeassistant.components.html5.const import (
ATTR_ACTIONS,
ATTR_BADGE,
ATTR_DIR,
ATTR_ICON,
ATTR_IMAGE,
ATTR_LANG,
ATTR_RENOTIFY,
ATTR_REQUIRE_INTERACTION,
ATTR_SILENT,
ATTR_TAG,
ATTR_TIMESTAMP,
ATTR_TTL,
ATTR_URGENCY,
ATTR_VIBRATE,
)
from homeassistant.components.html5.notify import ATTR_ACTION, DEFAULT_TTL
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_MESSAGE,
ATTR_TARGET,
ATTR_TITLE,
DOMAIN as NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
@@ -27,7 +47,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, snapshot_platform
@@ -817,19 +837,16 @@ async def test_send_message(
webpush_async.assert_awaited_once()
assert webpush_async.await_args
assert webpush_async.await_args.args == (
{
"endpoint": "https://googleapis.com",
"keys": {"auth": "auth", "p256dh": "p256dh"},
},
'{"badge": "/static/images/notification-badge.png", "body": "World", "icon": "/static/icons/favicon-192x192.png", "tag": "12345678-1234-5678-1234-567812345678", "title": "Hello", "timestamp": 1234567890000, "data": {"jwt": "JWT"}}',
"h6acSRds8_KR8hT9djD8WucTL06Gfe29XXyZ1KcUjN8",
{
"sub": "mailto:test@example.com",
"aud": "https://googleapis.com",
"exp": 1234611090,
},
)
_, payload, _, _ = webpush_async.await_args.args
assert json.loads(payload) == {
"title": "Hello",
"body": "World",
"badge": "/static/images/notification-badge.png",
"icon": "/static/icons/favicon-192x192.png",
"tag": "12345678-1234-5678-1234-567812345678",
"timestamp": 1234567890000,
"data": {"jwt": "JWT"},
}
@pytest.mark.parametrize(
@@ -849,6 +866,7 @@ async def test_send_message(
),
],
)
@pytest.mark.parametrize("domain", [NOTIFY_DOMAIN, DOMAIN])
@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
async def test_send_message_exceptions(
@@ -858,6 +876,7 @@ async def test_send_message_exceptions(
load_config: MagicMock,
exception: Exception,
translation_key: str,
domain: str,
) -> None:
"""Test sending a message with exceptions."""
load_config.return_value = {"my-desktop": SUBSCRIPTION_1}
@@ -872,7 +891,7 @@ async def test_send_message_exceptions(
with pytest.raises(HomeAssistantError) as e:
await hass.services.async_call(
NOTIFY_DOMAIN,
domain,
SERVICE_SEND_MESSAGE,
{
ATTR_ENTITY_ID: "notify.my_desktop",
@@ -963,3 +982,174 @@ async def test_send_message_unavailable(
state = hass.states.get("notify.my_desktop")
assert state
assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize(
("service_data", "expected_payload", "expected_ttl", "expected_headers"),
[
({ATTR_MESSAGE: "World"}, {"body": "World"}, DEFAULT_TTL, None),
(
{ATTR_ICON: "/static/icons/favicon-192x192.png"},
{"icon": "/static/icons/favicon-192x192.png"},
DEFAULT_TTL,
None,
),
(
{ATTR_BADGE: "/static/images/notification-badge.png"},
{"badge": "/static/images/notification-badge.png"},
DEFAULT_TTL,
None,
),
(
{ATTR_IMAGE: "/static/images/image.jpg"},
{"image": "/static/images/image.jpg"},
DEFAULT_TTL,
None,
),
({ATTR_TAG: "message-group-1"}, {"tag": "message-group-1"}, DEFAULT_TTL, None),
({ATTR_DIR: "rtl"}, {"dir": "rtl"}, DEFAULT_TTL, None),
({ATTR_RENOTIFY: True}, {"renotify": True}, DEFAULT_TTL, None),
({ATTR_SILENT: True}, {"silent": True}, DEFAULT_TTL, None),
(
{ATTR_REQUIRE_INTERACTION: True},
{"requireInteraction": True},
DEFAULT_TTL,
None,
),
(
{ATTR_VIBRATE: [200, 100, 200]},
{"vibrate": [200, 100, 200]},
DEFAULT_TTL,
None,
),
({ATTR_LANG: "es-419"}, {"lang": "es-419"}, DEFAULT_TTL, None),
({ATTR_TIMESTAMP: "1970-01-01 00:00:00"}, {"timestamp": 0}, DEFAULT_TTL, None),
({ATTR_TTL: {"days": 28}}, {}, 2419200, None),
({ATTR_TTL: {"seconds": 0}}, {}, 0, None),
(
{ATTR_URGENCY: "high"},
{},
DEFAULT_TTL,
{"Urgency": "high"},
),
(
{
ATTR_ACTIONS: [
{
ATTR_ACTION: "callback-event",
ATTR_TITLE: "Callback Event",
ATTR_ICON: "/static/icons/favicon-192x192.png",
}
]
},
{
"actions": [
{
"action": "callback-event",
"title": "Callback Event",
"icon": "/static/icons/favicon-192x192.png",
}
]
},
DEFAULT_TTL,
None,
),
(
{ATTR_DATA: {"customKey": "customValue"}},
{"data": {"jwt": "JWT", "customKey": "customValue"}},
DEFAULT_TTL,
None,
),
],
)
@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
async def test_html5_send_message(
hass: HomeAssistant,
config_entry: MockConfigEntry,
webpush_async: AsyncMock,
load_config: MagicMock,
service_data: dict[str, Any],
expected_payload: dict[str, Any],
expected_ttl: int,
expected_headers: dict[str, Any] | None,
) -> None:
"""Test sending a message via html5.send_message action."""
load_config.return_value = {"my-desktop": SUBSCRIPTION_1}
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
state = hass.states.get("notify.my_desktop")
assert state
assert state.state == STATE_UNKNOWN
await hass.services.async_call(
DOMAIN,
SERVICE_SEND_MESSAGE,
{ATTR_ENTITY_ID: "notify.my_desktop", ATTR_TITLE: "Hello", **service_data},
blocking=True,
)
state = hass.states.get("notify.my_desktop")
assert state
assert state.state == "2009-02-13T23:31:30+00:00"
webpush_async.assert_awaited_once()
assert webpush_async.await_args
_, payload, _, _ = webpush_async.await_args.args
assert json.loads(payload) == {
"title": "Hello",
"tag": "12345678-1234-5678-1234-567812345678",
"timestamp": 1234567890000,
"data": {"jwt": "JWT"},
**expected_payload,
}
assert webpush_async.await_args.kwargs["ttl"] == expected_ttl
assert webpush_async.await_args.kwargs["headers"] == expected_headers
@pytest.mark.parametrize(
("target", "issue_id"),
[
(["my-desktop"], "deprecated_notify_action_notify.html5_my_desktop"),
(None, "deprecated_notify_action_notify.html5"),
(["my-desktop", "my-phone"], "deprecated_notify_action_notify.html5"),
],
)
@pytest.mark.usefixtures("mock_wp", "mock_jwt", "mock_vapid", "mock_uuid")
async def test_deprecation_action_call(
hass: HomeAssistant,
config_entry: MockConfigEntry,
load_config: MagicMock,
issue_registry: ir.IssueRegistry,
target: list[str] | None,
issue_id: str,
) -> None:
"""Test deprecation action call."""
load_config.return_value = {
"my-desktop": SUBSCRIPTION_1,
"my-phone": SUBSCRIPTION_2,
}
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
NOTIFY_DOMAIN,
DOMAIN,
{ATTR_MESSAGE: "Hello", ATTR_TARGET: target},
blocking=True,
)
assert issue_registry.async_get_issue(
domain=DOMAIN,
issue_id=issue_id,
)