Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions homeassistant/components/ntfy/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
},
"services": {
"publish": {
"sections": {
"actions": "mdi:gesture-tap-button"
},
"service": "mdi:send"
}
}
Expand Down
66 changes: 63 additions & 3 deletions homeassistant/components/ntfy/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import timedelta
from typing import Any

from aiontfy import Message
from aiontfy import BroadcastAction, HttpAction, Message, ViewAction
from aiontfy.exceptions import (
NtfyException,
NtfyHTTPError,
Expand All @@ -31,7 +31,7 @@
from .entity import NtfyBaseEntity

PARALLEL_UPDATES = 0

MAX_ACTIONS_ALLOWED = 3 # ntfy only supports up to 3 actions per notification

SERVICE_PUBLISH = "publish"
ATTR_ATTACH = "attach"
Expand All @@ -43,7 +43,53 @@
ATTR_MARKDOWN = "markdown"
ATTR_PRIORITY = "priority"
ATTR_TAGS = "tags"

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_POSITION = "position"
ATTR_INTENT = "intent"
ATTR_EXTRAS = "extras"
ATTR_METHOD = "method"
ATTR_HEADERS = "headers"
ATTR_BODY = "body"
ACTIONS_MAP = {
ATTR_VIEW: ViewAction,
ATTR_BROADCAST: BroadcastAction,
ATTR_HTTP: HttpAction,
}

ACTION_SCHEMA = vol.Schema(
{
vol.Required(ATTR_LABEL): cv.string,
vol.Optional(ATTR_CLEAR, default=False): cv.boolean,
}
)
VIEW_SCHEMA = ACTION_SCHEMA.extend(
{
vol.Optional(ATTR_ACTION): vol.All(str, "view"),
vol.Required(ATTR_URL): cv.url,
}
)
BROADCAST_SCHEMA = ACTION_SCHEMA.extend(
{
vol.Optional(ATTR_ACTION): vol.All(str, "broadcast"),
vol.Optional(ATTR_INTENT): cv.string,
vol.Optional(ATTR_EXTRAS): dict[str, str],
}
)
HTTP_SCHEMA = VIEW_SCHEMA.extend(
{
vol.Optional(ATTR_ACTION): vol.All(str, "http"),
vol.Optional(ATTR_METHOD): cv.string,
vol.Optional(ATTR_HEADERS): dict[str, str],
vol.Optional(ATTR_BODY): cv.string,
}
)
SERVICE_PUBLISH_SCHEMA = cv.make_entity_service_schema(
{
vol.Optional(ATTR_TITLE): cv.string,
Expand All @@ -60,6 +106,9 @@
vol.Optional(ATTR_EMAIL): vol.Email(),
vol.Optional(ATTR_CALL): cv.string,
vol.Optional(ATTR_ICON): vol.All(vol.Url(), vol.Coerce(URL)),
vol.Optional(ATTR_ACTIONS): vol.All(
cv.ensure_list, [vol.Any(VIEW_SCHEMA, BROADCAST_SCHEMA, HTTP_SCHEMA)]
),
}
)

Expand Down Expand Up @@ -116,6 +165,17 @@ async def publish(self, **kwargs: Any) -> None:
translation_key="delay_no_call",
)

actions: list[dict[str, Any]] | None = params.get(ATTR_ACTIONS)
if actions:
if len(actions) > MAX_ACTIONS_ALLOWED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="too_many_actions",
)
params["actions"] = [
ACTIONS_MAP[action.pop(ATTR_ACTION)](**action) for action in actions
]

msg = Message(topic=self.topic, **params)
try:
await self.ntfy.publish(msg)
Expand Down
55 changes: 55 additions & 0 deletions homeassistant/components/ntfy/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,58 @@ 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:
selector:
select:
options:
- value: view
label: Open website/app
- value: broadcast
label: Send Android broadcast
- value: http
label: Send HTTP request
translation_key: action_type
mode: dropdown
label:
selector:
text:
required: true
url:
selector:
text:
type: url
required: true
intent:
selector:
text:
required: true
extras:
selector:
object:
method:
selector:
select:
options:
- GET
- POST
- PUT
- DELETE
custom_value: true
headers:
selector:
object:
body:
selector:
text:
multiline: true
clear:
selector:
boolean:
20 changes: 20 additions & 0 deletions homeassistant/components/ntfy/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@
},
"timeout_error": {
"message": "Failed to connect to ntfy service due to a connection timeout"
},
"too_many_actions": {
"message": "Too many actions defined. A maximum of 3 is supported"
}
},
"issues": {
Expand All @@ -306,6 +309,19 @@
}
},
"selector": {
"actions": {
"fields": {
"action": "Action type",
"body": "Body of the HTTP request ('http' action only)",
"clear": "Clear notification after action button is tapped",
"extras": "Extras to include in the intent as key-value pairs ('broadcast' action only)",
"headers": "Additional HTTP headers as key-value pairs ('http' action only)",
"intent": "Android intent to send when the 'broadcast' action is triggered",
"label": "Label of the action button",
"method": "HTTP method to use for the 'http' action",
"url": "URL to open for the 'view' action or to request for the 'http' action"
}
},
"priority": {
"options": {
"1": "Minimum",
Expand All @@ -320,6 +336,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"
Expand Down
60 changes: 58 additions & 2 deletions tests/components/ntfy/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -14,6 +14,7 @@
from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TITLE
from homeassistant.components.ntfy.const import DOMAIN
from homeassistant.components.ntfy.notify import (
ATTR_ACTIONS,
ATTR_ATTACH,
ATTR_CALL,
ATTR_CLICK,
Expand Down Expand Up @@ -60,6 +61,29 @@ async def test_ntfy_publish(
ATTR_MARKDOWN: True,
ATTR_PRIORITY: "5",
ATTR_TAGS: ["partying_face", "grin"],
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,
)
Expand All @@ -76,6 +100,27 @@ async def test_ntfy_publish(
markdown=True,
icon=URL("https://example.org/logo.png"),
delay="86430.0s",
actions=[
BroadcastAction(
label="Take picture",
intent="com.example.AN_INTENT",
extras={"cmd": "pic"},
clear=True,
),
ViewAction(
label="Open website",
url="https://example.com",
clear=False,
),
HttpAction(
label="Close door",
url="https://api.example.local/",
method="PUT",
headers={"Authorization": "Bearer ..."},
body=None,
clear=False,
),
],
)
)

Expand Down Expand Up @@ -142,12 +187,23 @@ async def test_send_message_exception(
{ATTR_DELAY: {"days": 1, "seconds": 30}, ATTR_EMAIL: "[email protected]"},
"Delayed email notifications are not supported",
),
(
{
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,
) -> None:
Expand Down
Loading