Repository: home-assistant/core
File: homeassistant/components/ntfy/notify.py (Python)

Code before:
ATTR_EMAIL = "email"
ATTR_ICON = "icon"
ATTR_MARKDOWN = "markdown"
ATTR_PRIORITY = "priority"
ATTR_TAGS = "tags"
ATTR_ATTACH_FILE = "attach_file"
ATTR_FILENAME = "filename"

SERVICE_PUBLISH_SCHEMA = cv.make_entity_service_schema(
    {
        vol.Optional(ATTR_TITLE): cv.string,
        vol.Optional(ATTR_MESSAGE): cv.string,
        vol.Optional(ATTR_MARKDOWN): cv.boolean,
        vol.Optional(ATTR_TAGS): vol.All(cv.ensure_list, [str]),
        vol.Optional(ATTR_PRIORITY): vol.All(vol.Coerce(int), vol.Range(1, 5)),
        vol.Optional(ATTR_CLICK): vol.All(vol.Url(), vol.Coerce(URL)),
        vol.Optional(ATTR_DELAY): vol.All(
            cv.time_period,
            vol.Range(min=timedelta(seconds=10), max=timedelta(days=3)),
        ),
        vol.Optional(ATTR_ATTACH): vol.All(vol.Url(), vol.Coerce(URL)),
        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_ATTACH_FILE): MediaSelector({"accept": ["*/*"]}),
        vol.Optional(ATTR_FILENAME): cv.string,
    }
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: NtfyConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the ntfy notification entity platform."""

    for subentry_id, subentry in config_entry.subentries.items():
        async_add_entities(
            [NtfyNotifyEntity(config_entry, subentry)], config_subentry_id=subentry_id
        )

    platform = entity_platform.async_get_current_platform()
    platform.async_register_entity_service(
        SERVICE_PUBLISH,
        SERVICE_PUBLISH_SCHEMA,
        "publish",
        description_placeholders={
            "markdown_guide_url": "https://www.markdownguide.org/basic-syntax/"
        },
    )

Review comment (suggestion):
Let's add another validator that checks that `ATTR_ATTACH_FILE` is present if `ATTR_FILENAME` is present, since `ATTR_FILENAME` is only valid in that case.

Code after:
ATTR_CALL = "call"
ATTR_CLICK = "click"
ATTR_DELAY = "delay"
ATTR_EMAIL = "email"
ATTR_ICON = "icon"
ATTR_MARKDOWN = "markdown"
ATTR_PRIORITY = "priority"
ATTR_TAGS = "tags"
ATTR_SEQUENCE_ID = "sequence_id"
ATTR_ATTACH_FILE = "attach_file"
ATTR_FILENAME = "filename"
GRP_ATTACHMENT = "attachment"
MSG_ATTACHMENT = "Only one attachment source is allowed: URL or local file"


def validate_filename(params: dict[str, Any]) -> dict[str, Any]:
    """Validate filename."""
    if ATTR_FILENAME in params and not (
        ATTR_ATTACH_FILE in params or ATTR_ATTACH in params
    ):
        raise vol.Invalid("Filename only allowed when attachment is provided")
    return params


SERVICE_PUBLISH_SCHEMA = vol.All(
    cv.make_entity_service_schema(
        {
            vol.Optional(ATTR_TITLE): cv.string,
            vol.Optional(ATTR_MESSAGE): cv.string,
            vol.Optional(ATTR_MARKDOWN): cv.boolean,
            vol.Optional(ATTR_TAGS): vol.All(cv.ensure_list, [str]),
            vol.Optional(ATTR_PRIORITY): vol.All(vol.Coerce(int), vol.Range(1, 5)),
            vol.Optional(ATTR_CLICK): vol.All(vol.Url(), vol.Coerce(URL)),
            vol.Optional(ATTR_DELAY): vol.All(
                cv.time_period,
                vol.Range(min=timedelta(seconds=10), max=timedelta(days=3)),
            ),
            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_SEQUENCE_ID): cv.string,
            vol.Exclusive(ATTR_ATTACH, GRP_ATTACHMENT, MSG_ATTACHMENT): vol.All(
                vol.Url(), vol.Coerce(URL)
            ),
            vol.Exclusive(
                ATTR_ATTACH_FILE, GRP_ATTACHMENT, MSG_ATTACHMENT
            ): MediaSelector({"accept": ["*/*"]}),
            vol.Optional(ATTR_FILENAME): cv.string,
        }
    ),
    validate_filename,