mirror of
https://github.com/home-assistant/core.git
synced 2026-05-08 17:49:37 +01:00
Add support for action buttons to ntfy integration (#152014)
This commit is contained in:
@@ -81,6 +81,9 @@
|
||||
"service": "mdi:comment-remove"
|
||||
},
|
||||
"publish": {
|
||||
"sections": {
|
||||
"actions": "mdi:gesture-tap-button"
|
||||
},
|
||||
"service": "mdi:send"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,14 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NtfyConfigEntry
|
||||
from .entity import NtfyBaseEntity
|
||||
from .services import ATTR_ATTACH_FILE, ATTR_FILENAME, ATTR_SEQUENCE_ID
|
||||
from .services import (
|
||||
ACTIONS_MAP,
|
||||
ATTR_ACTION,
|
||||
ATTR_ACTIONS,
|
||||
ATTR_ATTACH_FILE,
|
||||
ATTR_FILENAME,
|
||||
ATTR_SEQUENCE_ID,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -105,6 +112,15 @@ class NtfyNotifyEntity(NtfyBaseEntity, NotifyEntity):
|
||||
|
||||
params.setdefault(ATTR_FILENAME, media.path.name)
|
||||
|
||||
actions: list[dict[str, Any]] | None = params.get(ATTR_ACTIONS)
|
||||
if actions:
|
||||
params["actions"] = [
|
||||
ACTIONS_MAP[action[ATTR_ACTION]](
|
||||
**{k: v for k, v in action.items() if k != ATTR_ACTION}
|
||||
)
|
||||
for action in actions
|
||||
]
|
||||
|
||||
msg = Message(topic=self.topic, **params)
|
||||
try:
|
||||
await self.ntfy.publish(msg, attachment)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from aiontfy import BroadcastAction, CopyAction, HttpAction, ViewAction
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
@@ -34,6 +35,28 @@ ATTR_ATTACH_FILE = "attach_file"
|
||||
ATTR_FILENAME = "filename"
|
||||
GRP_ATTACHMENT = "attachment"
|
||||
MSG_ATTACHMENT = "Only one attachment source is allowed: URL or local file"
|
||||
ATTR_ACTIONS = "actions"
|
||||
ATTR_ACTION = "action"
|
||||
ATTR_VIEW = "view"
|
||||
ATTR_BROADCAST = "broadcast"
|
||||
ATTR_HTTP = "http"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_URL = "url"
|
||||
ATTR_CLEAR = "clear"
|
||||
ATTR_INTENT = "intent"
|
||||
ATTR_EXTRAS = "extras"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_HEADERS = "headers"
|
||||
ATTR_BODY = "body"
|
||||
ATTR_VALUE = "value"
|
||||
ATTR_COPY = "copy"
|
||||
ACTIONS_MAP = {
|
||||
ATTR_VIEW: ViewAction,
|
||||
ATTR_BROADCAST: BroadcastAction,
|
||||
ATTR_HTTP: HttpAction,
|
||||
ATTR_COPY: CopyAction,
|
||||
}
|
||||
MAX_ACTIONS_ALLOWED = 3 # ntfy only supports up to 3 actions per notification
|
||||
|
||||
|
||||
def validate_filename(params: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -45,6 +68,40 @@ def validate_filename(params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
|
||||
ACTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_LABEL): cv.string,
|
||||
vol.Optional(ATTR_CLEAR, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
VIEW_SCHEMA = ACTION_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(ATTR_ACTION): vol.Equal("view"),
|
||||
vol.Required(ATTR_URL): vol.All(vol.Url(), vol.Coerce(URL)),
|
||||
}
|
||||
)
|
||||
BROADCAST_SCHEMA = ACTION_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(ATTR_ACTION): vol.Equal("broadcast"),
|
||||
vol.Optional(ATTR_INTENT): cv.string,
|
||||
vol.Optional(ATTR_EXTRAS): dict[str, str],
|
||||
}
|
||||
)
|
||||
HTTP_SCHEMA = VIEW_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(ATTR_ACTION): vol.Equal("http"),
|
||||
vol.Optional(ATTR_METHOD): cv.string,
|
||||
vol.Optional(ATTR_HEADERS): dict[str, str],
|
||||
vol.Optional(ATTR_BODY): cv.string,
|
||||
}
|
||||
)
|
||||
COPY_SCHEMA = ACTION_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(ATTR_ACTION): vol.Equal("copy"),
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_PUBLISH_SCHEMA = vol.All(
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
@@ -69,6 +126,14 @@ SERVICE_PUBLISH_SCHEMA = vol.All(
|
||||
ATTR_ATTACH_FILE, GRP_ATTACHMENT, MSG_ATTACHMENT
|
||||
): MediaSelector({"accept": ["*/*"]}),
|
||||
vol.Optional(ATTR_FILENAME): cv.string,
|
||||
vol.Optional(ATTR_ACTIONS): vol.All(
|
||||
cv.ensure_list,
|
||||
vol.Length(
|
||||
max=MAX_ACTIONS_ALLOWED,
|
||||
msg="Too many actions defined. A maximum of 3 is supported",
|
||||
),
|
||||
[vol.Any(VIEW_SCHEMA, BROADCAST_SCHEMA, HTTP_SCHEMA, COPY_SCHEMA)],
|
||||
),
|
||||
}
|
||||
),
|
||||
validate_filename,
|
||||
|
||||
@@ -99,6 +99,65 @@ publish:
|
||||
type: url
|
||||
autocomplete: url
|
||||
example: https://example.org/logo.png
|
||||
actions:
|
||||
selector:
|
||||
object:
|
||||
label_field: "label"
|
||||
description_field: "url"
|
||||
multiple: true
|
||||
translation_key: actions
|
||||
fields:
|
||||
action:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- value: view
|
||||
label: Open website/app
|
||||
- value: http
|
||||
label: Send HTTP request
|
||||
- value: broadcast
|
||||
label: Send Android broadcast
|
||||
- value: copy
|
||||
label: Copy to clipboard
|
||||
translation_key: action_type
|
||||
mode: dropdown
|
||||
label:
|
||||
selector:
|
||||
text:
|
||||
required: true
|
||||
clear:
|
||||
selector:
|
||||
boolean:
|
||||
url:
|
||||
selector:
|
||||
text:
|
||||
type: url
|
||||
method:
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
custom_value: true
|
||||
headers:
|
||||
selector:
|
||||
object:
|
||||
body:
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
intent:
|
||||
selector:
|
||||
text:
|
||||
extras:
|
||||
selector:
|
||||
object:
|
||||
value:
|
||||
selector:
|
||||
text:
|
||||
sequence_id:
|
||||
required: false
|
||||
selector:
|
||||
|
||||
@@ -318,6 +318,50 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"actions": {
|
||||
"fields": {
|
||||
"action": {
|
||||
"description": "Select the type of action to add to the notification",
|
||||
"name": "Action type"
|
||||
},
|
||||
"body": {
|
||||
"description": "The body of the HTTP request for `http` actions.",
|
||||
"name": "HTTP body"
|
||||
},
|
||||
"clear": {
|
||||
"description": "Clear notification after action button is tapped",
|
||||
"name": "Clear notification"
|
||||
},
|
||||
"extras": {
|
||||
"description": "Extras to include in the intent as key-value pairs for 'broadcast' actions",
|
||||
"name": "Intent extras"
|
||||
},
|
||||
"headers": {
|
||||
"description": "Additional HTTP headers as key-value pairs for 'http' actions",
|
||||
"name": "HTTP headers"
|
||||
},
|
||||
"intent": {
|
||||
"description": "Android intent to send when the 'broadcast' action is triggered",
|
||||
"name": "Intent"
|
||||
},
|
||||
"label": {
|
||||
"description": "Label of the action button",
|
||||
"name": "Label"
|
||||
},
|
||||
"method": {
|
||||
"description": "HTTP method to use for the 'http' action",
|
||||
"name": "HTTP method"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL to open for the 'view' action or to request for the 'http' action",
|
||||
"name": "URL"
|
||||
},
|
||||
"value": {
|
||||
"description": "Value to copy to clipboard when the 'copy' action is triggered",
|
||||
"name": "Value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": {
|
||||
"options": {
|
||||
"1": "Minimum",
|
||||
@@ -352,6 +396,10 @@
|
||||
"publish": {
|
||||
"description": "Publishes a notification message to a ntfy topic",
|
||||
"fields": {
|
||||
"actions": {
|
||||
"description": "Up to three actions ('view', 'broadcast', or 'http') can be added as buttons below the notification. Actions are executed when the corresponding button is tapped or clicked.",
|
||||
"name": "Action buttons"
|
||||
},
|
||||
"attach": {
|
||||
"description": "Attach images or other files by URL.",
|
||||
"name": "Attachment URL"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiontfy import Message
|
||||
from aiontfy import BroadcastAction, HttpAction, Message, ViewAction
|
||||
from aiontfy.exceptions import (
|
||||
NtfyException,
|
||||
NtfyHTTPError,
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components import camera, image, media_source
|
||||
from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TITLE
|
||||
from homeassistant.components.ntfy.const import DOMAIN
|
||||
from homeassistant.components.ntfy.services import (
|
||||
ATTR_ACTIONS,
|
||||
ATTR_ATTACH,
|
||||
ATTR_ATTACH_FILE,
|
||||
ATTR_CALL,
|
||||
@@ -69,6 +70,29 @@ async def test_ntfy_publish(
|
||||
ATTR_PRIORITY: "5",
|
||||
ATTR_TAGS: ["partying_face", "grin"],
|
||||
ATTR_SEQUENCE_ID: "Mc3otamDNcpJ",
|
||||
ATTR_ACTIONS: [
|
||||
{
|
||||
"action": "broadcast",
|
||||
"label": "Take picture",
|
||||
"intent": "com.example.AN_INTENT",
|
||||
"extras": {"cmd": "pic"},
|
||||
"clear": True,
|
||||
},
|
||||
{
|
||||
"action": "view",
|
||||
"label": "Open website",
|
||||
"url": "https://example.com",
|
||||
"clear": False,
|
||||
},
|
||||
{
|
||||
"action": "http",
|
||||
"label": "Close door",
|
||||
"url": "https://api.example.local/",
|
||||
"method": "PUT",
|
||||
"headers": {"Authorization": "Bearer ..."},
|
||||
"clear": False,
|
||||
},
|
||||
],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
@@ -86,6 +110,27 @@ async def test_ntfy_publish(
|
||||
icon=URL("https://example.org/logo.png"),
|
||||
delay="86430.0s",
|
||||
sequence_id="Mc3otamDNcpJ",
|
||||
actions=[
|
||||
BroadcastAction(
|
||||
label="Take picture",
|
||||
intent="com.example.AN_INTENT",
|
||||
extras={"cmd": "pic"},
|
||||
clear=True,
|
||||
),
|
||||
ViewAction(
|
||||
label="Open website",
|
||||
url=URL("https://example.com"),
|
||||
clear=False,
|
||||
),
|
||||
HttpAction(
|
||||
label="Close door",
|
||||
url=URL("https://api.example.local/"),
|
||||
method="PUT",
|
||||
headers={"Authorization": "Bearer ..."},
|
||||
body=None,
|
||||
clear=False,
|
||||
),
|
||||
],
|
||||
),
|
||||
None,
|
||||
)
|
||||
@@ -173,12 +218,24 @@ async def test_send_message_exception(
|
||||
},
|
||||
"Filename only allowed when attachment is provided",
|
||||
),
|
||||
(
|
||||
vol.MultipleInvalid,
|
||||
{
|
||||
ATTR_ACTIONS: [
|
||||
{"action": "broadcast", "label": "1"},
|
||||
{"action": "broadcast", "label": "2"},
|
||||
{"action": "broadcast", "label": "3"},
|
||||
{"action": "broadcast", "label": "4"},
|
||||
],
|
||||
},
|
||||
"Too many actions defined. A maximum of 3 is supported",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_aiontfy")
|
||||
async def test_send_message_validation_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_aiontfy: AsyncMock,
|
||||
payload: dict[str, Any],
|
||||
error_msg: str,
|
||||
exception: type[Exception],
|
||||
|
||||
Reference in New Issue
Block a user