Skip to content

Commit

Permalink
Add ntfy integration
Browse files Browse the repository at this point in the history
  • Loading branch information
tr4nt0r committed Jan 9, 2025
1 parent de9c05a commit 9757da4
Show file tree
Hide file tree
Showing 18 changed files with 732 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,8 @@ build.json @home-assistant/supervisor
/tests/components/nsw_fuel_station/ @nickw444
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
/tests/components/nsw_rural_fire_service_feed/ @exxamalte
/homeassistant/components/ntfy/ @tr4nt0r
/tests/components/ntfy/ @tr4nt0r
/homeassistant/components/nuheat/ @tstabrawa
/tests/components/nuheat/ @tstabrawa
/homeassistant/components/nuki/ @pschmitt @pvizeli @pree
Expand Down
31 changes: 31 additions & 0 deletions homeassistant/components/ntfy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""The ntfy integration."""

from __future__ import annotations

from aiontfy import Ntfy

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_URL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession

PLATFORMS: list[Platform] = [Platform.NOTIFY]


type NtfyConfigEntry = ConfigEntry[Ntfy]


async def async_setup_entry(hass: HomeAssistant, entry: NtfyConfigEntry) -> bool:
"""Set up ntfy from a config entry."""

session = async_get_clientsession(hass)
entry.runtime_data = Ntfy(entry.data[CONF_URL], session)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: NtfyConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
76 changes: 76 additions & 0 deletions homeassistant/components/ntfy/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Config flow for the ntfy integration."""

from __future__ import annotations

import logging
import random
import string
from typing import Any

from aiontfy import Message, Ntfy
from aiontfy.exceptions import NtfyException, NtfyForbiddenAccessError, NtfyHTTPError
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_URL
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import CONF_TOPIC, DEFAULT_URL, DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL, default=DEFAULT_URL): str,
vol.Optional(CONF_TOPIC): str,
}
)


class NtfyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for ntfy."""

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
if not user_input.get(CONF_TOPIC):
user_input[CONF_TOPIC] = "".join(
random.choices(
string.ascii_lowercase + string.ascii_uppercase + string.digits,
k=16,
)
)
self._async_abort_entries_match(user_input)
try:
session = async_get_clientsession(self.hass)
ntfy = Ntfy(user_input[CONF_URL], session)
await ntfy.publish(
Message(
topic=user_input[CONF_TOPIC],
title="Home Assistant",
message="The Home Assistant ntfy integration has been successfully set up for this topic.",
)
)
except NtfyForbiddenAccessError:
errors["base"] = "forbidden_topic"
except NtfyHTTPError as e:
_LOGGER.debug("Error %s: %s [%s]", e.code, e.error, e.link)
errors["base"] = "cannot_connect"
except NtfyException:
errors["base"] = "cannot_connect"
except ValueError:
errors["base"] = "invalid_url"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=user_input[CONF_TOPIC], data=user_input
)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
8 changes: 8 additions & 0 deletions homeassistant/components/ntfy/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Constants for the ntfy integration."""

from typing import Final

DOMAIN = "ntfy"
DEFAULT_URL: Final = "https://ntfy.sh"

CONF_TOPIC = "topic"
9 changes: 9 additions & 0 deletions homeassistant/components/ntfy/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"entity": {
"notify": {
"publish": {
"default": "mdi:console-line"
}
}
}
}
11 changes: 11 additions & 0 deletions homeassistant/components/ntfy/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "ntfy",
"name": "ntfy",
"codeowners": ["@tr4nt0r"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ntfy",
"iot_class": "cloud_push",
"loggers": ["aionfty"],
"quality_scale": "silver",
"requirements": ["aiontfy==0.2.1"]
}
77 changes: 77 additions & 0 deletions homeassistant/components/ntfy/notify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""ntfy notification entity."""

from __future__ import annotations

from aiontfy import Message
from aiontfy.exceptions import NtfyException, NtfyHTTPError

from homeassistant.components.notify import (
NotifyEntity,
NotifyEntityDescription,
NotifyEntityFeature,
)
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import NtfyConfigEntry
from .const import CONF_TOPIC, DEFAULT_URL, DOMAIN

PARALLEL_UPDATES = 0


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

async_add_entities([NtfyNotifyEntity(config_entry, config_entry.data[CONF_TOPIC])])


class NtfyNotifyEntity(NotifyEntity):
"""Representation of a ntfy notification entity."""

entity_description = NotifyEntityDescription(
key="publish",
translation_key="publish",
name=None,
has_entity_name=True,
)

def __init__(self, config_entry: NtfyConfigEntry, topic: str) -> None:
"""Initialize a notification entity."""

self._attr_unique_id = f"{config_entry.entry_id}_{self.entity_description.key}"
self._attr_supported_features = NotifyEntityFeature.TITLE
self.device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer="ntfy LLC",
model="ntfy",
model_id=config_entry.data[CONF_URL],
name=topic,
configuration_url=f"{DEFAULT_URL}/{topic}",
identifiers={(DOMAIN, config_entry.entry_id)},
)
self.ntfy = config_entry.runtime_data
self.topic = topic

async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Publish a message to a topic."""
try:
msg = Message(topic=self.topic, message=message, title=title)
await self.ntfy.publish(msg)
except NtfyHTTPError as e:
raise HomeAssistantError(
translation_key="publish_failed_request_error",
translation_domain=DOMAIN,
translation_placeholders={"error_msg": e.error},
) from e
except NtfyException as e:
raise HomeAssistantError(
translation_key="publish_failed_exception",
translation_domain=DOMAIN,
) from e
90 changes: 90 additions & 0 deletions homeassistant/components/ntfy/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
rules:
# Bronze
action-setup:
status: exempt
comment: only entity actions
appropriate-polling:
status: exempt
comment: the integration does not poll
brands: done
common-modules:
status: exempt
comment: the integration currently implements only one platform and has no coordinator
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: the integration does not subscribe to events
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure:
status: done
comment: tested by publishing a success message to the topic
test-before-setup:
status: exempt
comment: testing would require to trigger a notification
unique-config-entry: done

# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: the integration has no options
docs-installation-parameters: done
entity-unavailable:
status: exempt
comment: the integration only implements a stateless notify entity.
integration-owner: done
log-when-unavailable:
status: exempt
comment: the integration only integrates state-less entities
parallel-updates: done
reauthentication-flow:
status: exempt
comment: the integration currently does not implement authenticated requests
test-coverage: done

# Gold
devices: todo
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class:
status: exempt
comment: no suitable device class for the notify entity
entity-disabled-by-default:
status: exempt
comment: only one entity
entity-translations:
status: exempt
comment: the notify entity uses the topic as name, no translation required
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: the integration has no repeairs
stale-devices:
status: exempt
comment: only one device per entry, is deleted with the entry.

# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo
35 changes: 35 additions & 0 deletions homeassistant/components/ntfy/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"config": {
"step": {
"user": {
"description": "Setup a topic for publishing notifications",
"data": {
"url": "Service URL",
"topic": "Topic"
},

"data_description": {
"url": "Address of the ntfy service. Modify this if you want to use a different server",
"topic": "Enter the name of the topic you want to publish to. Topics may not be password-protected, so choose a name that's not easy to guess. If left empty, a random topic name will be generated."
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"forbidden_topic": "Protected topics are not supported",
"unknown": "[%key:common::config_flow::error::unknown%]",
"invalid_url": "Invalid service URL"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"exceptions": {
"publish_failed_request_error": {
"message": "Failed to publish notification: {error_msg}"
},
"publish_failed_exception": {
"message": "Failed to publish notification due to a connection error"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@
"nobo_hub",
"nordpool",
"notion",
"ntfy",
"nuheat",
"nuki",
"nut",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -4276,6 +4276,12 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
"ntfy": {
"name": "ntfy",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push"
},
"nuheat": {
"name": "NuHeat",
"integration_type": "hub",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ aionanoleaf==0.2.1
# homeassistant.components.notion
aionotion==2024.03.0

# homeassistant.components.ntfy
aiontfy==0.2.1

# homeassistant.components.nut
aionut==4.3.3

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ aionanoleaf==0.2.1
# homeassistant.components.notion
aionotion==2024.03.0

# homeassistant.components.ntfy
aiontfy==0.2.1

# homeassistant.components.nut
aionut==4.3.3

Expand Down
1 change: 1 addition & 0 deletions tests/components/ntfy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for ntfy integration."""
Loading

0 comments on commit 9757da4

Please sign in to comment.