Html5 push notifications notify platform (#2807)
* Initial work to add Chrome Push Notification support * Remove push.js from home-assistant since it is now in Polymer * Chrome->HTML5, general cleanup/fixes * Make html5 generic, move manifest.json into frontend so that we can dynamically add the gcm_sender_id * Pylint, flake8, pydocstyle frontend init * HTML5 push fixes * Update polymer * Remove crypto req * Add notify default platform. * Fix HTML5 push * Registration fixes * Linting fix * pep257 fix * Add tests * pep257 fix * Update frontend
This commit is contained in:
parent
c6f67a5203
commit
dc68f61261
16 changed files with 381 additions and 45 deletions
|
@ -14,6 +14,19 @@ URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
|||
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||
PANELS = {}
|
||||
MANIFEST_JSON = {
|
||||
"background_color": "#FFFFFF",
|
||||
"description": "Open-source home automation platform running on Python 3.",
|
||||
"dir": "ltr",
|
||||
"display": "standalone",
|
||||
"icons": [],
|
||||
"lang": "en-US",
|
||||
"name": "Home Assistant",
|
||||
"orientation": "any",
|
||||
"short_name": "Assistant",
|
||||
"start_url": "/",
|
||||
"theme_color": "#03A9F4"
|
||||
}
|
||||
|
||||
# To keep track we don't register a component twice (gives a warning)
|
||||
_REGISTERED_COMPONENTS = set()
|
||||
|
@ -94,9 +107,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
|||
PANELS[url_path] = data
|
||||
|
||||
|
||||
def add_manifest_json_key(key, val):
|
||||
"""Add a keyval to the manifest.json."""
|
||||
MANIFEST_JSON[key] = val
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup serving the frontend."""
|
||||
hass.wsgi.register_view(BootstrapView)
|
||||
hass.wsgi.register_view(ManifestJSONView)
|
||||
|
||||
if hass.wsgi.development:
|
||||
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||
|
@ -126,6 +145,13 @@ def setup(hass, config):
|
|||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
|
||||
|
||||
for size in (192, 384, 512, 1024):
|
||||
MANIFEST_JSON['icons'].append({
|
||||
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
|
||||
"sizes": "{}x{}".format(size, size),
|
||||
"type": "image/png"
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -199,3 +225,17 @@ class IndexView(HomeAssistantView):
|
|||
panel_url=panel_url, panels=PANELS)
|
||||
|
||||
return self.Response(resp, mimetype='text/html')
|
||||
|
||||
|
||||
class ManifestJSONView(HomeAssistantView):
|
||||
"""View to return a manifest.json."""
|
||||
|
||||
requires_auth = False
|
||||
url = "/manifest.json"
|
||||
name = "manifestjson"
|
||||
|
||||
def get(self, request):
|
||||
"""Return the manifest.json."""
|
||||
import json
|
||||
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
|
||||
return self.Response(msg, mimetype="application/manifest+json")
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<title>Home Assistant</title>
|
||||
|
||||
<link rel='manifest' href='/static/manifest.json'>
|
||||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "457d5acd123e7dc38947c07984b3a5e8",
|
||||
"frontend.html": "829ee7cb591b8a63d7f22948a7aeb07a",
|
||||
"core.js": "b4ee3a700ef5549a36b436611e27d3a9",
|
||||
"frontend.html": "411fcc6c69b3cab0740ac3db4b9947c8",
|
||||
"mdi.html": "b399b5d3798f5b68b0a4fbaae3432d48",
|
||||
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
|
||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 474366c536ec3e471da12d5f15b07b79fe9b07e2
|
||||
Subproject commit af4af1e9332afef90d25d61589840d239baf7ded
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"name": "Home Assistant",
|
||||
"short_name": "Assistant",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#03A9F4",
|
||||
"background_color": "#FFFFFF",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/favicon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/favicon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/favicon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/favicon-1024x1024.png",
|
||||
"sizes": "1024x1024",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1 +1 @@
|
|||
"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}var precacheConfig=[["/","a463cb982f337e09c3ed47c41b2d9dda"],["/frontend/panels/dev-event-3cc881ae8026c0fba5aa67d334a3ab2b.html","e22ed0d2d10777c87eb9620d81f525b4"],["/frontend/panels/dev-info-34e2df1af32e60fffcafe7e008a92169.html","7e939dc762dc0c0ec769db4ea76a4b09"],["/frontend/panels/dev-service-bb5c587ada694e0fd42ceaaedd6fe6aa.html","782c4860c5e8ab274231ba9dfd528f29"],["/frontend/panels/dev-state-4608326978256644c42b13940c028e0a.html","26758b741ac1b7c8e9cfcb24762d8774"],["/frontend/panels/dev-template-0a099d4589636ed3038a3e9f020468a7.html","99114026cf9193263c74cc25f9f6a469"],["/frontend/panels/map-af7d04aff7dd5479c5a0016bc8d4dd7d.html","6031df1b4d23d5b321208449b2d293f8"],["/static/core-457d5acd123e7dc38947c07984b3a5e8.js","69e2a5b421d7ed7a7e70390cd9ced80e"],["/static/frontend-829ee7cb591b8a63d7f22948a7aeb07a.html","2afa980f1c1fdf9e596580112ac8e51a"],["/static/mdi-b399b5d3798f5b68b0a4fbaae3432d48.html","819d479ae2b690589687469045b22c26"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},createCacheKey=function(e,t,a,n){var c=new URL(e);return n&&c.toString().match(n)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(a)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),a.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],a=e[1],n=new URL(t,self.location),c=createCacheKey(n,hashParamName,a,!1);return[n.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(a){if(!t.has(a))return e.add(new Request(a,{credentials:"same-origin"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(a){return Promise.all(a.map(function(a){if(!t.has(a.url))return e.delete(a)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,a=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(a);var n="index.html";!t&&n&&(a=addDirectoryIndex(a,n),t=urlsToCacheKeys.has(a));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js)).)*$"],e.request.url)&&(a=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(a)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(a))}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}});
|
||||
"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}var precacheConfig=[["/","9a5d0507bd1f13e3eca6b35abd4cbebb"],["/frontend/panels/dev-event-3cc881ae8026c0fba5aa67d334a3ab2b.html","e22ed0d2d10777c87eb9620d81f525b4"],["/frontend/panels/dev-info-34e2df1af32e60fffcafe7e008a92169.html","7e939dc762dc0c0ec769db4ea76a4b09"],["/frontend/panels/dev-service-bb5c587ada694e0fd42ceaaedd6fe6aa.html","782c4860c5e8ab274231ba9dfd528f29"],["/frontend/panels/dev-state-4608326978256644c42b13940c028e0a.html","26758b741ac1b7c8e9cfcb24762d8774"],["/frontend/panels/dev-template-0a099d4589636ed3038a3e9f020468a7.html","99114026cf9193263c74cc25f9f6a469"],["/frontend/panels/map-af7d04aff7dd5479c5a0016bc8d4dd7d.html","6031df1b4d23d5b321208449b2d293f8"],["/static/core-b4ee3a700ef5549a36b436611e27d3a9.js","e2fb4f1dc0d1e8192a327b51768b3802"],["/static/frontend-411fcc6c69b3cab0740ac3db4b9947c8.html","5967b9cdaeb14753552c2461805eb397"],["/static/mdi-b399b5d3798f5b68b0a4fbaae3432d48.html","819d479ae2b690589687469045b22c26"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.toString().match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n))return e.add(new Request(n,{credentials:"same-origin"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);var a="index.html";!t&&a&&(n=addDirectoryIndex(n,a),t=urlsToCacheKeys.has(n));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n))}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t)))}),self.addEventListener("notificationclick",function(e){var t;e.notification.data&&e.notification.data.url&&(e.notification.close(),t=e.notification.data.url,t&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var n,a;for(n=0;n<e.length;n++)if(a=e[n],a.url===t&&"focus"in a)return a.focus();if(clients.openWindow)return clients.openWindow(t)})))});
|
Binary file not shown.
|
@ -13,9 +13,9 @@ import voluptuous as vol
|
|||
import homeassistant.bootstrap as bootstrap
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers import config_per_platform, template
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = "notify"
|
||||
|
||||
|
@ -34,6 +34,11 @@ ATTR_DATA = 'data'
|
|||
|
||||
SERVICE_NOTIFY = "notify"
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
NOTIFY_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_MESSAGE): cv.template,
|
||||
vol.Optional(ATTR_TITLE, default=ATTR_TITLE_DEFAULT): cv.string,
|
||||
|
@ -95,8 +100,8 @@ def setup(hass, config):
|
|||
data=data)
|
||||
|
||||
service_call_handler = partial(notify_message, notify_service)
|
||||
service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY)
|
||||
hass.services.register(DOMAIN, service_notify, service_call_handler,
|
||||
service_name = slugify(p_config.get(CONF_NAME) or SERVICE_NOTIFY)
|
||||
hass.services.register(DOMAIN, service_name, service_call_handler,
|
||||
descriptions.get(SERVICE_NOTIFY),
|
||||
schema=NOTIFY_SERVICE_SCHEMA)
|
||||
success = True
|
||||
|
|
173
homeassistant/components/notify/html5.py
Normal file
173
homeassistant/components/notify/html5.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
"""
|
||||
HTML5 Push Messaging notification service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.html5/
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.const import (
|
||||
HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR)
|
||||
from homeassistant.util import ensure_unique_string
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, ATTR_DATA, BaseNotificationService,
|
||||
PLATFORM_SCHEMA)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.frontend import add_manifest_json_key
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/web-push-libs/pywebpush/archive/'
|
||||
'e743dc92558fc62178d255c0018920d74fa778ed.zip#'
|
||||
'pywebpush==0.5.0']
|
||||
|
||||
DEPENDENCIES = ["frontend"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REGISTRATIONS_FILE = "html5_push_registrations.conf"
|
||||
|
||||
ATTR_GCM_SENDER_ID = 'gcm_sender_id'
|
||||
ATTR_GCM_API_KEY = 'gcm_api_key'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(ATTR_GCM_SENDER_ID): cv.string,
|
||||
vol.Optional(ATTR_GCM_API_KEY): cv.string,
|
||||
})
|
||||
|
||||
ATTR_SUBSCRIPTION = 'subscription'
|
||||
ATTR_BROWSER = 'browser'
|
||||
|
||||
REGISTER_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_SUBSCRIPTION): cv.match_all,
|
||||
vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox'])
|
||||
})
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the HTML5 push notification service."""
|
||||
json_path = hass.config.path(REGISTRATIONS_FILE)
|
||||
|
||||
registrations = _load_config(json_path)
|
||||
|
||||
if registrations is None:
|
||||
return None
|
||||
|
||||
hass.wsgi.register_view(
|
||||
HTML5PushRegistrationView(hass, registrations, json_path))
|
||||
|
||||
gcm_api_key = config.get('gcm_api_key')
|
||||
gcm_sender_id = config.get('gcm_sender_id')
|
||||
|
||||
if gcm_sender_id is not None:
|
||||
add_manifest_json_key('gcm_sender_id', config.get('gcm_sender_id'))
|
||||
|
||||
return HTML5NotificationService(gcm_api_key, registrations)
|
||||
|
||||
|
||||
def _load_config(filename):
|
||||
"""Load configuration."""
|
||||
if not os.path.isfile(filename):
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(filename, "r") as fdesc:
|
||||
inp = fdesc.read()
|
||||
|
||||
# In case empty file
|
||||
if not inp:
|
||||
return {}
|
||||
|
||||
return json.loads(inp)
|
||||
except (IOError, ValueError) as error:
|
||||
_LOGGER.error("Reading config file %s failed: %s", filename, error)
|
||||
return None
|
||||
|
||||
|
||||
def _save_config(filename, config):
|
||||
"""Save configuration."""
|
||||
try:
|
||||
with open(filename, "w") as fdesc:
|
||||
fdesc.write(json.dumps(config, indent=4, sort_keys=True))
|
||||
except (IOError, TypeError) as error:
|
||||
_LOGGER.error("Saving config file failed: %s", error)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class HTML5PushRegistrationView(HomeAssistantView):
|
||||
"""Accepts push registrations from a browser."""
|
||||
|
||||
url = "/api/notify.html5"
|
||||
name = "api:notify.html5"
|
||||
|
||||
def __init__(self, hass, registrations, json_path):
|
||||
"""Init HTML5PushRegistrationView."""
|
||||
super().__init__(hass)
|
||||
self.registrations = registrations
|
||||
self.json_path = json_path
|
||||
|
||||
def post(self, request):
|
||||
"""Accept the POST request for push registrations from a browser."""
|
||||
try:
|
||||
data = REGISTER_SCHEMA(request.json)
|
||||
except vol.Invalid as ex:
|
||||
return self.json_message(humanize_error(request.json, ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
name = ensure_unique_string('unnamed device',
|
||||
self.registrations.keys())
|
||||
|
||||
self.registrations[name] = data
|
||||
|
||||
if not _save_config(self.json_path, self.registrations):
|
||||
return self.json_message('Error saving registration.',
|
||||
HTTP_INTERNAL_SERVER_ERROR)
|
||||
|
||||
return self.json_message("Push notification subscriber registered.")
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class HTML5NotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for HTML5."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, gcm_key, registrations):
|
||||
"""Initialize the service."""
|
||||
self._gcm_key = gcm_key
|
||||
self.registrations = registrations
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
from pywebpush import WebPusher
|
||||
|
||||
payload = {
|
||||
'title': message,
|
||||
'icon': '/static/icons/favicon-192x192.png',
|
||||
}
|
||||
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
|
||||
if data:
|
||||
payload.update(data)
|
||||
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
if not targets:
|
||||
targets = self.registrations.keys()
|
||||
elif not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
info = self.registrations.get(target)
|
||||
if info is None:
|
||||
_LOGGER.error("%s is not a valid HTML5 push notification"
|
||||
" target!", target)
|
||||
continue
|
||||
|
||||
WebPusher(info[ATTR_SUBSCRIPTION]).send(
|
||||
json.dumps(payload), gcm_key=self._gcm_key, ttl='0')
|
|
@ -181,6 +181,9 @@ https://github.com/theolind/pymysensors/archive/cc5d0b325e13c2b623fa934f69eea7cd
|
|||
# homeassistant.components.alarm_control_panel.simplisafe
|
||||
https://github.com/w1ll1am23/simplisafe-python/archive/586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#simplisafe-python==0.0.1
|
||||
|
||||
# homeassistant.components.notify.html5
|
||||
https://github.com/web-push-libs/pywebpush/archive/e743dc92558fc62178d255c0018920d74fa778ed.zip#pywebpush==0.5.0
|
||||
|
||||
# homeassistant.components.media_player.lg_netcast
|
||||
https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ npm run frontend_prod
|
|||
|
||||
cp bower_components/webcomponentsjs/webcomponents-lite.min.js ..
|
||||
cp -r build/* ..
|
||||
node script/sw-precache.js
|
||||
BUILD_DEV=0 node script/gen-service-worker.js
|
||||
cp build/service_worker.js ..
|
||||
|
||||
cd ..
|
||||
|
|
145
tests/components/notify/test_html5.py
Normal file
145
tests/components/notify/test_html5.py
Normal file
|
@ -0,0 +1,145 @@
|
|||
"""Test HTML5 notify platform."""
|
||||
import json
|
||||
import tempfile
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from werkzeug.test import EnvironBuilder
|
||||
|
||||
from homeassistant.components.http import request_class
|
||||
from homeassistant.components.notify import html5
|
||||
|
||||
|
||||
class TestHtml5Notify(object):
|
||||
"""Tests for HTML5 notify platform."""
|
||||
|
||||
def test_get_service_with_no_json(self):
|
||||
"""Test empty json file."""
|
||||
hass = MagicMock()
|
||||
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
hass.config.path.return_value = fp.name
|
||||
service = html5.get_service(hass, {})
|
||||
|
||||
assert service is not None
|
||||
|
||||
def test_get_service_with_bad_json(self):
|
||||
"""Test ."""
|
||||
hass = MagicMock()
|
||||
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
fp.write('I am not JSON'.encode('utf-8'))
|
||||
fp.flush()
|
||||
hass.config.path.return_value = fp.name
|
||||
service = html5.get_service(hass, {})
|
||||
|
||||
assert service is None
|
||||
|
||||
@patch('pywebpush.WebPusher')
|
||||
def test_sending_message(self, mock_wp):
|
||||
"""Test sending message."""
|
||||
hass = MagicMock()
|
||||
|
||||
data = {
|
||||
'device': {
|
||||
'browser': 'chrome',
|
||||
'subscription': 'hello world',
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
fp.write(json.dumps(data).encode('utf-8'))
|
||||
fp.flush()
|
||||
hass.config.path.return_value = fp.name
|
||||
service = html5.get_service(hass, {'gcm_sender_id': '100'})
|
||||
|
||||
assert service is not None
|
||||
|
||||
service.send_message('Hello', target=['device', 'non_existing'],
|
||||
data={'icon': 'beer.png'})
|
||||
|
||||
assert len(mock_wp.mock_calls) == 2
|
||||
|
||||
# WebPusher constructor
|
||||
assert mock_wp.mock_calls[0][1][0] == 'hello world'
|
||||
|
||||
# Call to send
|
||||
payload = json.loads(mock_wp.mock_calls[1][1][0])
|
||||
|
||||
assert payload['title'] == 'Hello'
|
||||
assert payload['icon'] == 'beer.png'
|
||||
|
||||
def test_registering_new_device_view(self):
|
||||
"""Test that the HTML view works."""
|
||||
hass = MagicMock()
|
||||
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
hass.config.path.return_value = fp.name
|
||||
fp.close()
|
||||
service = html5.get_service(hass, {})
|
||||
|
||||
assert service is not None
|
||||
|
||||
# assert hass.called
|
||||
assert len(hass.mock_calls) == 2
|
||||
|
||||
view = hass.mock_calls[1][1][0]
|
||||
assert view.json_path == fp.name
|
||||
assert view.registrations == {}
|
||||
|
||||
builder = EnvironBuilder(method='POST', data=json.dumps({
|
||||
'browser': 'chrome',
|
||||
'subscription': 'sub info',
|
||||
}))
|
||||
Request = request_class()
|
||||
resp = view.post(Request(builder.get_environ()))
|
||||
|
||||
expected = {
|
||||
'unnamed device': {
|
||||
'browser': 'chrome',
|
||||
'subscription': 'sub info',
|
||||
},
|
||||
}
|
||||
|
||||
assert resp.status_code == 200, resp.response
|
||||
assert view.registrations == expected
|
||||
with open(fp.name) as fpp:
|
||||
assert json.load(fpp) == expected
|
||||
|
||||
def test_registering_new_device_validation(self):
|
||||
"""Test various errors when registering a new device."""
|
||||
hass = MagicMock()
|
||||
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
hass.config.path.return_value = fp.name
|
||||
service = html5.get_service(hass, {})
|
||||
|
||||
assert service is not None
|
||||
|
||||
# assert hass.called
|
||||
assert len(hass.mock_calls) == 2
|
||||
|
||||
view = hass.mock_calls[1][1][0]
|
||||
|
||||
Request = request_class()
|
||||
|
||||
builder = EnvironBuilder(method='POST', data=json.dumps({
|
||||
'browser': 'invalid browser',
|
||||
'subscription': 'sub info',
|
||||
}))
|
||||
resp = view.post(Request(builder.get_environ()))
|
||||
assert resp.status_code == 400, resp.response
|
||||
|
||||
builder = EnvironBuilder(method='POST', data=json.dumps({
|
||||
'browser': 'chrome',
|
||||
}))
|
||||
resp = view.post(Request(builder.get_environ()))
|
||||
assert resp.status_code == 400, resp.response
|
||||
|
||||
builder = EnvironBuilder(method='POST', data=json.dumps({
|
||||
'browser': 'chrome',
|
||||
'subscription': 'sub info',
|
||||
}))
|
||||
with patch('homeassistant.components.notify.html5._save_config',
|
||||
return_value=False):
|
||||
resp = view.post(Request(builder.get_environ()))
|
||||
assert resp.status_code == 500, resp.response
|
Loading…
Add table
Reference in a new issue