Add Apprise notification integration (#26868)

* Added apprise notification component

* flake-8 fixes; black formatting + import merged to 1 line

* pylint issues resolved

* added github name to manifest.json

* import moved to top as per code review request

* manifest formatting to avoid failing ci

* .coveragerc updated to include apprise

* removed block for written tests

* more test coverage

* formatting as per code review

* tests converted to async style as per code review

* increased coverage

* bumped version of apprise to 0.8.1

* test that mocked entries are called

* added tests for hass.service loading

* support tags for those who identify the TARGET option

* renamed variable as per code review

* 'assert not' used instead of 'is False'

* added period (in case linter isn't happy)
This commit is contained in:
Chris Caron
2019-10-14 18:53:59 -04:00
committed by Martin Hjelmare
parent 3231e22ddf
commit 3cb844f22c
8 changed files with 242 additions and 0 deletions

View File

@ -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

View File

@ -0,0 +1 @@
"""The apprise component."""

View File

@ -0,0 +1,12 @@
{
"domain": "apprise",
"name": "Apprise",
"documentation": "https://www.home-assistant.io/components/apprise",
"requirements": [
"apprise==0.8.1"
],
"dependencies": [],
"codeowners": [
"@caronc"
]
}

View File

@ -0,0 +1,73 @@
"""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_TARGET,
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.
If no target/tags are specified, then services are notified as is
However, if any tags are specified, then they will be applied
to the notification causing filtering (if set up that way).
"""
targets = kwargs.get(ATTR_TARGET)
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
self.apprise.notify(body=message, title=title, tag=targets)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
"""Tests for the apprise component."""

View File

@ -0,0 +1,148 @@
"""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):
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()
# Test that our service failed to load
assert not hass.services.has_service(BASE_COMPONENT, "test")
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()
# Test that our service failed to load
assert not hass.services.has_service(BASE_COMPONENT, "test")
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")
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):
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()
# Test that our service failed to load
assert not hass.services.has_service(BASE_COMPONENT, "test")
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()
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")
# Test the call to our underlining notify() call
await hass.services.async_call(BASE_COMPONENT, "test", data)
await hass.async_block_till_done()
# 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"], "tag": None}
)
async def test_apprise_notification_with_target(hass, tmp_path):
"""Test apprise notification with a target."""
# Test cases where our URL is invalid
d = tmp_path / "apprise-config"
d.mkdir()
f = d / "apprise"
# Write 2 config entries each assigned to different tags
f.write_text("devops=mailto://user:pass@example.com/\r\n")
f.write_text("system,alert=syslog://\r\n")
config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}}
# Our Message, only notify the services tagged with "devops"
data = {"title": "Test Title", "message": "Test Message", "target": ["devops"]}
with patch("apprise.Apprise") as mock_apprise:
apprise_obj = MagicMock()
apprise_obj.add.return_value = True
apprise_obj.notify.return_value = True
mock_apprise.return_value = apprise_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")
# Test the call to our underlining notify() call
await hass.services.async_call(BASE_COMPONENT, "test", data)
await hass.async_block_till_done()
# Validate calls were made under the hood correctly
apprise_obj.notify.assert_called_once_with(
**{"body": data["message"], "title": data["title"], "tag": data["target"]}
)