Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Apprise notification integration #26868

Merged
merged 19 commits into from Oct 14, 2019
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -26,6 +26,7 @@ homeassistant/components/ambient_station/* @bachya
homeassistant/components/androidtv/* @JeffLIrion
homeassistant/components/apache_kafka/* @bachya
homeassistant/components/api/* @home-assistant/core
homeassistant/components/apprise/* @caronc
homeassistant/components/aprs/* @PhilRW
homeassistant/components/arcam_fmj/* @elupus
homeassistant/components/arduino/* @fabaff
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/apprise/__init__.py
@@ -0,0 +1 @@
"""The apprise component."""
12 changes: 12 additions & 0 deletions homeassistant/components/apprise/manifest.json
@@ -0,0 +1,12 @@
{
"domain": "apprise",
"name": "Apprise",
"documentation": "https://www.home-assistant.io/components/apprise",
"requirements": [
"apprise==0.8.1"
],
"dependencies": [],
"codeowners": [
"@caronc"
]
}
66 changes: 66 additions & 0 deletions homeassistant/components/apprise/notify.py
@@ -0,0 +1,66 @@
"""Apprise platform for notify component."""
import logging

import voluptuous as vol

import apprise

import homeassistant.helpers.config_validation as cv

from homeassistant.components.notify import (
ATTR_TITLE,
ATTR_TITLE_DEFAULT,
PLATFORM_SCHEMA,
BaseNotificationService,
)

_LOGGER = logging.getLogger(__name__)

CONF_FILE = "config"
CONF_URL = "url"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_URL): vol.All(cv.ensure_list, [str]),
vol.Optional(CONF_FILE): cv.string,
}
)


def get_service(hass, config, discovery_info=None):
"""Get the Apprise notification service."""

# Create our object
a_obj = apprise.Apprise()

if config.get(CONF_FILE):
# Sourced from a Configuration File
a_config = apprise.AppriseConfig()
if not a_config.add(config[CONF_FILE]):
_LOGGER.error("Invalid Apprise config url provided")
return None

if not a_obj.add(a_config):
_LOGGER.error("Invalid Apprise config url provided")
return None

if config.get(CONF_URL):
# Ordered list of URLs
if not a_obj.add(config[CONF_URL]):
_LOGGER.error("Invalid Apprise URL(s) supplied")
return None

return AppriseNotificationService(a_obj)


class AppriseNotificationService(BaseNotificationService):
"""Implement the notification service for Apprise."""

def __init__(self, a_obj):
"""Initialize the service."""
self.apprise = a_obj

def send_message(self, message="", **kwargs):
"""Send a message to a specified target."""
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
self.apprise.notify(body=message, title=title)
3 changes: 3 additions & 0 deletions requirements_all.txt
Expand Up @@ -217,6 +217,9 @@ apcaccess==0.0.13
# homeassistant.components.apns
apns2==0.3.0

# homeassistant.components.apprise
apprise==0.8.1

# homeassistant.components.aprs
aprslib==0.6.46

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Expand Up @@ -103,6 +103,9 @@ androidtv==0.0.30
# homeassistant.components.apns
apns2==0.3.0

# homeassistant.components.apprise
apprise==0.8.1

# homeassistant.components.aprs
aprslib==0.6.46

Expand Down
1 change: 1 addition & 0 deletions tests/components/apprise/__init__.py
@@ -0,0 +1 @@
"""Tests for the apprise component."""
110 changes: 110 additions & 0 deletions tests/components/apprise/test_notify.py
@@ -0,0 +1,110 @@
"""The tests for the apprise notification platform."""
from unittest.mock import patch
from unittest.mock import MagicMock

from homeassistant.setup import async_setup_component

BASE_COMPONENT = "notify"


async def test_apprise_config_load_fail01(hass):
"""Test apprise configuration failures 1."""

config = {
BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"}
}

with patch("apprise.AppriseConfig.add", return_value=False):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()

MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
# Test that our service failed to load
assert hass.services.has_service(BASE_COMPONENT, "test") is False
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved


async def test_apprise_config_load_fail02(hass):
"""Test apprise configuration failures 2."""

config = {
BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"}
}

with patch("apprise.Apprise.add", return_value=False):
with patch("apprise.AppriseConfig.add", return_value=True):
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()

MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
# Test that our service failed to load
assert hass.services.has_service(BASE_COMPONENT, "test") is False
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved


async def test_apprise_config_load_okay(hass, tmp_path):
"""Test apprise configuration failures."""

# Test cases where our URL is invalid
d = tmp_path / "apprise-config"
d.mkdir()
f = d / "apprise"
f.write_text("mailto://user:pass@example.com/")

config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}}

assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()

# Valid configuration was loaded; our service is good
assert hass.services.has_service(BASE_COMPONENT, "test") is True
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved


async def test_apprise_url_load_fail(hass):
"""Test apprise url failure."""

config = {
BASE_COMPONENT: {
"name": "test",
"platform": "apprise",
"url": "mailto://user:pass@example.com",
}
}
with patch("apprise.Apprise.add", return_value=False):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()

# Test that our service failed to load
assert hass.services.has_service(BASE_COMPONENT, "test") is False
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved


async def test_apprise_notification(hass):
"""Test apprise notification."""

config = {
BASE_COMPONENT: {
"name": "test",
"platform": "apprise",
"url": "mailto://user:pass@example.com",
}
}

# Our Message
data = {"title": "Test Title", "message": "Test Message"}

with patch("apprise.Apprise") as mock_apprise:
obj = MagicMock()
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
obj.add.return_value = True
obj.notify.return_value = True
mock_apprise.return_value = obj
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()

# Test the existance of our service
assert hass.services.has_service(BASE_COMPONENT, "test") is True
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved

# Test the call to our underlining notify() call
await hass.services.async_call(BASE_COMPONENT, "test", data)
await hass.async_block_till_done()
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved

# Validate calls were made under the hood correctly
obj.add.assert_called_once_with([config[BASE_COMPONENT]["url"]])
obj.notify.assert_called_once_with(
**{"body": data["message"], "title": data["title"]}
)