""" APNS Notification platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.apns/ """ 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 from homeassistant.helpers import template as template_helper DOMAIN = "apns" APNS_DEVICES = "apns.yaml" DEVICE_TRACKER_DOMAIN = "device_tracker" SERVICE_REGISTER = "apns_register" ATTR_PUSH_ID = "push_id" ATTR_NAME = "name" REGISTER_SERVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_PUSH_ID): cv.string, vol.Optional(ATTR_NAME, default=None): cv.string, }) REQUIREMENTS = ["apns2==0.1.1"] def get_service(hass, config): """Return push service.""" descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) 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 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, topic, sandbox, cert_file) hass.services.register(DOMAIN, name, service.register, descriptions.get(SERVICE_REGISTER), schema=REGISTER_SERVICE_SCHEMA) return service class ApnsDevice(object): """ Apns Device class. Stores information about a device that is registered for push notifications. """ 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): """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): """ Device Id. The id of a device that is tracked by the device tracking component. """ return self.tracking_id @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. """ 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__): 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 APNS service.""" def __init__(self, hass, app_name, topic, 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 = {} 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('disabled', False) ) for (key, value) in load_yaml_config_file(self.yaml_path).items() } 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, 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): """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)) if device.disabled: attributes.append('disabled: True') 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(ATTR_PUSH_ID) if push_id is None: return False 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 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 True if device != current_device: self.devices[push_id] = device self.write_devices() return True def send_message(self, message=None, **kwargs): """Send push message to registered devices.""" from apns2.client import APNsClient from apns2.payload import Payload from apns2.errors import Unregistered apns = APNsClient( self.certificate, use_sandbox=self.sandbox, use_alternative_port=False) device_state = kwargs.get(ATTR_TARGET) message_data = kwargs.get(ATTR_DATA) 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( 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 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