1
0
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:
Manu
2026-02-23 21:46:00 +01:00
committed by GitHub
parent bea84151b1
commit fb118ed516
6 changed files with 251 additions and 3 deletions
+3
View File
@@ -81,6 +81,9 @@
"service": "mdi:comment-remove"
},
"publish": {
"sections": {
"actions": "mdi:gesture-tap-button"
},
"service": "mdi:send"
}
}
+17 -1
View File
@@ -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)
+65
View File
@@ -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"
+59 -2
View File
@@ -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],