From d9e9520d6542073079f437ff62eeebbba9298153 Mon Sep 17 00:00:00 2001 From: samowens Date: Wed, 24 Aug 2016 23:26:31 +0100 Subject: [PATCH 01/33] added push notification implementation --- homeassistant/components/notify/apns.py | 206 ++++++++++++++++++ tests/components/notify/test_apns.py | 266 ++++++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 homeassistant/components/notify/apns.py create mode 100644 tests/components/notify/test_apns.py diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py new file mode 100644 index 00000000000000..9da4c05a5ae74d --- /dev/null +++ b/homeassistant/components/notify/apns.py @@ -0,0 +1,206 @@ +"""Support for Apple push notification service.""" +import logging +import os + +from homeassistant.helpers.event import track_state_change +from homeassistant.config import load_yaml_config_file +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_DATA, BaseNotificationService) + +DOMAIN = "apns" +APNS_DEVICES = "apns.yaml" +DEVICE_TRACKER_DOMAIN = "device_tracker" + + +def get_service(hass, config): + """Return push service""" + name = config.get("name") + if name is None: + logging.error("Name must be specified.") + return None + + cert_file = config.get('cert_file') + if cert_file is None: + logging.error("Certificate must be specified.") + return None + + sandbox = bool(config.get('sandbox', False)) + + service = ApnsNotificationService(hass, name, sandbox, cert_file) + hass.services.register(DOMAIN, name, service.register) + return service + + +class ApnsDevice(object): + """ + Stores information about a device that is + registered for push notifications. + """ + + def __init__(self, push_id, name, tracking_device_id=None): + """Initialize Apns Device.""" + self.device_push_id = push_id + self.device_name = name + self.tracking_id = tracking_device_id + + @property + def push_id(self): + """The apns id for the device.""" + return self.device_push_id + + @property + def name(self): + """The friendly name for the device.""" + return self.device_name + + @property + def tracking_device_id(self): + """ + The id of a device that is tracked by the device + tracking component. + """ + return self.tracking_id + + @property + def full_tracking_device_id(self): + """ + The full id of a device that is tracked by the device + tracking component. + """ + return DEVICE_TRACKER_DOMAIN + '.' + self.tracking_id + + def __eq__(self, other): + """Return the comparision.""" + if isinstance(other, self.__class__): + return self.push_id == other.push_id and self.name == other.name + return NotImplemented + + def __ne__(self, other): + """Return the comparision.""" + return not self.__eq__(other) + + +class ApnsNotificationService(BaseNotificationService): + """Implement the notification service for the AWS SNS service.""" + + def __init__(self, hass, app_name, sandbox, cert_file): + """Initialize APNS application.""" + self.hass = hass + self.app_name = app_name + self.sandbox = sandbox + self.certificate = cert_file + self.yaml_path = hass.config.path(app_name + '_' + APNS_DEVICES) + self.devices = {} + self.device_states = {} + if os.path.isfile(self.yaml_path): + self.devices = { + str(key): ApnsDevice( + str(key), + value.get('name'), + value.get('tracking_device_id') + ) + for (key, value) in + load_yaml_config_file(self.yaml_path).items() + } + + def state_changed_listener(entity_id, from_s, to_s): + """ + Track device state change if a device + has a tracking id specified. + """ + self.device_states[entity_id] = str(to_s.state) + return + + tracking_ids = [ + device.full_tracking_device_id + for (key, device) in self.devices.items() + if device.tracking_device_id is not None + ] + track_state_change(hass, tracking_ids, state_changed_listener) + + @staticmethod + def write_device(out, device): + """Write a single device to file.""" + attributes = [] + if device.name is not None: + attributes.append( + 'name: {}'.format(device.name)) + if device.tracking_device_id is not None: + attributes.append( + 'tracking_device_id: {}'.format(device.tracking_device_id)) + + out.write(device.push_id) + out.write(": {") + if len(attributes) > 0: + separator = ", " + out.write(separator.join(attributes)) + + out.write("}\n") + + def write_devices(self): + """Write all known devices to file.""" + with open(self.yaml_path, 'w+') as out: + for _, device in self.devices.items(): + ApnsNotificationService.write_device(out, device) + + def register(self, call): + """Register a device to receive push messages.""" + + push_id = call.data.get("push_id") + if push_id is None: + return False + + device_name = call.data.get("name") + current_device = self.devices.get(push_id) + current_tracking_id = None if current_device is None \ + else current_device.tracking_device_id + + device = ApnsDevice( + push_id, + device_name, + current_tracking_id) + + if current_device is None: + self.devices[push_id] = device + with open(self.yaml_path, 'a') as out: + self.write_device(out, device) + return + + if device != current_device: + self.devices[push_id] = device + self.write_devices() + + return True + + def send_message(self, message="", **kwargs): + """Send push message to registered devices.""" + from apns3 import APNs, Payload + + apns = APNs( + use_sandbox=self.sandbox, + cert_file=self.certificate, + key_file=self.certificate) + + device_state = kwargs.get(ATTR_TARGET) + message_data = kwargs.get(ATTR_DATA) + + if message_data is None: + message_data = {} + + payload = Payload( + message, + message_data.get("badge"), + message_data.get("sound"), + message_data.get("category"), + message_data.get("custom", {}), + message_data.get("content_available", False)) + + for push_id, device in self.devices.items(): + if device_state is None: + apns.gateway_server.send_notification(push_id, payload) + elif device.tracking_device_id is not None: + state = self.device_states.get(device.full_tracking_device_id) + if state == str(device_state): + apns.gateway_server.send_notification(push_id, payload) + + return True diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py new file mode 100644 index 00000000000000..0fe77bdc23e024 --- /dev/null +++ b/tests/components/notify/test_apns.py @@ -0,0 +1,266 @@ +"""The tests for the APNS component.""" +import unittest +import os + +import homeassistant.components.notify as notify +from tests.common import get_test_home_assistant +from homeassistant.config import load_yaml_config_file +from unittest.mock import patch + + +class TestApns(unittest.TestCase): + """Test the APNS component.""" + + def test_apns_setup_full(self): + """Test setup with all data.""" + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'sandbox': 'True', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + self.assertTrue(notify.setup(hass, config)) + + def test_apns_setup_missing_name(self): + """Test setup with missing name.""" + config = { + 'notify': { + 'platform': 'apns', + 'sandbox': 'True', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + self.assertFalse(notify.setup(hass, config)) + + def test_apns_setup_missing_certificate(self): + """Test setup with missing name.""" + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app' + } + } + hass = get_test_home_assistant() + self.assertFalse(notify.setup(hass, config)) + + def test_register_new_device(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + {'push_id': '1234', + 'name': 'test device'}, + blocking=True)) + + devices = {str(key): value for (key,value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('test device', test_device_1.get('name')) + + os.remove(devices_path) + + def test_register_device_without_name(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', 'test_app', + {'push_id': '1234'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device = devices.get('1234') + + self.assertIsNotNone(test_device) + self.assertIsNone(test_device.get('name')) + + os.remove(devices_path) + + def test_update_existing_device(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1}\n') + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + {'push_id': '1234', + 'name': 'updated device 1'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('updated device 1', test_device_1.get('name')) + + os.remove(devices_path) + + def test_update_existing_device_with_tracking_id(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + { 'push_id': '1234', + 'name': 'updated device 1'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('tracking123', + test_device_1.get('tracking_device_id')) + self.assertEqual('tracking456', + test_device_2.get('tracking_device_id')) + + os.remove(devices_path) + + @patch('apns3.GatewayConnection.send_notification') + def test_send(self, mock_send_notification): + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1}\n') + + notify.setup(hass, config) + + self.assertTrue(hass.services.call('notify', 'test_app', + {'message': 'Hello', + 'data': { + 'badge': 1, + 'sound': 'test.mp3', + 'category': 'testing' + } + }, + blocking=True)) + + self.assertTrue(mock_send_notification.called) + self.assertEqual(1, len(mock_send_notification.mock_calls)) + + target = mock_send_notification.mock_calls[0][1][0] + payload = mock_send_notification.mock_calls[0][1][1] + + self.assertEqual('1234', target) + self.assertEqual('Hello', payload.alert) + self.assertEqual(1, payload.badge) + self.assertEqual('test.mp3', payload.sound) + self.assertEqual('testing', payload.category) + + @patch('apns3.GatewayConnection.send_notification') + def test_send_with_state(self, mock_send_notification): + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + + notify.setup(hass, config) + + hass.states.set('device_tracker.tracking456', + 'home', + force_update=True) + + hass.pool.block_till_done() + + self.assertTrue(hass.services.call('notify', 'test_app', + {'message': 'Hello', + 'target': 'home'}, + blocking=True)) + + self.assertTrue(mock_send_notification.called) + self.assertEqual(1, len(mock_send_notification.mock_calls)) + + target = mock_send_notification.mock_calls[0][1][0] + payload = mock_send_notification.mock_calls[0][1][1] + + self.assertEqual('5678', target) + self.assertEqual('Hello', payload.alert) From f48fe528b57ff94d2ac871037447b0cd24843b3b Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 25 Aug 2016 11:07:51 +0100 Subject: [PATCH 02/33] some lint changes --- homeassistant/components/notify/apns.py | 4 +++- tests/components/notify/test_apns.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 9da4c05a5ae74d..4e756bca887ae2 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -13,7 +13,8 @@ def get_service(hass, config): - """Return push service""" + """Return push service.""" + name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -139,6 +140,7 @@ def write_device(out, device): def write_devices(self): """Write all known devices to file.""" + with open(self.yaml_path, 'w+') as out: for _, device in self.devices.items(): ApnsNotificationService.write_device(out, device) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 0fe77bdc23e024..037a63b9581b34 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -67,10 +67,10 @@ def test_register_new_device(self): self.assertTrue(hass.services.call('apns', 'test_app', {'push_id': '1234', - 'name': 'test device'}, + 'name': 'test device'}, blocking=True)) - devices = {str(key): value for (key,value) in + devices = {str(key): value for (key, value) in load_yaml_config_file(devices_path).items()} test_device_1 = devices.get('1234') @@ -162,14 +162,14 @@ def test_update_existing_device_with_tracking_id(self): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 notify.setup(hass, config) self.assertTrue(hass.services.call('apns', 'test_app', - { 'push_id': '1234', - 'name': 'updated device 1'}, + {'push_id': '1234', + 'name': 'updated device 1'}, blocking=True)) devices = {str(key): value for (key, value) in @@ -240,8 +240,8 @@ def test_send_with_state(self, mock_send_notification): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 notify.setup(hass, config) From 0762673dd4802bc84b06c45f47e23c4a242e467e Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 25 Aug 2016 22:56:42 +0100 Subject: [PATCH 03/33] added docs --- homeassistant/components/notify/apns.py | 83 ++++++++++++++++++- .../components/notify/apns_services.yaml | 11 +++ 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/notify/apns_services.yaml diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 4e756bca887ae2..50728607272bb6 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,20 +1,91 @@ -"""Support for Apple push notification service.""" +""" +The APNS platform uses the Apple Push Notification service (APNS) to deliver +notifications from Home Assistant. + +To use the APNS service you will need an apple developer account +and you will need to create an App to receive push notifications. +For more information see the apple developer documentation. + +Sample Configuration: + + notify: + platform: apns + name: doorbell_app + sandbox: true + cert_file: doorbell_app.pem + +Configuration Variables: + + name: The name of the app. + sandbox: If true notifications will be sent to the sandbox (test) notification service. + cert_file: The certificate to use to authenticate with the APNS service. + +Usage: + + The APNS platform will register two services, notify/[app_name] and apns/[app_name]. + + apns/app_name: + This service will register device id's with home assistant. In order to + receive a notification a device must be registered. The app on the + device can use this service to send its id during startup, the id will + be stored in the [app_name]_apns.yaml. + + See didRegisterForRemoteNotificationsWithDeviceToken in the apple developer + documentation for more information. + + + notify/app_name + This service will send messages to a registered device. The following parameters can be used: + + message: + The message to send + + target: + The desired state of the device, only devices that match the state will receive messages. + To enable state tracking a registered device must have a device_tracking_id added to the + [app_name]_apns.yaml file. If this id matches a device in known_devices.yaml its state + will be tracked. + + data: + badge: + The number to display as the badge of the app ic + sound: + The name of a sound file in the app bundle or in the Library/Sounds folder. + category: + Provide this key with a string value that represents the identifier + property of the UIMutableUserNotificationCategory + content_available: + Provide this key with a value of 1 to indicate that new content is available. +""" import logging import os +import voluptuous as vol from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( ATTR_TARGET, ATTR_DATA, BaseNotificationService) +import homeassistant.helpers.config_validation as cv DOMAIN = "apns" APNS_DEVICES = "apns.yaml" DEVICE_TRACKER_DOMAIN = "device_tracker" +SERVICE_REGISTER = "register" +ATTR_PUSH_ID = "push_id" +ATTR_NAME = "name" + +REGISTER_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_PUSH_ID): cv.template, + vol.Optional(ATTR_NAME, default=None): cv.string, +}) def get_service(hass, config): """Return push service.""" + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'apns_services.yaml')) + name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -28,7 +99,11 @@ def get_service(hass, config): sandbox = bool(config.get('sandbox', False)) service = ApnsNotificationService(hass, name, sandbox, cert_file) - hass.services.register(DOMAIN, name, service.register) + hass.services.register(DOMAIN, + name, + service.register, + descriptions.get(SERVICE_REGISTER), + schema=REGISTER_SERVICE_SCHEMA) return service @@ -148,11 +223,11 @@ def write_devices(self): def register(self, call): """Register a device to receive push messages.""" - push_id = call.data.get("push_id") + push_id = call.data.get(ATTR_PUSH_ID) if push_id is None: return False - device_name = call.data.get("name") + device_name = call.data.get(ATTR_NAME) current_device = self.devices.get(push_id) current_tracking_id = None if current_device is None \ else current_device.tracking_device_id diff --git a/homeassistant/components/notify/apns_services.yaml b/homeassistant/components/notify/apns_services.yaml new file mode 100644 index 00000000000000..41f1baf373f054 --- /dev/null +++ b/homeassistant/components/notify/apns_services.yaml @@ -0,0 +1,11 @@ +register: + description: Registers a device to recieve push notifications. + + fields: + push_id: + description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service. + example: '72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62' + + name: + description: A friendly name for the device (optional). + example: 'Sam''s iPhone' \ No newline at end of file From 9f9a8b6fb9074670a3b896e6e6c3d91540db3427 Mon Sep 17 00:00:00 2001 From: samowens Date: Wed, 24 Aug 2016 23:26:31 +0100 Subject: [PATCH 04/33] added push notification implementation --- homeassistant/components/notify/apns.py | 206 ++++++++++++++++++ tests/components/notify/test_apns.py | 266 ++++++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 homeassistant/components/notify/apns.py create mode 100644 tests/components/notify/test_apns.py diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py new file mode 100644 index 00000000000000..9da4c05a5ae74d --- /dev/null +++ b/homeassistant/components/notify/apns.py @@ -0,0 +1,206 @@ +"""Support for Apple push notification service.""" +import logging +import os + +from homeassistant.helpers.event import track_state_change +from homeassistant.config import load_yaml_config_file +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_DATA, BaseNotificationService) + +DOMAIN = "apns" +APNS_DEVICES = "apns.yaml" +DEVICE_TRACKER_DOMAIN = "device_tracker" + + +def get_service(hass, config): + """Return push service""" + name = config.get("name") + if name is None: + logging.error("Name must be specified.") + return None + + cert_file = config.get('cert_file') + if cert_file is None: + logging.error("Certificate must be specified.") + return None + + sandbox = bool(config.get('sandbox', False)) + + service = ApnsNotificationService(hass, name, sandbox, cert_file) + hass.services.register(DOMAIN, name, service.register) + return service + + +class ApnsDevice(object): + """ + Stores information about a device that is + registered for push notifications. + """ + + def __init__(self, push_id, name, tracking_device_id=None): + """Initialize Apns Device.""" + self.device_push_id = push_id + self.device_name = name + self.tracking_id = tracking_device_id + + @property + def push_id(self): + """The apns id for the device.""" + return self.device_push_id + + @property + def name(self): + """The friendly name for the device.""" + return self.device_name + + @property + def tracking_device_id(self): + """ + The id of a device that is tracked by the device + tracking component. + """ + return self.tracking_id + + @property + def full_tracking_device_id(self): + """ + The full id of a device that is tracked by the device + tracking component. + """ + return DEVICE_TRACKER_DOMAIN + '.' + self.tracking_id + + def __eq__(self, other): + """Return the comparision.""" + if isinstance(other, self.__class__): + return self.push_id == other.push_id and self.name == other.name + return NotImplemented + + def __ne__(self, other): + """Return the comparision.""" + return not self.__eq__(other) + + +class ApnsNotificationService(BaseNotificationService): + """Implement the notification service for the AWS SNS service.""" + + def __init__(self, hass, app_name, sandbox, cert_file): + """Initialize APNS application.""" + self.hass = hass + self.app_name = app_name + self.sandbox = sandbox + self.certificate = cert_file + self.yaml_path = hass.config.path(app_name + '_' + APNS_DEVICES) + self.devices = {} + self.device_states = {} + if os.path.isfile(self.yaml_path): + self.devices = { + str(key): ApnsDevice( + str(key), + value.get('name'), + value.get('tracking_device_id') + ) + for (key, value) in + load_yaml_config_file(self.yaml_path).items() + } + + def state_changed_listener(entity_id, from_s, to_s): + """ + Track device state change if a device + has a tracking id specified. + """ + self.device_states[entity_id] = str(to_s.state) + return + + tracking_ids = [ + device.full_tracking_device_id + for (key, device) in self.devices.items() + if device.tracking_device_id is not None + ] + track_state_change(hass, tracking_ids, state_changed_listener) + + @staticmethod + def write_device(out, device): + """Write a single device to file.""" + attributes = [] + if device.name is not None: + attributes.append( + 'name: {}'.format(device.name)) + if device.tracking_device_id is not None: + attributes.append( + 'tracking_device_id: {}'.format(device.tracking_device_id)) + + out.write(device.push_id) + out.write(": {") + if len(attributes) > 0: + separator = ", " + out.write(separator.join(attributes)) + + out.write("}\n") + + def write_devices(self): + """Write all known devices to file.""" + with open(self.yaml_path, 'w+') as out: + for _, device in self.devices.items(): + ApnsNotificationService.write_device(out, device) + + def register(self, call): + """Register a device to receive push messages.""" + + push_id = call.data.get("push_id") + if push_id is None: + return False + + device_name = call.data.get("name") + current_device = self.devices.get(push_id) + current_tracking_id = None if current_device is None \ + else current_device.tracking_device_id + + device = ApnsDevice( + push_id, + device_name, + current_tracking_id) + + if current_device is None: + self.devices[push_id] = device + with open(self.yaml_path, 'a') as out: + self.write_device(out, device) + return + + if device != current_device: + self.devices[push_id] = device + self.write_devices() + + return True + + def send_message(self, message="", **kwargs): + """Send push message to registered devices.""" + from apns3 import APNs, Payload + + apns = APNs( + use_sandbox=self.sandbox, + cert_file=self.certificate, + key_file=self.certificate) + + device_state = kwargs.get(ATTR_TARGET) + message_data = kwargs.get(ATTR_DATA) + + if message_data is None: + message_data = {} + + payload = Payload( + message, + message_data.get("badge"), + message_data.get("sound"), + message_data.get("category"), + message_data.get("custom", {}), + message_data.get("content_available", False)) + + for push_id, device in self.devices.items(): + if device_state is None: + apns.gateway_server.send_notification(push_id, payload) + elif device.tracking_device_id is not None: + state = self.device_states.get(device.full_tracking_device_id) + if state == str(device_state): + apns.gateway_server.send_notification(push_id, payload) + + return True diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py new file mode 100644 index 00000000000000..0fe77bdc23e024 --- /dev/null +++ b/tests/components/notify/test_apns.py @@ -0,0 +1,266 @@ +"""The tests for the APNS component.""" +import unittest +import os + +import homeassistant.components.notify as notify +from tests.common import get_test_home_assistant +from homeassistant.config import load_yaml_config_file +from unittest.mock import patch + + +class TestApns(unittest.TestCase): + """Test the APNS component.""" + + def test_apns_setup_full(self): + """Test setup with all data.""" + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'sandbox': 'True', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + self.assertTrue(notify.setup(hass, config)) + + def test_apns_setup_missing_name(self): + """Test setup with missing name.""" + config = { + 'notify': { + 'platform': 'apns', + 'sandbox': 'True', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + self.assertFalse(notify.setup(hass, config)) + + def test_apns_setup_missing_certificate(self): + """Test setup with missing name.""" + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app' + } + } + hass = get_test_home_assistant() + self.assertFalse(notify.setup(hass, config)) + + def test_register_new_device(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + {'push_id': '1234', + 'name': 'test device'}, + blocking=True)) + + devices = {str(key): value for (key,value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('test device', test_device_1.get('name')) + + os.remove(devices_path) + + def test_register_device_without_name(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', 'test_app', + {'push_id': '1234'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device = devices.get('1234') + + self.assertIsNotNone(test_device) + self.assertIsNone(test_device.get('name')) + + os.remove(devices_path) + + def test_update_existing_device(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1}\n') + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + {'push_id': '1234', + 'name': 'updated device 1'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('updated device 1', test_device_1.get('name')) + + os.remove(devices_path) + + def test_update_existing_device_with_tracking_id(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + { 'push_id': '1234', + 'name': 'updated device 1'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('tracking123', + test_device_1.get('tracking_device_id')) + self.assertEqual('tracking456', + test_device_2.get('tracking_device_id')) + + os.remove(devices_path) + + @patch('apns3.GatewayConnection.send_notification') + def test_send(self, mock_send_notification): + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1}\n') + + notify.setup(hass, config) + + self.assertTrue(hass.services.call('notify', 'test_app', + {'message': 'Hello', + 'data': { + 'badge': 1, + 'sound': 'test.mp3', + 'category': 'testing' + } + }, + blocking=True)) + + self.assertTrue(mock_send_notification.called) + self.assertEqual(1, len(mock_send_notification.mock_calls)) + + target = mock_send_notification.mock_calls[0][1][0] + payload = mock_send_notification.mock_calls[0][1][1] + + self.assertEqual('1234', target) + self.assertEqual('Hello', payload.alert) + self.assertEqual(1, payload.badge) + self.assertEqual('test.mp3', payload.sound) + self.assertEqual('testing', payload.category) + + @patch('apns3.GatewayConnection.send_notification') + def test_send_with_state(self, mock_send_notification): + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + + notify.setup(hass, config) + + hass.states.set('device_tracker.tracking456', + 'home', + force_update=True) + + hass.pool.block_till_done() + + self.assertTrue(hass.services.call('notify', 'test_app', + {'message': 'Hello', + 'target': 'home'}, + blocking=True)) + + self.assertTrue(mock_send_notification.called) + self.assertEqual(1, len(mock_send_notification.mock_calls)) + + target = mock_send_notification.mock_calls[0][1][0] + payload = mock_send_notification.mock_calls[0][1][1] + + self.assertEqual('5678', target) + self.assertEqual('Hello', payload.alert) From 07e79eb6581d6d453c11bc6d374e5484e110778f Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 25 Aug 2016 11:07:51 +0100 Subject: [PATCH 05/33] some lint changes --- homeassistant/components/notify/apns.py | 4 +++- tests/components/notify/test_apns.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 9da4c05a5ae74d..4e756bca887ae2 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -13,7 +13,8 @@ def get_service(hass, config): - """Return push service""" + """Return push service.""" + name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -139,6 +140,7 @@ def write_device(out, device): def write_devices(self): """Write all known devices to file.""" + with open(self.yaml_path, 'w+') as out: for _, device in self.devices.items(): ApnsNotificationService.write_device(out, device) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 0fe77bdc23e024..037a63b9581b34 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -67,10 +67,10 @@ def test_register_new_device(self): self.assertTrue(hass.services.call('apns', 'test_app', {'push_id': '1234', - 'name': 'test device'}, + 'name': 'test device'}, blocking=True)) - devices = {str(key): value for (key,value) in + devices = {str(key): value for (key, value) in load_yaml_config_file(devices_path).items()} test_device_1 = devices.get('1234') @@ -162,14 +162,14 @@ def test_update_existing_device_with_tracking_id(self): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 notify.setup(hass, config) self.assertTrue(hass.services.call('apns', 'test_app', - { 'push_id': '1234', - 'name': 'updated device 1'}, + {'push_id': '1234', + 'name': 'updated device 1'}, blocking=True)) devices = {str(key): value for (key, value) in @@ -240,8 +240,8 @@ def test_send_with_state(self, mock_send_notification): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 notify.setup(hass, config) From bc5eaa2bbd4a3bbdb1fa832a35d434a6bec0db9f Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 25 Aug 2016 22:56:42 +0100 Subject: [PATCH 06/33] added docs --- homeassistant/components/notify/apns.py | 83 ++++++++++++++++++- .../components/notify/apns_services.yaml | 11 +++ 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/notify/apns_services.yaml diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 4e756bca887ae2..50728607272bb6 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,20 +1,91 @@ -"""Support for Apple push notification service.""" +""" +The APNS platform uses the Apple Push Notification service (APNS) to deliver +notifications from Home Assistant. + +To use the APNS service you will need an apple developer account +and you will need to create an App to receive push notifications. +For more information see the apple developer documentation. + +Sample Configuration: + + notify: + platform: apns + name: doorbell_app + sandbox: true + cert_file: doorbell_app.pem + +Configuration Variables: + + name: The name of the app. + sandbox: If true notifications will be sent to the sandbox (test) notification service. + cert_file: The certificate to use to authenticate with the APNS service. + +Usage: + + The APNS platform will register two services, notify/[app_name] and apns/[app_name]. + + apns/app_name: + This service will register device id's with home assistant. In order to + receive a notification a device must be registered. The app on the + device can use this service to send its id during startup, the id will + be stored in the [app_name]_apns.yaml. + + See didRegisterForRemoteNotificationsWithDeviceToken in the apple developer + documentation for more information. + + + notify/app_name + This service will send messages to a registered device. The following parameters can be used: + + message: + The message to send + + target: + The desired state of the device, only devices that match the state will receive messages. + To enable state tracking a registered device must have a device_tracking_id added to the + [app_name]_apns.yaml file. If this id matches a device in known_devices.yaml its state + will be tracked. + + data: + badge: + The number to display as the badge of the app ic + sound: + The name of a sound file in the app bundle or in the Library/Sounds folder. + category: + Provide this key with a string value that represents the identifier + property of the UIMutableUserNotificationCategory + content_available: + Provide this key with a value of 1 to indicate that new content is available. +""" import logging import os +import voluptuous as vol from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( ATTR_TARGET, ATTR_DATA, BaseNotificationService) +import homeassistant.helpers.config_validation as cv DOMAIN = "apns" APNS_DEVICES = "apns.yaml" DEVICE_TRACKER_DOMAIN = "device_tracker" +SERVICE_REGISTER = "register" +ATTR_PUSH_ID = "push_id" +ATTR_NAME = "name" + +REGISTER_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_PUSH_ID): cv.template, + vol.Optional(ATTR_NAME, default=None): cv.string, +}) def get_service(hass, config): """Return push service.""" + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'apns_services.yaml')) + name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -28,7 +99,11 @@ def get_service(hass, config): sandbox = bool(config.get('sandbox', False)) service = ApnsNotificationService(hass, name, sandbox, cert_file) - hass.services.register(DOMAIN, name, service.register) + hass.services.register(DOMAIN, + name, + service.register, + descriptions.get(SERVICE_REGISTER), + schema=REGISTER_SERVICE_SCHEMA) return service @@ -148,11 +223,11 @@ def write_devices(self): def register(self, call): """Register a device to receive push messages.""" - push_id = call.data.get("push_id") + push_id = call.data.get(ATTR_PUSH_ID) if push_id is None: return False - device_name = call.data.get("name") + device_name = call.data.get(ATTR_NAME) current_device = self.devices.get(push_id) current_tracking_id = None if current_device is None \ else current_device.tracking_device_id diff --git a/homeassistant/components/notify/apns_services.yaml b/homeassistant/components/notify/apns_services.yaml new file mode 100644 index 00000000000000..41f1baf373f054 --- /dev/null +++ b/homeassistant/components/notify/apns_services.yaml @@ -0,0 +1,11 @@ +register: + description: Registers a device to recieve push notifications. + + fields: + push_id: + description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service. + example: '72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62' + + name: + description: A friendly name for the device (optional). + example: 'Sam''s iPhone' \ No newline at end of file From 2e6317ff7343cbb756dec16e98a632e6559897ba Mon Sep 17 00:00:00 2001 From: samowens Date: Fri, 26 Aug 2016 09:35:06 +0100 Subject: [PATCH 07/33] Fixed comment formatting issues --- homeassistant/components/notify/apns.py | 53 ++++++++++++++++--------- tests/components/notify/test_apns.py | 8 ++-- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 50728607272bb6..b38c09fa0918e6 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,4 +1,6 @@ """ +APNS Notificaion platform. + The APNS platform uses the Apple Push Notification service (APNS) to deliver notifications from Home Assistant. @@ -16,13 +18,18 @@ Configuration Variables: - name: The name of the app. - sandbox: If true notifications will be sent to the sandbox (test) notification service. - cert_file: The certificate to use to authenticate with the APNS service. + name: + The name of the app. + sandbox: + If true notifications will be sent to the sandbox (test) notification + service. + cert_file: + The certificate to use to authenticate with the APNS service. Usage: - The APNS platform will register two services, notify/[app_name] and apns/[app_name]. + The APNS platform will register two services, notify/[app_name] and + apns/[app_name]. apns/app_name: This service will register device id's with home assistant. In order to @@ -30,32 +37,36 @@ device can use this service to send its id during startup, the id will be stored in the [app_name]_apns.yaml. - See didRegisterForRemoteNotificationsWithDeviceToken in the apple developer - documentation for more information. + See didRegisterForRemoteNotificationsWithDeviceToken in the apple + developer documentation for more information. notify/app_name - This service will send messages to a registered device. The following parameters can be used: + This service will send messages to a registered device. The following + parameters can be used: message: The message to send target: - The desired state of the device, only devices that match the state will receive messages. - To enable state tracking a registered device must have a device_tracking_id added to the - [app_name]_apns.yaml file. If this id matches a device in known_devices.yaml its state - will be tracked. + The desired state of the device, only devices that match the state + will receive messages. To enable state tracking a registered + device must have a device_tracking_id added to the + [app_name]_apns.yaml file. If this id matches a device in + known_devices.yaml its state will be tracked. data: badge: The number to display as the badge of the app ic sound: - The name of a sound file in the app bundle or in the Library/Sounds folder. + The name of a sound file in the app bundle or in the + Library/Sounds folder. category: - Provide this key with a string value that represents the identifier - property of the UIMutableUserNotificationCategory + Provide this key with a string value that represents the + identifier property of the UIMutableUserNotificationCategory content_available: - Provide this key with a value of 1 to indicate that new content is available. + Provide this key with a value of 1 to indicate that new + content is available. """ import logging import os @@ -80,9 +91,9 @@ vol.Optional(ATTR_NAME, default=None): cv.string, }) + def get_service(hass, config): """Return push service.""" - descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'apns_services.yaml')) @@ -109,6 +120,8 @@ def get_service(hass, config): class ApnsDevice(object): """ + Apns Device class. + Stores information about a device that is registered for push notifications. """ @@ -132,6 +145,8 @@ def name(self): @property def tracking_device_id(self): """ + Device Id. + The id of a device that is tracked by the device tracking component. """ @@ -140,6 +155,8 @@ def tracking_device_id(self): @property def full_tracking_device_id(self): """ + Fully qualified device id. + The full id of a device that is tracked by the device tracking component. """ @@ -181,6 +198,8 @@ def __init__(self, hass, app_name, sandbox, cert_file): def state_changed_listener(entity_id, from_s, to_s): """ + Listener for sate change. + Track device state change if a device has a tracking id specified. """ @@ -215,14 +234,12 @@ def write_device(out, device): def write_devices(self): """Write all known devices to file.""" - with open(self.yaml_path, 'w+') as out: for _, device in self.devices.items(): ApnsNotificationService.write_device(out, device) def register(self, call): """Register a device to receive push messages.""" - push_id = call.data.get(ATTR_PUSH_ID) if push_id is None: return False diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 037a63b9581b34..9d53b1fe9d7290 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -84,7 +84,7 @@ def test_register_new_device(self): os.remove(devices_path) def test_register_device_without_name(self): - + """Test registering a without a name.""" config = { 'notify': { 'platform': 'apns', @@ -114,7 +114,7 @@ def test_register_device_without_name(self): os.remove(devices_path) def test_update_existing_device(self): - + """Test updating an existing device.""" config = { 'notify': { 'platform': 'apns', @@ -150,7 +150,7 @@ def test_update_existing_device(self): os.remove(devices_path) def test_update_existing_device_with_tracking_id(self): - + """Test updating an existing device that has a tracking id.""" config = { 'notify': { 'platform': 'apns', @@ -190,6 +190,7 @@ def test_update_existing_device_with_tracking_id(self): @patch('apns3.GatewayConnection.send_notification') def test_send(self, mock_send_notification): + """Test updating an existing device.""" config = { 'notify': { 'platform': 'apns', @@ -229,6 +230,7 @@ def test_send(self, mock_send_notification): @patch('apns3.GatewayConnection.send_notification') def test_send_with_state(self, mock_send_notification): + """Test updating an existing device.""" config = { 'notify': { 'platform': 'apns', From 837cfd0986cd3a2abcb2c707116aaf89c75ab860 Mon Sep 17 00:00:00 2001 From: sam-io Date: Fri, 26 Aug 2016 22:13:46 +0100 Subject: [PATCH 08/33] Added requirments --- homeassistant/components/notify/apns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index b38c09fa0918e6..1c89813bc7ca23 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,5 +1,5 @@ """ -APNS Notificaion platform. +APNS Notification platform. The APNS platform uses the Apple Push Notification service (APNS) to deliver notifications from Home Assistant. @@ -91,6 +91,7 @@ vol.Optional(ATTR_NAME, default=None): cv.string, }) +REQUIREMENTS = ["apns3==1.0.0"] def get_service(hass, config): """Return push service.""" From ffd85cab8b24e4589b1afd69d240119001af4523 Mon Sep 17 00:00:00 2001 From: sam-io Date: Fri, 26 Aug 2016 22:15:18 +0100 Subject: [PATCH 09/33] Update requirements_all.txt --- requirements_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index fd16391f6e613e..3bf094843845cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -52,6 +52,9 @@ boto3==1.3.1 # homeassistant.components.http cherrypy==7.1.0 +# homeassistant.components.notify.apns +apns3==1.0.0 + # homeassistant.scripts.check_config colorlog>2.1<3 From ef77e1de8170b5b2c4bb654f4114a3846aa13e89 Mon Sep 17 00:00:00 2001 From: sam-io Date: Fri, 26 Aug 2016 22:25:38 +0100 Subject: [PATCH 10/33] Update apns.py --- homeassistant/components/notify/apns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 1c89813bc7ca23..f551c66a6e676b 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -93,6 +93,7 @@ REQUIREMENTS = ["apns3==1.0.0"] + def get_service(hass, config): """Return push service.""" descriptions = load_yaml_config_file( From f57366e4904bfe60a638c8cc1534887568b7deaa Mon Sep 17 00:00:00 2001 From: samowens Date: Mon, 5 Sep 2016 08:50:45 +0100 Subject: [PATCH 11/33] re-generated requirments_all.txt --- requirements_all.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 3bf094843845cc..0de3d5eda59a5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -31,6 +31,9 @@ Werkzeug==0.11.10 # homeassistant.components.apcupsd apcaccess==0.0.4 +# homeassistant.components.notify.apns +apns3==1.0.0 + # homeassistant.components.sun astral==1.2 @@ -52,9 +55,6 @@ boto3==1.3.1 # homeassistant.components.http cherrypy==7.1.0 -# homeassistant.components.notify.apns -apns3==1.0.0 - # homeassistant.scripts.check_config colorlog>2.1<3 From 941ad847107e123bd0678ce738684c51ae00691a Mon Sep 17 00:00:00 2001 From: sam-io Date: Mon, 12 Sep 2016 09:23:45 +0100 Subject: [PATCH 12/33] Added link to online docs --- homeassistant/components/notify/apns.py | 68 +------------------------ 1 file changed, 2 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index f551c66a6e676b..817773ab3a9b99 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,72 +1,8 @@ """ APNS Notification platform. -The APNS platform uses the Apple Push Notification service (APNS) to deliver -notifications from Home Assistant. - -To use the APNS service you will need an apple developer account -and you will need to create an App to receive push notifications. -For more information see the apple developer documentation. - -Sample Configuration: - - notify: - platform: apns - name: doorbell_app - sandbox: true - cert_file: doorbell_app.pem - -Configuration Variables: - - name: - The name of the app. - sandbox: - If true notifications will be sent to the sandbox (test) notification - service. - cert_file: - The certificate to use to authenticate with the APNS service. - -Usage: - - The APNS platform will register two services, notify/[app_name] and - apns/[app_name]. - - apns/app_name: - This service will register device id's with home assistant. In order to - receive a notification a device must be registered. The app on the - device can use this service to send its id during startup, the id will - be stored in the [app_name]_apns.yaml. - - See didRegisterForRemoteNotificationsWithDeviceToken in the apple - developer documentation for more information. - - - notify/app_name - This service will send messages to a registered device. The following - parameters can be used: - - message: - The message to send - - target: - The desired state of the device, only devices that match the state - will receive messages. To enable state tracking a registered - device must have a device_tracking_id added to the - [app_name]_apns.yaml file. If this id matches a device in - known_devices.yaml its state will be tracked. - - data: - badge: - The number to display as the badge of the app ic - sound: - The name of a sound file in the app bundle or in the - Library/Sounds folder. - category: - Provide this key with a string value that represents the - identifier property of the UIMutableUserNotificationCategory - content_available: - Provide this key with a value of 1 to indicate that new - content is available. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.apns/ """ import logging import os From 087c504b8a1154122be7c9505492f6c367d03565 Mon Sep 17 00:00:00 2001 From: samowens Date: Wed, 24 Aug 2016 23:26:31 +0100 Subject: [PATCH 13/33] added push notification implementation --- homeassistant/components/notify/apns.py | 206 ++++++++++++++++++ tests/components/notify/test_apns.py | 266 ++++++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 homeassistant/components/notify/apns.py create mode 100644 tests/components/notify/test_apns.py diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py new file mode 100644 index 00000000000000..9da4c05a5ae74d --- /dev/null +++ b/homeassistant/components/notify/apns.py @@ -0,0 +1,206 @@ +"""Support for Apple push notification service.""" +import logging +import os + +from homeassistant.helpers.event import track_state_change +from homeassistant.config import load_yaml_config_file +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_DATA, BaseNotificationService) + +DOMAIN = "apns" +APNS_DEVICES = "apns.yaml" +DEVICE_TRACKER_DOMAIN = "device_tracker" + + +def get_service(hass, config): + """Return push service""" + name = config.get("name") + if name is None: + logging.error("Name must be specified.") + return None + + cert_file = config.get('cert_file') + if cert_file is None: + logging.error("Certificate must be specified.") + return None + + sandbox = bool(config.get('sandbox', False)) + + service = ApnsNotificationService(hass, name, sandbox, cert_file) + hass.services.register(DOMAIN, name, service.register) + return service + + +class ApnsDevice(object): + """ + Stores information about a device that is + registered for push notifications. + """ + + def __init__(self, push_id, name, tracking_device_id=None): + """Initialize Apns Device.""" + self.device_push_id = push_id + self.device_name = name + self.tracking_id = tracking_device_id + + @property + def push_id(self): + """The apns id for the device.""" + return self.device_push_id + + @property + def name(self): + """The friendly name for the device.""" + return self.device_name + + @property + def tracking_device_id(self): + """ + The id of a device that is tracked by the device + tracking component. + """ + return self.tracking_id + + @property + def full_tracking_device_id(self): + """ + The full id of a device that is tracked by the device + tracking component. + """ + return DEVICE_TRACKER_DOMAIN + '.' + self.tracking_id + + def __eq__(self, other): + """Return the comparision.""" + if isinstance(other, self.__class__): + return self.push_id == other.push_id and self.name == other.name + return NotImplemented + + def __ne__(self, other): + """Return the comparision.""" + return not self.__eq__(other) + + +class ApnsNotificationService(BaseNotificationService): + """Implement the notification service for the AWS SNS service.""" + + def __init__(self, hass, app_name, sandbox, cert_file): + """Initialize APNS application.""" + self.hass = hass + self.app_name = app_name + self.sandbox = sandbox + self.certificate = cert_file + self.yaml_path = hass.config.path(app_name + '_' + APNS_DEVICES) + self.devices = {} + self.device_states = {} + if os.path.isfile(self.yaml_path): + self.devices = { + str(key): ApnsDevice( + str(key), + value.get('name'), + value.get('tracking_device_id') + ) + for (key, value) in + load_yaml_config_file(self.yaml_path).items() + } + + def state_changed_listener(entity_id, from_s, to_s): + """ + Track device state change if a device + has a tracking id specified. + """ + self.device_states[entity_id] = str(to_s.state) + return + + tracking_ids = [ + device.full_tracking_device_id + for (key, device) in self.devices.items() + if device.tracking_device_id is not None + ] + track_state_change(hass, tracking_ids, state_changed_listener) + + @staticmethod + def write_device(out, device): + """Write a single device to file.""" + attributes = [] + if device.name is not None: + attributes.append( + 'name: {}'.format(device.name)) + if device.tracking_device_id is not None: + attributes.append( + 'tracking_device_id: {}'.format(device.tracking_device_id)) + + out.write(device.push_id) + out.write(": {") + if len(attributes) > 0: + separator = ", " + out.write(separator.join(attributes)) + + out.write("}\n") + + def write_devices(self): + """Write all known devices to file.""" + with open(self.yaml_path, 'w+') as out: + for _, device in self.devices.items(): + ApnsNotificationService.write_device(out, device) + + def register(self, call): + """Register a device to receive push messages.""" + + push_id = call.data.get("push_id") + if push_id is None: + return False + + device_name = call.data.get("name") + current_device = self.devices.get(push_id) + current_tracking_id = None if current_device is None \ + else current_device.tracking_device_id + + device = ApnsDevice( + push_id, + device_name, + current_tracking_id) + + if current_device is None: + self.devices[push_id] = device + with open(self.yaml_path, 'a') as out: + self.write_device(out, device) + return + + if device != current_device: + self.devices[push_id] = device + self.write_devices() + + return True + + def send_message(self, message="", **kwargs): + """Send push message to registered devices.""" + from apns3 import APNs, Payload + + apns = APNs( + use_sandbox=self.sandbox, + cert_file=self.certificate, + key_file=self.certificate) + + device_state = kwargs.get(ATTR_TARGET) + message_data = kwargs.get(ATTR_DATA) + + if message_data is None: + message_data = {} + + payload = Payload( + message, + message_data.get("badge"), + message_data.get("sound"), + message_data.get("category"), + message_data.get("custom", {}), + message_data.get("content_available", False)) + + for push_id, device in self.devices.items(): + if device_state is None: + apns.gateway_server.send_notification(push_id, payload) + elif device.tracking_device_id is not None: + state = self.device_states.get(device.full_tracking_device_id) + if state == str(device_state): + apns.gateway_server.send_notification(push_id, payload) + + return True diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py new file mode 100644 index 00000000000000..0fe77bdc23e024 --- /dev/null +++ b/tests/components/notify/test_apns.py @@ -0,0 +1,266 @@ +"""The tests for the APNS component.""" +import unittest +import os + +import homeassistant.components.notify as notify +from tests.common import get_test_home_assistant +from homeassistant.config import load_yaml_config_file +from unittest.mock import patch + + +class TestApns(unittest.TestCase): + """Test the APNS component.""" + + def test_apns_setup_full(self): + """Test setup with all data.""" + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'sandbox': 'True', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + self.assertTrue(notify.setup(hass, config)) + + def test_apns_setup_missing_name(self): + """Test setup with missing name.""" + config = { + 'notify': { + 'platform': 'apns', + 'sandbox': 'True', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + self.assertFalse(notify.setup(hass, config)) + + def test_apns_setup_missing_certificate(self): + """Test setup with missing name.""" + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app' + } + } + hass = get_test_home_assistant() + self.assertFalse(notify.setup(hass, config)) + + def test_register_new_device(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + {'push_id': '1234', + 'name': 'test device'}, + blocking=True)) + + devices = {str(key): value for (key,value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('test device', test_device_1.get('name')) + + os.remove(devices_path) + + def test_register_device_without_name(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', 'test_app', + {'push_id': '1234'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device = devices.get('1234') + + self.assertIsNotNone(test_device) + self.assertIsNone(test_device.get('name')) + + os.remove(devices_path) + + def test_update_existing_device(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1}\n') + out.write('5678: {name: test device 2}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + {'push_id': '1234', + 'name': 'updated device 1'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('updated device 1', test_device_1.get('name')) + + os.remove(devices_path) + + def test_update_existing_device_with_tracking_id(self): + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + + notify.setup(hass, config) + self.assertTrue(hass.services.call('apns', + 'test_app', + { 'push_id': '1234', + 'name': 'updated device 1'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + test_device_2 = devices.get('5678') + + self.assertIsNotNone(test_device_1) + self.assertIsNotNone(test_device_2) + + self.assertEqual('tracking123', + test_device_1.get('tracking_device_id')) + self.assertEqual('tracking456', + test_device_2.get('tracking_device_id')) + + os.remove(devices_path) + + @patch('apns3.GatewayConnection.send_notification') + def test_send(self, mock_send_notification): + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1}\n') + + notify.setup(hass, config) + + self.assertTrue(hass.services.call('notify', 'test_app', + {'message': 'Hello', + 'data': { + 'badge': 1, + 'sound': 'test.mp3', + 'category': 'testing' + } + }, + blocking=True)) + + self.assertTrue(mock_send_notification.called) + self.assertEqual(1, len(mock_send_notification.mock_calls)) + + target = mock_send_notification.mock_calls[0][1][0] + payload = mock_send_notification.mock_calls[0][1][1] + + self.assertEqual('1234', target) + self.assertEqual('Hello', payload.alert) + self.assertEqual(1, payload.badge) + self.assertEqual('test.mp3', payload.sound) + self.assertEqual('testing', payload.category) + + @patch('apns3.GatewayConnection.send_notification') + def test_send_with_state(self, mock_send_notification): + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + + notify.setup(hass, config) + + hass.states.set('device_tracker.tracking456', + 'home', + force_update=True) + + hass.pool.block_till_done() + + self.assertTrue(hass.services.call('notify', 'test_app', + {'message': 'Hello', + 'target': 'home'}, + blocking=True)) + + self.assertTrue(mock_send_notification.called) + self.assertEqual(1, len(mock_send_notification.mock_calls)) + + target = mock_send_notification.mock_calls[0][1][0] + payload = mock_send_notification.mock_calls[0][1][1] + + self.assertEqual('5678', target) + self.assertEqual('Hello', payload.alert) From 4ad485260c38ff68b65cd8d1a0cee3d1ef237296 Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 25 Aug 2016 11:07:51 +0100 Subject: [PATCH 14/33] some lint changes --- homeassistant/components/notify/apns.py | 4 +++- tests/components/notify/test_apns.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 9da4c05a5ae74d..4e756bca887ae2 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -13,7 +13,8 @@ def get_service(hass, config): - """Return push service""" + """Return push service.""" + name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -139,6 +140,7 @@ def write_device(out, device): def write_devices(self): """Write all known devices to file.""" + with open(self.yaml_path, 'w+') as out: for _, device in self.devices.items(): ApnsNotificationService.write_device(out, device) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 0fe77bdc23e024..037a63b9581b34 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -67,10 +67,10 @@ def test_register_new_device(self): self.assertTrue(hass.services.call('apns', 'test_app', {'push_id': '1234', - 'name': 'test device'}, + 'name': 'test device'}, blocking=True)) - devices = {str(key): value for (key,value) in + devices = {str(key): value for (key, value) in load_yaml_config_file(devices_path).items()} test_device_1 = devices.get('1234') @@ -162,14 +162,14 @@ def test_update_existing_device_with_tracking_id(self): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 notify.setup(hass, config) self.assertTrue(hass.services.call('apns', 'test_app', - { 'push_id': '1234', - 'name': 'updated device 1'}, + {'push_id': '1234', + 'name': 'updated device 1'}, blocking=True)) devices = {str(key): value for (key, value) in @@ -240,8 +240,8 @@ def test_send_with_state(self, mock_send_notification): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 notify.setup(hass, config) From cced763febf1c9565f2ec32fbd320a1f81f83847 Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 25 Aug 2016 22:56:42 +0100 Subject: [PATCH 15/33] added docs --- homeassistant/components/notify/apns.py | 83 ++++++++++++++++++- .../components/notify/apns_services.yaml | 11 +++ 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/notify/apns_services.yaml diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 4e756bca887ae2..50728607272bb6 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,20 +1,91 @@ -"""Support for Apple push notification service.""" +""" +The APNS platform uses the Apple Push Notification service (APNS) to deliver +notifications from Home Assistant. + +To use the APNS service you will need an apple developer account +and you will need to create an App to receive push notifications. +For more information see the apple developer documentation. + +Sample Configuration: + + notify: + platform: apns + name: doorbell_app + sandbox: true + cert_file: doorbell_app.pem + +Configuration Variables: + + name: The name of the app. + sandbox: If true notifications will be sent to the sandbox (test) notification service. + cert_file: The certificate to use to authenticate with the APNS service. + +Usage: + + The APNS platform will register two services, notify/[app_name] and apns/[app_name]. + + apns/app_name: + This service will register device id's with home assistant. In order to + receive a notification a device must be registered. The app on the + device can use this service to send its id during startup, the id will + be stored in the [app_name]_apns.yaml. + + See didRegisterForRemoteNotificationsWithDeviceToken in the apple developer + documentation for more information. + + + notify/app_name + This service will send messages to a registered device. The following parameters can be used: + + message: + The message to send + + target: + The desired state of the device, only devices that match the state will receive messages. + To enable state tracking a registered device must have a device_tracking_id added to the + [app_name]_apns.yaml file. If this id matches a device in known_devices.yaml its state + will be tracked. + + data: + badge: + The number to display as the badge of the app ic + sound: + The name of a sound file in the app bundle or in the Library/Sounds folder. + category: + Provide this key with a string value that represents the identifier + property of the UIMutableUserNotificationCategory + content_available: + Provide this key with a value of 1 to indicate that new content is available. +""" import logging import os +import voluptuous as vol from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( ATTR_TARGET, ATTR_DATA, BaseNotificationService) +import homeassistant.helpers.config_validation as cv DOMAIN = "apns" APNS_DEVICES = "apns.yaml" DEVICE_TRACKER_DOMAIN = "device_tracker" +SERVICE_REGISTER = "register" +ATTR_PUSH_ID = "push_id" +ATTR_NAME = "name" + +REGISTER_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_PUSH_ID): cv.template, + vol.Optional(ATTR_NAME, default=None): cv.string, +}) def get_service(hass, config): """Return push service.""" + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'apns_services.yaml')) + name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -28,7 +99,11 @@ def get_service(hass, config): sandbox = bool(config.get('sandbox', False)) service = ApnsNotificationService(hass, name, sandbox, cert_file) - hass.services.register(DOMAIN, name, service.register) + hass.services.register(DOMAIN, + name, + service.register, + descriptions.get(SERVICE_REGISTER), + schema=REGISTER_SERVICE_SCHEMA) return service @@ -148,11 +223,11 @@ def write_devices(self): def register(self, call): """Register a device to receive push messages.""" - push_id = call.data.get("push_id") + push_id = call.data.get(ATTR_PUSH_ID) if push_id is None: return False - device_name = call.data.get("name") + device_name = call.data.get(ATTR_NAME) current_device = self.devices.get(push_id) current_tracking_id = None if current_device is None \ else current_device.tracking_device_id diff --git a/homeassistant/components/notify/apns_services.yaml b/homeassistant/components/notify/apns_services.yaml new file mode 100644 index 00000000000000..41f1baf373f054 --- /dev/null +++ b/homeassistant/components/notify/apns_services.yaml @@ -0,0 +1,11 @@ +register: + description: Registers a device to recieve push notifications. + + fields: + push_id: + description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service. + example: '72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62' + + name: + description: A friendly name for the device (optional). + example: 'Sam''s iPhone' \ No newline at end of file From 2aff3c5b4b8cb93e72832a95d23a147657331bae Mon Sep 17 00:00:00 2001 From: samowens Date: Wed, 24 Aug 2016 23:26:31 +0100 Subject: [PATCH 16/33] added push notification implementation --- homeassistant/components/notify/apns.py | 87 ++----------------------- tests/components/notify/test_apns.py | 16 ++--- 2 files changed, 13 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 50728607272bb6..9da4c05a5ae74d 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,91 +1,19 @@ -""" -The APNS platform uses the Apple Push Notification service (APNS) to deliver -notifications from Home Assistant. - -To use the APNS service you will need an apple developer account -and you will need to create an App to receive push notifications. -For more information see the apple developer documentation. - -Sample Configuration: - - notify: - platform: apns - name: doorbell_app - sandbox: true - cert_file: doorbell_app.pem - -Configuration Variables: - - name: The name of the app. - sandbox: If true notifications will be sent to the sandbox (test) notification service. - cert_file: The certificate to use to authenticate with the APNS service. - -Usage: - - The APNS platform will register two services, notify/[app_name] and apns/[app_name]. - - apns/app_name: - This service will register device id's with home assistant. In order to - receive a notification a device must be registered. The app on the - device can use this service to send its id during startup, the id will - be stored in the [app_name]_apns.yaml. - - See didRegisterForRemoteNotificationsWithDeviceToken in the apple developer - documentation for more information. - - - notify/app_name - This service will send messages to a registered device. The following parameters can be used: - - message: - The message to send - - target: - The desired state of the device, only devices that match the state will receive messages. - To enable state tracking a registered device must have a device_tracking_id added to the - [app_name]_apns.yaml file. If this id matches a device in known_devices.yaml its state - will be tracked. - - data: - badge: - The number to display as the badge of the app ic - sound: - The name of a sound file in the app bundle or in the Library/Sounds folder. - category: - Provide this key with a string value that represents the identifier - property of the UIMutableUserNotificationCategory - content_available: - Provide this key with a value of 1 to indicate that new content is available. -""" +"""Support for Apple push notification service.""" import logging import os -import voluptuous as vol from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( ATTR_TARGET, ATTR_DATA, BaseNotificationService) -import homeassistant.helpers.config_validation as cv DOMAIN = "apns" APNS_DEVICES = "apns.yaml" DEVICE_TRACKER_DOMAIN = "device_tracker" -SERVICE_REGISTER = "register" -ATTR_PUSH_ID = "push_id" -ATTR_NAME = "name" - -REGISTER_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_PUSH_ID): cv.template, - vol.Optional(ATTR_NAME, default=None): cv.string, -}) def get_service(hass, config): - """Return push service.""" - - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'apns_services.yaml')) - + """Return push service""" name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -99,11 +27,7 @@ def get_service(hass, config): sandbox = bool(config.get('sandbox', False)) service = ApnsNotificationService(hass, name, sandbox, cert_file) - hass.services.register(DOMAIN, - name, - service.register, - descriptions.get(SERVICE_REGISTER), - schema=REGISTER_SERVICE_SCHEMA) + hass.services.register(DOMAIN, name, service.register) return service @@ -215,7 +139,6 @@ def write_device(out, device): def write_devices(self): """Write all known devices to file.""" - with open(self.yaml_path, 'w+') as out: for _, device in self.devices.items(): ApnsNotificationService.write_device(out, device) @@ -223,11 +146,11 @@ def write_devices(self): def register(self, call): """Register a device to receive push messages.""" - push_id = call.data.get(ATTR_PUSH_ID) + push_id = call.data.get("push_id") if push_id is None: return False - device_name = call.data.get(ATTR_NAME) + device_name = call.data.get("name") current_device = self.devices.get(push_id) current_tracking_id = None if current_device is None \ else current_device.tracking_device_id diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 037a63b9581b34..0fe77bdc23e024 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -67,10 +67,10 @@ def test_register_new_device(self): self.assertTrue(hass.services.call('apns', 'test_app', {'push_id': '1234', - 'name': 'test device'}, + 'name': 'test device'}, blocking=True)) - devices = {str(key): value for (key, value) in + devices = {str(key): value for (key,value) in load_yaml_config_file(devices_path).items()} test_device_1 = devices.get('1234') @@ -162,14 +162,14 @@ def test_update_existing_device_with_tracking_id(self): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') notify.setup(hass, config) self.assertTrue(hass.services.call('apns', 'test_app', - {'push_id': '1234', - 'name': 'updated device 1'}, + { 'push_id': '1234', + 'name': 'updated device 1'}, blocking=True)) devices = {str(key): value for (key, value) in @@ -240,8 +240,8 @@ def test_send_with_state(self, mock_send_notification): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') notify.setup(hass, config) From 4176709809ad4428e1fd98f848f683ce29f86555 Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 25 Aug 2016 11:07:51 +0100 Subject: [PATCH 17/33] some lint changes --- homeassistant/components/notify/apns.py | 4 +++- tests/components/notify/test_apns.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 9da4c05a5ae74d..4e756bca887ae2 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -13,7 +13,8 @@ def get_service(hass, config): - """Return push service""" + """Return push service.""" + name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -139,6 +140,7 @@ def write_device(out, device): def write_devices(self): """Write all known devices to file.""" + with open(self.yaml_path, 'w+') as out: for _, device in self.devices.items(): ApnsNotificationService.write_device(out, device) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 0fe77bdc23e024..037a63b9581b34 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -67,10 +67,10 @@ def test_register_new_device(self): self.assertTrue(hass.services.call('apns', 'test_app', {'push_id': '1234', - 'name': 'test device'}, + 'name': 'test device'}, blocking=True)) - devices = {str(key): value for (key,value) in + devices = {str(key): value for (key, value) in load_yaml_config_file(devices_path).items()} test_device_1 = devices.get('1234') @@ -162,14 +162,14 @@ def test_update_existing_device_with_tracking_id(self): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 notify.setup(hass, config) self.assertTrue(hass.services.call('apns', 'test_app', - { 'push_id': '1234', - 'name': 'updated device 1'}, + {'push_id': '1234', + 'name': 'updated device 1'}, blocking=True)) devices = {str(key): value for (key, value) in @@ -240,8 +240,8 @@ def test_send_with_state(self, mock_send_notification): devices_path = hass.config.path('test_app_apns.yaml') with open(devices_path, 'w+') as out: - out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') - out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') + out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 + out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 notify.setup(hass, config) From 7aeef2a7128bd9c62becb89a5a565b52d6238c09 Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 25 Aug 2016 22:56:42 +0100 Subject: [PATCH 18/33] added docs --- homeassistant/components/notify/apns.py | 83 +++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 4e756bca887ae2..50728607272bb6 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,20 +1,91 @@ -"""Support for Apple push notification service.""" +""" +The APNS platform uses the Apple Push Notification service (APNS) to deliver +notifications from Home Assistant. + +To use the APNS service you will need an apple developer account +and you will need to create an App to receive push notifications. +For more information see the apple developer documentation. + +Sample Configuration: + + notify: + platform: apns + name: doorbell_app + sandbox: true + cert_file: doorbell_app.pem + +Configuration Variables: + + name: The name of the app. + sandbox: If true notifications will be sent to the sandbox (test) notification service. + cert_file: The certificate to use to authenticate with the APNS service. + +Usage: + + The APNS platform will register two services, notify/[app_name] and apns/[app_name]. + + apns/app_name: + This service will register device id's with home assistant. In order to + receive a notification a device must be registered. The app on the + device can use this service to send its id during startup, the id will + be stored in the [app_name]_apns.yaml. + + See didRegisterForRemoteNotificationsWithDeviceToken in the apple developer + documentation for more information. + + + notify/app_name + This service will send messages to a registered device. The following parameters can be used: + + message: + The message to send + + target: + The desired state of the device, only devices that match the state will receive messages. + To enable state tracking a registered device must have a device_tracking_id added to the + [app_name]_apns.yaml file. If this id matches a device in known_devices.yaml its state + will be tracked. + + data: + badge: + The number to display as the badge of the app ic + sound: + The name of a sound file in the app bundle or in the Library/Sounds folder. + category: + Provide this key with a string value that represents the identifier + property of the UIMutableUserNotificationCategory + content_available: + Provide this key with a value of 1 to indicate that new content is available. +""" import logging import os +import voluptuous as vol from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( ATTR_TARGET, ATTR_DATA, BaseNotificationService) +import homeassistant.helpers.config_validation as cv DOMAIN = "apns" APNS_DEVICES = "apns.yaml" DEVICE_TRACKER_DOMAIN = "device_tracker" +SERVICE_REGISTER = "register" +ATTR_PUSH_ID = "push_id" +ATTR_NAME = "name" + +REGISTER_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_PUSH_ID): cv.template, + vol.Optional(ATTR_NAME, default=None): cv.string, +}) def get_service(hass, config): """Return push service.""" + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'apns_services.yaml')) + name = config.get("name") if name is None: logging.error("Name must be specified.") @@ -28,7 +99,11 @@ def get_service(hass, config): sandbox = bool(config.get('sandbox', False)) service = ApnsNotificationService(hass, name, sandbox, cert_file) - hass.services.register(DOMAIN, name, service.register) + hass.services.register(DOMAIN, + name, + service.register, + descriptions.get(SERVICE_REGISTER), + schema=REGISTER_SERVICE_SCHEMA) return service @@ -148,11 +223,11 @@ def write_devices(self): def register(self, call): """Register a device to receive push messages.""" - push_id = call.data.get("push_id") + push_id = call.data.get(ATTR_PUSH_ID) if push_id is None: return False - device_name = call.data.get("name") + device_name = call.data.get(ATTR_NAME) current_device = self.devices.get(push_id) current_tracking_id = None if current_device is None \ else current_device.tracking_device_id From ad7709214fab9f5c339c6be2ae0a7d6daa8a00c4 Mon Sep 17 00:00:00 2001 From: samowens Date: Fri, 26 Aug 2016 09:35:06 +0100 Subject: [PATCH 19/33] Fixed comment formatting issues --- homeassistant/components/notify/apns.py | 53 ++++++++++++++++--------- tests/components/notify/test_apns.py | 8 ++-- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 50728607272bb6..b38c09fa0918e6 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,4 +1,6 @@ """ +APNS Notificaion platform. + The APNS platform uses the Apple Push Notification service (APNS) to deliver notifications from Home Assistant. @@ -16,13 +18,18 @@ Configuration Variables: - name: The name of the app. - sandbox: If true notifications will be sent to the sandbox (test) notification service. - cert_file: The certificate to use to authenticate with the APNS service. + name: + The name of the app. + sandbox: + If true notifications will be sent to the sandbox (test) notification + service. + cert_file: + The certificate to use to authenticate with the APNS service. Usage: - The APNS platform will register two services, notify/[app_name] and apns/[app_name]. + The APNS platform will register two services, notify/[app_name] and + apns/[app_name]. apns/app_name: This service will register device id's with home assistant. In order to @@ -30,32 +37,36 @@ device can use this service to send its id during startup, the id will be stored in the [app_name]_apns.yaml. - See didRegisterForRemoteNotificationsWithDeviceToken in the apple developer - documentation for more information. + See didRegisterForRemoteNotificationsWithDeviceToken in the apple + developer documentation for more information. notify/app_name - This service will send messages to a registered device. The following parameters can be used: + This service will send messages to a registered device. The following + parameters can be used: message: The message to send target: - The desired state of the device, only devices that match the state will receive messages. - To enable state tracking a registered device must have a device_tracking_id added to the - [app_name]_apns.yaml file. If this id matches a device in known_devices.yaml its state - will be tracked. + The desired state of the device, only devices that match the state + will receive messages. To enable state tracking a registered + device must have a device_tracking_id added to the + [app_name]_apns.yaml file. If this id matches a device in + known_devices.yaml its state will be tracked. data: badge: The number to display as the badge of the app ic sound: - The name of a sound file in the app bundle or in the Library/Sounds folder. + The name of a sound file in the app bundle or in the + Library/Sounds folder. category: - Provide this key with a string value that represents the identifier - property of the UIMutableUserNotificationCategory + Provide this key with a string value that represents the + identifier property of the UIMutableUserNotificationCategory content_available: - Provide this key with a value of 1 to indicate that new content is available. + Provide this key with a value of 1 to indicate that new + content is available. """ import logging import os @@ -80,9 +91,9 @@ vol.Optional(ATTR_NAME, default=None): cv.string, }) + def get_service(hass, config): """Return push service.""" - descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'apns_services.yaml')) @@ -109,6 +120,8 @@ def get_service(hass, config): class ApnsDevice(object): """ + Apns Device class. + Stores information about a device that is registered for push notifications. """ @@ -132,6 +145,8 @@ def name(self): @property def tracking_device_id(self): """ + Device Id. + The id of a device that is tracked by the device tracking component. """ @@ -140,6 +155,8 @@ def tracking_device_id(self): @property def full_tracking_device_id(self): """ + Fully qualified device id. + The full id of a device that is tracked by the device tracking component. """ @@ -181,6 +198,8 @@ def __init__(self, hass, app_name, sandbox, cert_file): def state_changed_listener(entity_id, from_s, to_s): """ + Listener for sate change. + Track device state change if a device has a tracking id specified. """ @@ -215,14 +234,12 @@ def write_device(out, device): def write_devices(self): """Write all known devices to file.""" - with open(self.yaml_path, 'w+') as out: for _, device in self.devices.items(): ApnsNotificationService.write_device(out, device) def register(self, call): """Register a device to receive push messages.""" - push_id = call.data.get(ATTR_PUSH_ID) if push_id is None: return False diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 037a63b9581b34..9d53b1fe9d7290 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -84,7 +84,7 @@ def test_register_new_device(self): os.remove(devices_path) def test_register_device_without_name(self): - + """Test registering a without a name.""" config = { 'notify': { 'platform': 'apns', @@ -114,7 +114,7 @@ def test_register_device_without_name(self): os.remove(devices_path) def test_update_existing_device(self): - + """Test updating an existing device.""" config = { 'notify': { 'platform': 'apns', @@ -150,7 +150,7 @@ def test_update_existing_device(self): os.remove(devices_path) def test_update_existing_device_with_tracking_id(self): - + """Test updating an existing device that has a tracking id.""" config = { 'notify': { 'platform': 'apns', @@ -190,6 +190,7 @@ def test_update_existing_device_with_tracking_id(self): @patch('apns3.GatewayConnection.send_notification') def test_send(self, mock_send_notification): + """Test updating an existing device.""" config = { 'notify': { 'platform': 'apns', @@ -229,6 +230,7 @@ def test_send(self, mock_send_notification): @patch('apns3.GatewayConnection.send_notification') def test_send_with_state(self, mock_send_notification): + """Test updating an existing device.""" config = { 'notify': { 'platform': 'apns', From 35090e1025df6299a96150692174d3fdb0dad36e Mon Sep 17 00:00:00 2001 From: sam-io Date: Fri, 26 Aug 2016 22:13:46 +0100 Subject: [PATCH 20/33] Added requirments --- homeassistant/components/notify/apns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index b38c09fa0918e6..1c89813bc7ca23 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,5 +1,5 @@ """ -APNS Notificaion platform. +APNS Notification platform. The APNS platform uses the Apple Push Notification service (APNS) to deliver notifications from Home Assistant. @@ -91,6 +91,7 @@ vol.Optional(ATTR_NAME, default=None): cv.string, }) +REQUIREMENTS = ["apns3==1.0.0"] def get_service(hass, config): """Return push service.""" From 59fa36e497db71a98b214af7825243d9922f1cda Mon Sep 17 00:00:00 2001 From: sam-io Date: Fri, 26 Aug 2016 22:15:18 +0100 Subject: [PATCH 21/33] Update requirements_all.txt --- requirements_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index 640931ceb89cea..ac46f64aa3cf55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -61,6 +61,9 @@ cherrypy==8.1.0 # homeassistant.components.sensor.coinmarketcap coinmarketcap==2.0.1 +# homeassistant.components.notify.apns +apns3==1.0.0 + # homeassistant.scripts.check_config colorlog>2.1,<3 From ed29822a934ad0f287d40325a7a80ff518bb6580 Mon Sep 17 00:00:00 2001 From: sam-io Date: Fri, 26 Aug 2016 22:25:38 +0100 Subject: [PATCH 22/33] Update apns.py --- homeassistant/components/notify/apns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 1c89813bc7ca23..f551c66a6e676b 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -93,6 +93,7 @@ REQUIREMENTS = ["apns3==1.0.0"] + def get_service(hass, config): """Return push service.""" descriptions = load_yaml_config_file( From 8539613cb9e2fa82d7269f0e99d198e8b06993d8 Mon Sep 17 00:00:00 2001 From: samowens Date: Mon, 5 Sep 2016 08:50:45 +0100 Subject: [PATCH 23/33] re-generated requirments_all.txt --- requirements_all.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index ac46f64aa3cf55..275986eae7f83e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -34,6 +34,9 @@ Werkzeug==0.11.11 # homeassistant.components.apcupsd apcaccess==0.0.4 +# homeassistant.components.notify.apns +apns3==1.0.0 + # homeassistant.components.sun astral==1.2 @@ -61,9 +64,6 @@ cherrypy==8.1.0 # homeassistant.components.sensor.coinmarketcap coinmarketcap==2.0.1 -# homeassistant.components.notify.apns -apns3==1.0.0 - # homeassistant.scripts.check_config colorlog>2.1,<3 From 9ebaed2d2f9c7bad25d79f8e8959f77b43166b26 Mon Sep 17 00:00:00 2001 From: sam-io Date: Mon, 12 Sep 2016 09:23:45 +0100 Subject: [PATCH 24/33] Added link to online docs --- homeassistant/components/notify/apns.py | 68 +------------------------ 1 file changed, 2 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index f551c66a6e676b..817773ab3a9b99 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -1,72 +1,8 @@ """ APNS Notification platform. -The APNS platform uses the Apple Push Notification service (APNS) to deliver -notifications from Home Assistant. - -To use the APNS service you will need an apple developer account -and you will need to create an App to receive push notifications. -For more information see the apple developer documentation. - -Sample Configuration: - - notify: - platform: apns - name: doorbell_app - sandbox: true - cert_file: doorbell_app.pem - -Configuration Variables: - - name: - The name of the app. - sandbox: - If true notifications will be sent to the sandbox (test) notification - service. - cert_file: - The certificate to use to authenticate with the APNS service. - -Usage: - - The APNS platform will register two services, notify/[app_name] and - apns/[app_name]. - - apns/app_name: - This service will register device id's with home assistant. In order to - receive a notification a device must be registered. The app on the - device can use this service to send its id during startup, the id will - be stored in the [app_name]_apns.yaml. - - See didRegisterForRemoteNotificationsWithDeviceToken in the apple - developer documentation for more information. - - - notify/app_name - This service will send messages to a registered device. The following - parameters can be used: - - message: - The message to send - - target: - The desired state of the device, only devices that match the state - will receive messages. To enable state tracking a registered - device must have a device_tracking_id added to the - [app_name]_apns.yaml file. If this id matches a device in - known_devices.yaml its state will be tracked. - - data: - badge: - The number to display as the badge of the app ic - sound: - The name of a sound file in the app bundle or in the - Library/Sounds folder. - category: - Provide this key with a string value that represents the - identifier property of the UIMutableUserNotificationCategory - content_available: - Provide this key with a value of 1 to indicate that new - content is available. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.apns/ """ import logging import os From 7e55ffbf257f4208d18b8c7d9144a44d0e981519 Mon Sep 17 00:00:00 2001 From: samowens Date: Thu, 6 Oct 2016 23:43:05 +0100 Subject: [PATCH 25/33] changed to use http/2 library for push notifications --- homeassistant/components/notify/apns.py | 99 ++++++++++++++------ requirements_all.txt | 2 +- tests/components/notify/test_apns.py | 115 +++++++++++++++++++++--- 3 files changed, 177 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 817773ab3a9b99..d9c764f0f4fb3e 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -13,6 +13,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_DATA, BaseNotificationService) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import template as template_helper DOMAIN = "apns" APNS_DEVICES = "apns.yaml" @@ -23,11 +24,11 @@ ATTR_NAME = "name" REGISTER_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_PUSH_ID): cv.template, + vol.Required(ATTR_PUSH_ID): cv.string, vol.Optional(ATTR_NAME, default=None): cv.string, }) -REQUIREMENTS = ["apns3==1.0.0"] +REQUIREMENTS = ["apns2==0.1.1"] def get_service(hass, config): @@ -45,9 +46,14 @@ def get_service(hass, config): logging.error("Certificate must be specified.") return None + topic = config.get('topic') + if topic is None: + logging.error("Topic must be specified.") + return None + sandbox = bool(config.get('sandbox', False)) - service = ApnsNotificationService(hass, name, sandbox, cert_file) + service = ApnsNotificationService(hass, name, topic, sandbox, cert_file) hass.services.register(DOMAIN, name, service.register, @@ -64,11 +70,12 @@ class ApnsDevice(object): registered for push notifications. """ - def __init__(self, push_id, name, tracking_device_id=None): + def __init__(self, push_id, name, tracking_device_id=None, disabled=False): """Initialize Apns Device.""" self.device_push_id = push_id self.device_name = name self.tracking_id = tracking_device_id + self.device_disabled = disabled @property def push_id(self): @@ -100,6 +107,15 @@ def full_tracking_device_id(self): """ return DEVICE_TRACKER_DOMAIN + '.' + self.tracking_id + @property + def disabled(self): + """Should receive notifications.""" + return self.device_disabled + + def disable(self): + """Disable the device from recieving notifications.""" + self.device_disabled = True + def __eq__(self, other): """Return the comparision.""" if isinstance(other, self.__class__): @@ -110,11 +126,12 @@ def __ne__(self, other): """Return the comparision.""" return not self.__eq__(other) - class ApnsNotificationService(BaseNotificationService): - """Implement the notification service for the AWS SNS service.""" + """Implement the notification service for the APNS service.""" - def __init__(self, hass, app_name, sandbox, cert_file): + # pylint: disable=too-many-arguments + # pylint: disable=too-many-instance-attributes + def __init__(self, hass, app_name, topic, sandbox, cert_file): """Initialize APNS application.""" self.hass = hass self.app_name = app_name @@ -123,12 +140,14 @@ def __init__(self, hass, app_name, sandbox, cert_file): self.yaml_path = hass.config.path(app_name + '_' + APNS_DEVICES) self.devices = {} self.device_states = {} + self.topic = topic if os.path.isfile(self.yaml_path): self.devices = { str(key): ApnsDevice( str(key), value.get('name'), - value.get('tracking_device_id') + value.get('tracking_device_id'), + value.get('disabled', False) ) for (key, value) in load_yaml_config_file(self.yaml_path).items() @@ -161,6 +180,8 @@ def write_device(out, device): if device.tracking_device_id is not None: attributes.append( 'tracking_device_id: {}'.format(device.tracking_device_id)) + if device.disabled: + attributes.append('disabled: True') out.write(device.push_id) out.write(": {") @@ -196,7 +217,7 @@ def register(self, call): self.devices[push_id] = device with open(self.yaml_path, 'a') as out: self.write_device(out, device) - return + return True if device != current_device: self.devices[push_id] = device @@ -204,14 +225,16 @@ def register(self, call): return True - def send_message(self, message="", **kwargs): + def send_message(self, message=None, **kwargs): """Send push message to registered devices.""" - from apns3 import APNs, Payload + from apns2.client import APNsClient + from apns2.payload import Payload + from apns2.errors import Unregistered - apns = APNs( + apns = APNsClient( + self.certificate, use_sandbox=self.sandbox, - cert_file=self.certificate, - key_file=self.certificate) + use_alternative_port=False) device_state = kwargs.get(ATTR_TARGET) message_data = kwargs.get(ATTR_DATA) @@ -219,20 +242,44 @@ def send_message(self, message="", **kwargs): if message_data is None: message_data = {} + if isinstance(message, str): + rendered_message = message + elif isinstance(message, template_helper.Template): + rendered_message = message.render() + else: + rendered_message = "" + payload = Payload( - message, - message_data.get("badge"), - message_data.get("sound"), - message_data.get("category"), - message_data.get("custom", {}), - message_data.get("content_available", False)) + alert=rendered_message, + badge=message_data.get("badge"), + sound=message_data.get("sound"), + category=message_data.get("category"), + custom=message_data.get("custom", {}), + content_available=message_data.get("content_available", False)) + + device_update = False for push_id, device in self.devices.items(): - if device_state is None: - apns.gateway_server.send_notification(push_id, payload) - elif device.tracking_device_id is not None: - state = self.device_states.get(device.full_tracking_device_id) - if state == str(device_state): - apns.gateway_server.send_notification(push_id, payload) + if not device.disabled: + state = None + if device.tracking_device_id is not None: + state = self.device_states.get( + device.full_tracking_device_id) + + if device_state is None or state == str(device_state): + try: + apns.send_notification( + push_id, + payload, + topic=self.topic) + except Unregistered: + logging.error( + "Device %s has unregistered.", + push_id) + device_update = True + device.disable() + + if device_update: + self.write_devices() return True diff --git a/requirements_all.txt b/requirements_all.txt index 275986eae7f83e..c58999c213fae5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Werkzeug==0.11.11 apcaccess==0.0.4 # homeassistant.components.notify.apns -apns3==1.0.0 +apns2==0.1.1 # homeassistant.components.sun astral==1.2 diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 9d53b1fe9d7290..02e5e799eed4bd 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -6,6 +6,7 @@ from tests.common import get_test_home_assistant from homeassistant.config import load_yaml_config_file from unittest.mock import patch +from apns2.errors import Unregistered class TestApns(unittest.TestCase): @@ -18,6 +19,7 @@ def test_apns_setup_full(self): 'platform': 'apns', 'name': 'test_app', 'sandbox': 'True', + 'topic': 'testapp.appname', 'cert_file': 'test_app.pem' } } @@ -31,6 +33,7 @@ def test_apns_setup_missing_name(self): 'notify': { 'platform': 'apns', 'sandbox': 'True', + 'topic': 'testapp.appname', 'cert_file': 'test_app.pem' } } @@ -42,6 +45,19 @@ def test_apns_setup_missing_certificate(self): config = { 'notify': { 'platform': 'apns', + 'topic': 'testapp.appname', + 'name': 'test_app' + } + } + hass = get_test_home_assistant() + self.assertFalse(notify.setup(hass, config)) + + def test_apns_setup_missing_topic(self): + """Test setup with missing topic.""" + config = { + 'notify': { + 'platform': 'apns', + 'cert_file': 'test_app.pem', 'name': 'test_app' } } @@ -54,6 +70,7 @@ def test_register_new_device(self): 'notify': { 'platform': 'apns', 'name': 'test_app', + 'topic': 'testapp.appname', 'cert_file': 'test_app.pem' } } @@ -89,6 +106,7 @@ def test_register_device_without_name(self): 'notify': { 'platform': 'apns', 'name': 'test_app', + 'topic': 'testapp.appname', 'cert_file': 'test_app.pem' } } @@ -119,6 +137,7 @@ def test_update_existing_device(self): 'notify': { 'platform': 'apns', 'name': 'test_app', + 'topic': 'testapp.appname', 'cert_file': 'test_app.pem' } } @@ -155,6 +174,7 @@ def test_update_existing_device_with_tracking_id(self): 'notify': { 'platform': 'apns', 'name': 'test_app', + 'topic': 'testapp.appname', 'cert_file': 'test_app.pem' } } @@ -188,13 +208,15 @@ def test_update_existing_device_with_tracking_id(self): os.remove(devices_path) - @patch('apns3.GatewayConnection.send_notification') - def test_send(self, mock_send_notification): + @patch('apns2.client.APNsClient') + def test_send(self, mock_client): """Test updating an existing device.""" + send = mock_client.return_value.send_notification config = { 'notify': { 'platform': 'apns', 'name': 'test_app', + 'topic': 'testapp.appname', 'cert_file': 'test_app.pem' } } @@ -216,11 +238,11 @@ def test_send(self, mock_send_notification): }, blocking=True)) - self.assertTrue(mock_send_notification.called) - self.assertEqual(1, len(mock_send_notification.mock_calls)) + self.assertTrue(send.called) + self.assertEqual(1, len(send.mock_calls)) - target = mock_send_notification.mock_calls[0][1][0] - payload = mock_send_notification.mock_calls[0][1][1] + target = send.mock_calls[0][1][0] + payload = send.mock_calls[0][1][1] self.assertEqual('1234', target) self.assertEqual('Hello', payload.alert) @@ -228,13 +250,47 @@ def test_send(self, mock_send_notification): self.assertEqual('test.mp3', payload.sound) self.assertEqual('testing', payload.category) - @patch('apns3.GatewayConnection.send_notification') - def test_send_with_state(self, mock_send_notification): + @patch('apns2.client.APNsClient') + def test_send_when_disabled(self, mock_client): """Test updating an existing device.""" + send = mock_client.return_value.send_notification config = { 'notify': { 'platform': 'apns', 'name': 'test_app', + 'topic': 'testapp.appname', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1, disabled: True}\n') + + notify.setup(hass, config) + + self.assertTrue(hass.services.call('notify', 'test_app', + {'message': 'Hello', + 'data': { + 'badge': 1, + 'sound': 'test.mp3', + 'category': 'testing' + } + }, + blocking=True)) + + self.assertFalse(send.called) + + @patch('apns2.client.APNsClient') + def test_send_with_state(self, mock_client): + """Test updating an existing device.""" + send = mock_client.return_value.send_notification + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'topic': 'testapp.appname', 'cert_file': 'test_app.pem' } } @@ -258,11 +314,46 @@ def test_send_with_state(self, mock_send_notification): 'target': 'home'}, blocking=True)) - self.assertTrue(mock_send_notification.called) - self.assertEqual(1, len(mock_send_notification.mock_calls)) + self.assertTrue(send.called) + self.assertEqual(1, len(send.mock_calls)) - target = mock_send_notification.mock_calls[0][1][0] - payload = mock_send_notification.mock_calls[0][1][1] + target = send.mock_calls[0][1][0] + payload = send.mock_calls[0][1][1] self.assertEqual('5678', target) self.assertEqual('Hello', payload.alert) + + @patch('apns2.client.APNsClient') + def test_disable_when_unregistered(self, mock_client): + """Test disabling a device when it is unregistered.""" + send = mock_client.return_value.send_notification + send.side_effect = Unregistered() + + config = { + 'notify': { + 'platform': 'apns', + 'name': 'test_app', + 'topic': 'testapp.appname', + 'cert_file': 'test_app.pem' + } + } + hass = get_test_home_assistant() + + devices_path = hass.config.path('test_app_apns.yaml') + with open(devices_path, 'w+') as out: + out.write('1234: {name: test device 1}\n') + + notify.setup(hass, config) + + self.assertTrue(hass.services.call('notify', 'test_app', + {'message': 'Hello'}, + blocking=True)) + + devices = {str(key): value for (key, value) in + load_yaml_config_file(devices_path).items()} + + test_device_1 = devices.get('1234') + self.assertIsNotNone(test_device_1) + self.assertEqual(True, test_device_1.get('disabled')) + + os.remove(devices_path) From df34179c508bf527239b3a61684c1f5c2e2ad5e5 Mon Sep 17 00:00:00 2001 From: samowens Date: Fri, 7 Oct 2016 08:38:40 +0100 Subject: [PATCH 26/33] fixed lint issue --- homeassistant/components/notify/apns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index d9c764f0f4fb3e..8b74d6464f0556 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -126,6 +126,7 @@ def __ne__(self, other): """Return the comparision.""" return not self.__eq__(other) + class ApnsNotificationService(BaseNotificationService): """Implement the notification service for the APNS service.""" From aab52d42757cbd2b8e4a1f3c86ad33dd20e963cc Mon Sep 17 00:00:00 2001 From: samowens Date: Fri, 7 Oct 2016 09:24:42 +0100 Subject: [PATCH 27/33] fixed test that fails on CI --- tests/components/notify/test_apns.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 02e5e799eed4bd..33ebf19421b058 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -5,9 +5,10 @@ import homeassistant.components.notify as notify from tests.common import get_test_home_assistant from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.event import track_state_change from unittest.mock import patch from apns2.errors import Unregistered - +from threading import Event class TestApns(unittest.TestCase): """Test the APNS component.""" @@ -303,11 +304,21 @@ def test_send_with_state(self, mock_client): notify.setup(hass, config) + states_received = Event() + + def state_changed_listener(entity_id, from_s, to_s): + states_received.set() + + track_state_change( + hass, + ["device_tracker.tracking456"], + state_changed_listener) + hass.states.set('device_tracker.tracking456', 'home', force_update=True) - hass.pool.block_till_done() + states_received.wait(5) self.assertTrue(hass.services.call('notify', 'test_app', {'message': 'Hello', From 41dafdcf66281463adf4527e91efb7579bccca09 Mon Sep 17 00:00:00 2001 From: samowens Date: Fri, 7 Oct 2016 09:52:18 +0100 Subject: [PATCH 28/33] another go at fixing test that fails on CI --- tests/components/notify/test_apns.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 33ebf19421b058..8e436fc4de4167 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -10,6 +10,7 @@ from apns2.errors import Unregistered from threading import Event + class TestApns(unittest.TestCase): """Test the APNS component.""" @@ -319,6 +320,7 @@ def state_changed_listener(entity_id, from_s, to_s): force_update=True) states_received.wait(5) + hass.pool.block_till_done() self.assertTrue(hass.services.call('notify', 'test_app', {'message': 'Hello', From 8d87b04b81daaed42f4e6c00160467a65bd9a084 Mon Sep 17 00:00:00 2001 From: samowens Date: Fri, 7 Oct 2016 22:58:17 +0100 Subject: [PATCH 29/33] another go at fixing test that fails on CI --- tests/components/notify/test_apns.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 8e436fc4de4167..067bf3f74659d5 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -305,22 +305,11 @@ def test_send_with_state(self, mock_client): notify.setup(hass, config) - states_received = Event() - - def state_changed_listener(entity_id, from_s, to_s): - states_received.set() - - track_state_change( - hass, - ["device_tracker.tracking456"], - state_changed_listener) - hass.states.set('device_tracker.tracking456', 'home', force_update=True) - states_received.wait(5) - hass.pool.block_till_done() + hass.block_till_done() self.assertTrue(hass.services.call('notify', 'test_app', {'message': 'Hello', From f89e338b2c5b3c5f247b59f085836dd93a5929c5 Mon Sep 17 00:00:00 2001 From: samowens Date: Fri, 7 Oct 2016 23:40:41 +0100 Subject: [PATCH 30/33] another go at fixing test that fails on CI --- homeassistant/components/notify/apns.py | 25 ++++++++++--------- tests/components/notify/test_apns.py | 33 +++++++++++-------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 8b74d6464f0556..252f1e4e62e991 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -154,22 +154,25 @@ def __init__(self, hass, app_name, topic, sandbox, cert_file): load_yaml_config_file(self.yaml_path).items() } - def state_changed_listener(entity_id, from_s, to_s): - """ - Listener for sate change. - - Track device state change if a device - has a tracking id specified. - """ - self.device_states[entity_id] = str(to_s.state) - return - tracking_ids = [ device.full_tracking_device_id for (key, device) in self.devices.items() if device.tracking_device_id is not None ] - track_state_change(hass, tracking_ids, state_changed_listener) + track_state_change( + hass, + tracking_ids, + self.device_state_changed_listener) + + def device_state_changed_listener(self, entity_id, from_s, to_s): + """ + Listener for sate change. + + Track device state change if a device + has a tracking id specified. + """ + self.device_states[entity_id] = str(to_s.state) + return @staticmethod def write_device(out, device): diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 067bf3f74659d5..50cb4d03cc00fd 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -3,12 +3,12 @@ import os import homeassistant.components.notify as notify +from homeassistant.core import State +from homeassistant.components.notify.apns import ApnsNotificationService from tests.common import get_test_home_assistant from homeassistant.config import load_yaml_config_file -from homeassistant.helpers.event import track_state_change from unittest.mock import patch from apns2.errors import Unregistered -from threading import Event class TestApns(unittest.TestCase): @@ -288,14 +288,7 @@ def test_send_when_disabled(self, mock_client): def test_send_with_state(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification - config = { - 'notify': { - 'platform': 'apns', - 'name': 'test_app', - 'topic': 'testapp.appname', - 'cert_file': 'test_app.pem' - } - } + hass = get_test_home_assistant() devices_path = hass.config.path('test_app_apns.yaml') @@ -303,18 +296,22 @@ def test_send_with_state(self, mock_client): out.write('1234: {name: test device 1, tracking_device_id: tracking123}\n') # nopep8 out.write('5678: {name: test device 2, tracking_device_id: tracking456}\n') # nopep8 - notify.setup(hass, config) + notify_service = ApnsNotificationService( + hass, + 'test_app', + 'testapp.appname', + False, + 'test_app.pem' + ) - hass.states.set('device_tracker.tracking456', - 'home', - force_update=True) + notify_service.device_state_changed_listener( + 'device_tracker.tracking456', + State('device_tracker.tracking456', None), + State('device_tracker.tracking456', 'home')) hass.block_till_done() - self.assertTrue(hass.services.call('notify', 'test_app', - {'message': 'Hello', - 'target': 'home'}, - blocking=True)) + notify_service.send_message(message='Hello', target='home') self.assertTrue(send.called) self.assertEqual(1, len(send.mock_calls)) From 2692154d72b6bf672c54757d2aa81d1189b7e80a Mon Sep 17 00:00:00 2001 From: samowens Date: Sat, 8 Oct 2016 08:29:51 +0100 Subject: [PATCH 31/33] added missing docstring --- tests/components/notify/test_apns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 50cb4d03cc00fd..7103b6cdc8b24b 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -67,7 +67,7 @@ def test_apns_setup_missing_topic(self): self.assertFalse(notify.setup(hass, config)) def test_register_new_device(self): - + """Test registering a new device with a name.""" config = { 'notify': { 'platform': 'apns', From f10744fb6bf524761559539ebd997a7a764fe4fd Mon Sep 17 00:00:00 2001 From: samowens Date: Sun, 16 Oct 2016 22:50:26 +0100 Subject: [PATCH 32/33] moved service description to main services.yaml file --- homeassistant/components/notify/apns.py | 2 +- homeassistant/components/notify/apns_services.yaml | 11 ----------- homeassistant/components/notify/services.yaml | 12 ++++++++++++ 3 files changed, 13 insertions(+), 12 deletions(-) delete mode 100644 homeassistant/components/notify/apns_services.yaml diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 252f1e4e62e991..868d3a4ac8ad7c 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -34,7 +34,7 @@ def get_service(hass, config): """Return push service.""" descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'apns_services.yaml')) + os.path.join(os.path.dirname(__file__), 'services.yaml')) name = config.get("name") if name is None: diff --git a/homeassistant/components/notify/apns_services.yaml b/homeassistant/components/notify/apns_services.yaml deleted file mode 100644 index 41f1baf373f054..00000000000000 --- a/homeassistant/components/notify/apns_services.yaml +++ /dev/null @@ -1,11 +0,0 @@ -register: - description: Registers a device to recieve push notifications. - - fields: - push_id: - description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service. - example: '72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62' - - name: - description: A friendly name for the device (optional). - example: 'Sam''s iPhone' \ No newline at end of file diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 419a796199afe7..5d40bf7348e409 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -17,3 +17,15 @@ notify: data: description: Extended information for notification. Optional depending on the platform example: platform specific + +register: + description: Registers a device to receive push notifications. + + fields: + push_id: + description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service. + example: '72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62' + + name: + description: A friendly name for the device (optional). + example: 'Sam''s iPhone' From c2a671ceb3a73961d326c4f212b7d983bd25305e Mon Sep 17 00:00:00 2001 From: samowens Date: Mon, 17 Oct 2016 21:34:28 +0100 Subject: [PATCH 33/33] renamed apns service --- homeassistant/components/notify/apns.py | 2 +- homeassistant/components/notify/services.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 868d3a4ac8ad7c..5e5a8088aa7376 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -18,7 +18,7 @@ DOMAIN = "apns" APNS_DEVICES = "apns.yaml" DEVICE_TRACKER_DOMAIN = "device_tracker" -SERVICE_REGISTER = "register" +SERVICE_REGISTER = "apns_register" ATTR_PUSH_ID = "push_id" ATTR_NAME = "name" diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 5d40bf7348e409..c6a8df8c6044a2 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -18,7 +18,7 @@ notify: description: Extended information for notification. Optional depending on the platform example: platform specific -register: +apns_register: description: Registers a device to receive push notifications. fields: