Add notify.html5_dismiss service (#19912)

* Add notify.html5_dismiss service

* fix test

* add can_dismiss

* fix service data payload

* fix hasattr -> getattr

* fixes

* move dismiss service to html5

* fix services.yaml

* fix line to long
This commit is contained in:
Tommy Jonsson 2019-01-16 00:31:57 +01:00 committed by Paulus Schoutsen
parent 0ec1401be7
commit 1b79872dd6
3 changed files with 105 additions and 8 deletions

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/notify.html5/
import datetime
import json
import logging
from functools import partial
import time
import uuid
@ -20,7 +21,7 @@ from homeassistant.components.frontend import add_manifest_json_key
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.notify import (
ATTR_DATA, ATTR_TITLE, ATTR_TARGET, PLATFORM_SCHEMA, ATTR_TITLE_DEFAULT,
BaseNotificationService)
BaseNotificationService, DOMAIN)
from homeassistant.const import (
URL_ROOT, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_INTERNAL_SERVER_ERROR)
from homeassistant.helpers import config_validation as cv
@ -34,6 +35,8 @@ _LOGGER = logging.getLogger(__name__)
REGISTRATIONS_FILE = 'html5_push_registrations.conf'
SERVICE_DISMISS = 'html5_dismiss'
ATTR_GCM_SENDER_ID = 'gcm_sender_id'
ATTR_GCM_API_KEY = 'gcm_api_key'
@ -57,6 +60,7 @@ ATTR_ACTION = 'action'
ATTR_ACTIONS = 'actions'
ATTR_TYPE = 'type'
ATTR_URL = 'url'
ATTR_DISMISS = 'dismiss'
ATTR_JWT = 'jwt'
@ -80,6 +84,11 @@ SUBSCRIPTION_SCHEMA = vol.All(
})
)
DISMISS_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_DATA): dict,
})
REGISTER_SCHEMA = vol.Schema({
vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA,
vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox']),
@ -122,7 +131,8 @@ def get_service(hass, config, discovery_info=None):
add_manifest_json_key(
ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID))
return HTML5NotificationService(gcm_api_key, registrations, json_path)
return HTML5NotificationService(
hass, gcm_api_key, registrations, json_path)
def _load_config(filename):
@ -326,12 +336,29 @@ class HTML5PushCallbackView(HomeAssistantView):
class HTML5NotificationService(BaseNotificationService):
"""Implement the notification service for HTML5."""
def __init__(self, gcm_key, registrations, json_path):
def __init__(self, hass, gcm_key, registrations, json_path):
"""Initialize the service."""
self._gcm_key = gcm_key
self.registrations = registrations
self.registrations_json_path = json_path
async def async_dismiss_message(service):
"""Handle dismissing notification message service calls."""
kwargs = {}
if self.targets is not None:
kwargs[ATTR_TARGET] = self.targets
elif service.data.get(ATTR_TARGET) is not None:
kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET)
kwargs[ATTR_DATA] = service.data.get(ATTR_DATA)
await self.async_dismiss(**kwargs)
hass.services.async_register(
DOMAIN, SERVICE_DISMISS, async_dismiss_message,
schema=DISMISS_SERVICE_SCHEMA)
@property
def targets(self):
"""Return a dictionary of registered targets."""
@ -340,12 +367,28 @@ class HTML5NotificationService(BaseNotificationService):
targets[registration] = registration
return targets
def dismiss(self, **kwargs):
"""Dismisses a notification."""
data = kwargs.get(ATTR_DATA)
tag = data.get(ATTR_TAG) if data else ""
payload = {
ATTR_TAG: tag,
ATTR_DISMISS: True,
ATTR_DATA: {}
}
self._push_message(payload, **kwargs)
async def async_dismiss(self, **kwargs):
"""Dismisses a notification.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(
partial(self.dismiss, **kwargs))
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
import jwt
from pywebpush import WebPusher
timestamp = int(time.time())
tag = str(uuid.uuid4())
payload = {
@ -354,7 +397,6 @@ class HTML5NotificationService(BaseNotificationService):
ATTR_DATA: {},
'icon': '/static/icons/favicon-192x192.png',
ATTR_TAG: tag,
'timestamp': (timestamp*1000), # Javascript ms since epoch
ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
}
@ -378,6 +420,17 @@ class HTML5NotificationService(BaseNotificationService):
payload.get(ATTR_ACTIONS) is None):
payload[ATTR_DATA][ATTR_URL] = URL_ROOT
self._push_message(payload, **kwargs)
def _push_message(self, payload, **kwargs):
"""Send the message."""
import jwt
from pywebpush import WebPusher
timestamp = int(time.time())
payload['timestamp'] = (timestamp*1000) # Javascript ms since epoch
targets = kwargs.get(ATTR_TARGET)
if not targets:

View File

@ -16,6 +16,16 @@ notify:
description: Extended information for notification. Optional depending on the platform.
example: platform specific
html5_dismiss:
description: Dismiss a html5 notification.
fields:
target:
description: An array of targets. Optional.
example: ['my_phone', 'my_tablet']
data:
description: Extended information of notification. Supports tag. Optional.
example: '{ "tag": "tagname" }'
apns_register:
description: Registers a device to receive push notifications.
fields:

View File

@ -81,6 +81,40 @@ class TestHtml5Notify:
assert service is not None
@patch('pywebpush.WebPusher')
def test_dismissing_message(self, mock_wp):
"""Test dismissing message."""
hass = MagicMock()
data = {
'device': SUBSCRIPTION_1
}
m = mock_open(read_data=json.dumps(data))
with patch(
'homeassistant.util.json.open',
m, create=True
):
service = html5.get_service(hass, {'gcm_sender_id': '100'})
assert service is not None
service.dismiss(target=['device', 'non_existing'],
data={'tag': 'test'})
assert len(mock_wp.mock_calls) == 3
# WebPusher constructor
assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_1['subscription']
# Third mock_call checks the status_code of the response.
assert mock_wp.mock_calls[2][0] == '().send().status_code.__eq__'
# Call to send
payload = json.loads(mock_wp.mock_calls[1][1][0])
assert payload['dismiss'] is True
assert payload['tag'] == 'test'
@patch('pywebpush.WebPusher')
def test_sending_message(self, mock_wp):
"""Test sending message."""