From 1b79872dd62486dc16c2c9aa61eee9ab1c4ccfdf Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Wed, 16 Jan 2019 00:31:57 +0100 Subject: [PATCH] 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 --- homeassistant/components/notify/html5.py | 69 ++++++++++++++++--- homeassistant/components/notify/services.yaml | 10 +++ tests/components/notify/test_html5.py | 34 +++++++++ 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index f70a9cb73c1..6a486bb6362 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -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: diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 23b1c968c4a..1b7944cc7da 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -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: diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 6aeba650a8c..e33c297b166 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -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."""