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:
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"services": {
|
||||
"dismiss": {
|
||||
"service": "mdi:bell-off"
|
||||
},
|
||||
"send_message": {
|
||||
"service": "mdi:message-arrow-right"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
homeassistant/components/html5/issue.py
Normal file
31
homeassistant/components/html5/issue.py
Normal 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},
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
82
homeassistant/components/html5/services.py
Normal file
82
homeassistant/components/html5/services.py
Normal 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",
|
||||
)
|
||||
@@ -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'}"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user