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:
Paulus Schoutsen 2016-08-14 01:10:07 -07:00 committed by GitHub
parent c6f67a5203
commit dc68f61261
16 changed files with 381 additions and 45 deletions

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 474366c536ec3e471da12d5f15b07b79fe9b07e2
Subproject commit af4af1e9332afef90d25d61589840d239baf7ded

View file

@ -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"
}
]
}

View file

@ -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)})))});

View file

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

View 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')

View file

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

View file

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

View 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