Skip to content
Merged
21 changes: 18 additions & 3 deletions homeassistant/components/mobile_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
from homeassistant import config_entries
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.components.webhook import async_register as webhook_register
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, discovery
from homeassistant.helpers.typing import ConfigType, HomeAssistantType

from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_OS_VERSION, DATA_BINARY_SENSOR,
from .const import (ATTR_APP_DATA, ATTR_DEVICE_ID, ATTR_DEVICE_NAME,
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_PUSH_TOKEN,
ATTR_PUSH_URL, ATTR_OS_VERSION, DATA_BINARY_SENSOR,
DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES,
DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY,
STORAGE_VERSION)
Expand Down Expand Up @@ -52,6 +53,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
except ValueError:
pass

discovery.load_platform(hass, 'notify', DOMAIN, {}, config)
Comment thread
robbiet480 marked this conversation as resolved.
Outdated

return True


Expand Down Expand Up @@ -115,3 +118,15 @@ async def async_step_registration(self, user_input=None):
"""Handle a flow initialized during registration."""
return self.async_create_entry(title=user_input[ATTR_DEVICE_NAME],
data=user_input)


def push_registrations(hass):
"""Return a dictionary of push enabled registrations."""
targets = {}
for webhook_id, entry in hass.data[DOMAIN][DATA_CONFIG_ENTRIES].items():
data = entry.data
app_data = data[ATTR_APP_DATA]
if ATTR_PUSH_TOKEN in app_data and ATTR_PUSH_URL in app_data:
device_name = data[ATTR_DEVICE_NAME]
targets[device_name] = webhook_id
Comment thread
robbiet480 marked this conversation as resolved.
Outdated
return targets
2 changes: 2 additions & 0 deletions homeassistant/components/mobile_app/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
ATTR_MODEL = 'model'
ATTR_OS_NAME = 'os_name'
ATTR_OS_VERSION = 'os_version'
ATTR_PUSH_TOKEN = 'push_token'
ATTR_PUSH_URL = 'push_url'
ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption'

ATTR_EVENT_DATA = 'event_data'
Expand Down
117 changes: 117 additions & 0 deletions homeassistant/components/mobile_app/notify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Support for mobile_app push notifications."""
from datetime import datetime, timezone
import logging

import requests

from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.components.notify import (
ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT,
BaseNotificationService)
from homeassistant.components.mobile_app import push_registrations
from homeassistant.components.mobile_app.const import (
ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, ATTR_APP_VERSION,
ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_NAME,
ATTR_OS_VERSION, ATTR_PUSH_TOKEN, ATTR_PUSH_URL, DATA_CONFIG_ENTRIES,
DOMAIN)
import homeassistant.util.dt as dt_util

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ["mobile_app"]


# pylint: disable=invalid-name
def log_rate_limits(hass, device_name, resp, level=20):
Comment thread
robbiet480 marked this conversation as resolved.
Outdated
"""Output rate limit log line at given level."""
rate_limits = resp["rateLimits"]
resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"])
resetsAtTime = resetsAt - datetime.now(timezone.utc)
rate_limit_msg = ("mobile_app push notification rate limits for %s: "
"%d sent, %d allowed, %d errors, "
"resets in %s")
_LOGGER.log(level, rate_limit_msg,
device_name,
rate_limits["successful"],
rate_limits["maximum"], rate_limits["errors"],
str(resetsAtTime).split(".")[0])


def get_service(hass, config, discovery_info=None):
"""Get the mobile_app notification service."""
return MobileAppNotificationService()


class MobileAppNotificationService(BaseNotificationService):
"""Implement the notification service for mobile_app."""

def __init__(self):
"""Initialize the service."""

@property
def targets(self):
"""Return a dictionary of registered targets."""
return push_registrations(self.hass)

def send_message(self, message="", **kwargs):
Comment thread
robbiet480 marked this conversation as resolved.
Outdated
"""Send a message to the Lambda APNS gateway."""
data = {ATTR_MESSAGE: message}

if kwargs.get(ATTR_TITLE) is not None:
# Remove default title from notifications.
if kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT:
data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)

targets = kwargs.get(ATTR_TARGET)

if not targets:
targets = push_registrations(self.hass)

if kwargs.get(ATTR_DATA) is not None:
data[ATTR_DATA] = kwargs.get(ATTR_DATA)

for target in targets:

entry = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target]
entry_data = entry.data

app_data = entry_data[ATTR_APP_DATA]
push_token = app_data[ATTR_PUSH_TOKEN]
push_url = app_data[ATTR_PUSH_URL]

data[ATTR_PUSH_TOKEN] = push_token

reg_info = {
ATTR_APP_DATA: app_data,
ATTR_APP_ID: entry_data[ATTR_APP_ID],
ATTR_APP_NAME: entry_data[ATTR_APP_NAME],
ATTR_APP_VERSION: entry_data[ATTR_APP_VERSION],
ATTR_DEVICE_NAME: entry_data[ATTR_DEVICE_NAME],
ATTR_MANUFACTURER: entry_data[ATTR_MANUFACTURER],
ATTR_MODEL: entry_data[ATTR_MODEL],
ATTR_OS_NAME: entry_data[ATTR_OS_NAME],
CONF_WEBHOOK_ID: entry_data[CONF_WEBHOOK_ID],
}
if ATTR_OS_VERSION in entry_data:
reg_info[ATTR_OS_VERSION] = entry_data[ATTR_OS_VERSION]

data['registration_info'] = reg_info
Comment thread
robbiet480 marked this conversation as resolved.

req = requests.post(push_url, json=data, timeout=10)
Comment thread
robbiet480 marked this conversation as resolved.
Outdated

if req.status_code != 201:
Comment thread
robbiet480 marked this conversation as resolved.
Outdated
fallback_error = req.json().get("errorMessage",
Comment thread
robbiet480 marked this conversation as resolved.
Outdated
"Unknown error")
fallback_message = ("Internal server error, "
"please try again later: "
"{}").format(fallback_error)
message = req.json().get("message", fallback_message)
if req.status_code == 429:
_LOGGER.warning(message)
Comment thread
robbiet480 marked this conversation as resolved.
log_rate_limits(self.hass, entry_data[ATTR_DEVICE_NAME],
req.json(), 30)
else:
_LOGGER.error(message)
else:
log_rate_limits(self.hass, entry_data[ATTR_DEVICE_NAME],
req.json())