324 lines
10 KiB
Python
324 lines
10 KiB
Python
|
"""
|
||
|
Native Home Assistant iOS app component.
|
||
|
|
||
|
For more details about this platform, please refer to the documentation at
|
||
|
https://home-assistant.io/components/ios/
|
||
|
"""
|
||
|
import os
|
||
|
import json
|
||
|
import logging
|
||
|
|
||
|
import voluptuous as vol
|
||
|
from voluptuous.humanize import humanize_error
|
||
|
|
||
|
from homeassistant.helpers import config_validation as cv
|
||
|
|
||
|
import homeassistant.loader as loader
|
||
|
|
||
|
from homeassistant.helpers import discovery
|
||
|
|
||
|
from homeassistant.components.http import HomeAssistantView
|
||
|
|
||
|
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
|
||
|
HTTP_BAD_REQUEST)
|
||
|
|
||
|
from homeassistant.components.notify import DOMAIN as NotifyDomain
|
||
|
|
||
|
_LOGGER = logging.getLogger(__name__)
|
||
|
|
||
|
DOMAIN = "ios"
|
||
|
|
||
|
DEPENDENCIES = ["http"]
|
||
|
|
||
|
CONF_PUSH = "push"
|
||
|
CONF_PUSH_CATEGORIES = "categories"
|
||
|
CONF_PUSH_CATEGORIES_NAME = "name"
|
||
|
CONF_PUSH_CATEGORIES_IDENTIFIER = "identifier"
|
||
|
CONF_PUSH_CATEGORIES_ACTIONS = "actions"
|
||
|
|
||
|
CONF_PUSH_ACTIONS_IDENTIFIER = "identifier"
|
||
|
CONF_PUSH_ACTIONS_TITLE = "title"
|
||
|
CONF_PUSH_ACTIONS_ACTIVATION_MODE = "activationMode"
|
||
|
CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED = "authenticationRequired"
|
||
|
CONF_PUSH_ACTIONS_DESTRUCTIVE = "destructive"
|
||
|
CONF_PUSH_ACTIONS_BEHAVIOR = "behavior"
|
||
|
CONF_PUSH_ACTIONS_CONTEXT = "context"
|
||
|
CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE = "textInputButtonTitle"
|
||
|
CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER = "textInputPlaceholder"
|
||
|
|
||
|
ATTR_FOREGROUND = "foreground"
|
||
|
ATTR_BACKGROUND = "background"
|
||
|
|
||
|
ACTIVATION_MODES = [ATTR_FOREGROUND, ATTR_BACKGROUND]
|
||
|
|
||
|
ATTR_DEFAULT_BEHAVIOR = "default"
|
||
|
ATTR_TEXT_INPUT_BEHAVIOR = "textInput"
|
||
|
|
||
|
BEHAVIORS = [ATTR_DEFAULT_BEHAVIOR, ATTR_TEXT_INPUT_BEHAVIOR]
|
||
|
|
||
|
ATTR_DEVICE = "device"
|
||
|
ATTR_PUSH_TOKEN = "pushToken"
|
||
|
ATTR_APP = "app"
|
||
|
ATTR_PERMISSIONS = "permissions"
|
||
|
ATTR_PUSH_ID = "pushId"
|
||
|
ATTR_DEVICE_ID = "deviceId"
|
||
|
ATTR_PUSH_SOUNDS = "pushSounds"
|
||
|
ATTR_BATTERY = "battery"
|
||
|
|
||
|
ATTR_DEVICE_NAME = "name"
|
||
|
ATTR_DEVICE_LOCALIZED_MODEL = "localizedModel"
|
||
|
ATTR_DEVICE_MODEL = "model"
|
||
|
ATTR_DEVICE_PERMANENT_ID = "permanentID"
|
||
|
ATTR_DEVICE_SYSTEM_VERSION = "systemVersion"
|
||
|
ATTR_DEVICE_TYPE = "type"
|
||
|
ATTR_DEVICE_SYSTEM_NAME = "systemName"
|
||
|
|
||
|
ATTR_APP_BUNDLE_IDENTIFER = "bundleIdentifer"
|
||
|
ATTR_APP_BUILD_NUMBER = "buildNumber"
|
||
|
ATTR_APP_VERSION_NUMBER = "versionNumber"
|
||
|
|
||
|
ATTR_LOCATION_PERMISSION = "location"
|
||
|
ATTR_NOTIFICATIONS_PERMISSION = "notifications"
|
||
|
|
||
|
PERMISSIONS = [ATTR_LOCATION_PERMISSION, ATTR_NOTIFICATIONS_PERMISSION]
|
||
|
|
||
|
ATTR_BATTERY_STATE = "state"
|
||
|
ATTR_BATTERY_LEVEL = "level"
|
||
|
|
||
|
ATTR_BATTERY_STATE_UNPLUGGED = "Unplugged"
|
||
|
ATTR_BATTERY_STATE_CHARGING = "Charging"
|
||
|
ATTR_BATTERY_STATE_FULL = "Full"
|
||
|
ATTR_BATTERY_STATE_UNKNOWN = "Unknown"
|
||
|
|
||
|
BATTERY_STATES = [ATTR_BATTERY_STATE_UNPLUGGED, ATTR_BATTERY_STATE_CHARGING,
|
||
|
ATTR_BATTERY_STATE_FULL, ATTR_BATTERY_STATE_UNKNOWN]
|
||
|
|
||
|
ATTR_DEVICES = "devices"
|
||
|
|
||
|
ACTION_SCHEMA = vol.Schema({
|
||
|
vol.Required(CONF_PUSH_ACTIONS_IDENTIFIER): vol.Upper,
|
||
|
vol.Required(CONF_PUSH_ACTIONS_TITLE): cv.string,
|
||
|
vol.Optional(CONF_PUSH_ACTIONS_ACTIVATION_MODE,
|
||
|
default=ATTR_BACKGROUND): vol.In(ACTIVATION_MODES),
|
||
|
vol.Optional(CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED,
|
||
|
default=False): cv.boolean,
|
||
|
vol.Optional(CONF_PUSH_ACTIONS_DESTRUCTIVE,
|
||
|
default=False): cv.boolean,
|
||
|
vol.Optional(CONF_PUSH_ACTIONS_BEHAVIOR,
|
||
|
default=ATTR_DEFAULT_BEHAVIOR): vol.In(BEHAVIORS),
|
||
|
vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE): cv.string,
|
||
|
vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER): cv.string,
|
||
|
}, extra=vol.ALLOW_EXTRA)
|
||
|
|
||
|
ACTION_SCHEMA_LIST = vol.All(cv.ensure_list, [ACTION_SCHEMA])
|
||
|
|
||
|
CONFIG_SCHEMA = vol.Schema({
|
||
|
DOMAIN: {
|
||
|
CONF_PUSH: {
|
||
|
CONF_PUSH_CATEGORIES: vol.All(cv.ensure_list, [{
|
||
|
vol.Required(CONF_PUSH_CATEGORIES_NAME): cv.string,
|
||
|
vol.Required(CONF_PUSH_CATEGORIES_IDENTIFIER): vol.Upper,
|
||
|
vol.Required(CONF_PUSH_CATEGORIES_ACTIONS): ACTION_SCHEMA_LIST
|
||
|
}])
|
||
|
}
|
||
|
}
|
||
|
}, extra=vol.ALLOW_EXTRA)
|
||
|
|
||
|
IDENTIFY_DEVICE_SCHEMA = vol.Schema({
|
||
|
vol.Required(ATTR_DEVICE_NAME): cv.string,
|
||
|
vol.Required(ATTR_DEVICE_LOCALIZED_MODEL): cv.string,
|
||
|
vol.Required(ATTR_DEVICE_MODEL): cv.string,
|
||
|
vol.Required(ATTR_DEVICE_PERMANENT_ID): cv.string,
|
||
|
vol.Required(ATTR_DEVICE_SYSTEM_VERSION): cv.string,
|
||
|
vol.Required(ATTR_DEVICE_TYPE): cv.string,
|
||
|
vol.Required(ATTR_DEVICE_SYSTEM_NAME): cv.string,
|
||
|
}, extra=vol.ALLOW_EXTRA)
|
||
|
|
||
|
IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA)
|
||
|
|
||
|
IDENTIFY_APP_SCHEMA = vol.Schema({
|
||
|
vol.Required(ATTR_APP_BUNDLE_IDENTIFER): cv.string,
|
||
|
vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int,
|
||
|
vol.Required(ATTR_APP_VERSION_NUMBER): cv.positive_int
|
||
|
}, extra=vol.ALLOW_EXTRA)
|
||
|
|
||
|
IDENTIFY_APP_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_APP_SCHEMA)
|
||
|
|
||
|
IDENTIFY_BATTERY_SCHEMA = vol.Schema({
|
||
|
vol.Required(ATTR_BATTERY_LEVEL): cv.positive_int,
|
||
|
vol.Required(ATTR_BATTERY_STATE): vol.In(BATTERY_STATES)
|
||
|
}, extra=vol.ALLOW_EXTRA)
|
||
|
|
||
|
IDENTIFY_BATTERY_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_BATTERY_SCHEMA)
|
||
|
|
||
|
IDENTIFY_SCHEMA = vol.Schema({
|
||
|
vol.Required(ATTR_DEVICE): IDENTIFY_DEVICE_SCHEMA_CONTAINER,
|
||
|
vol.Required(ATTR_BATTERY): IDENTIFY_BATTERY_SCHEMA_CONTAINER,
|
||
|
vol.Required(ATTR_PUSH_TOKEN): cv.string,
|
||
|
vol.Required(ATTR_APP): IDENTIFY_APP_SCHEMA_CONTAINER,
|
||
|
vol.Required(ATTR_PERMISSIONS): vol.All(cv.ensure_list,
|
||
|
[vol.In(PERMISSIONS)]),
|
||
|
vol.Required(ATTR_PUSH_ID): cv.string,
|
||
|
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||
|
vol.Optional(ATTR_PUSH_SOUNDS): list
|
||
|
}, extra=vol.ALLOW_EXTRA)
|
||
|
|
||
|
CONFIGURATION_FILE = "ios.conf"
|
||
|
|
||
|
CONFIG_FILE = {ATTR_DEVICES: {}}
|
||
|
|
||
|
CONFIG_FILE_PATH = ""
|
||
|
|
||
|
|
||
|
def _load_config(filename):
|
||
|
"""Load configuration."""
|
||
|
if not os.path.isfile(filename):
|
||
|
return {}
|
||
|
|
||
|
try:
|
||
|
with open(filename, "r") as fdesc:
|
||
|
inp = fdesc.read()
|
||
|
|
||
|
# In case empty file
|
||
|
if not inp:
|
||
|
return {}
|
||
|
|
||
|
return json.loads(inp)
|
||
|
except (IOError, ValueError) as error:
|
||
|
_LOGGER.error("Reading config file %s failed: %s", filename, error)
|
||
|
return None
|
||
|
|
||
|
|
||
|
def _save_config(filename, config):
|
||
|
"""Save configuration."""
|
||
|
try:
|
||
|
with open(filename, "w") as fdesc:
|
||
|
fdesc.write(json.dumps(config))
|
||
|
except (IOError, TypeError) as error:
|
||
|
_LOGGER.error("Saving config file failed: %s", error)
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
|
||
|
def devices_with_push():
|
||
|
"""Return a dictionary of push enabled targets."""
|
||
|
targets = {}
|
||
|
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
|
||
|
if device.get(ATTR_PUSH_ID) is not None:
|
||
|
targets[device_name] = device.get(ATTR_PUSH_ID)
|
||
|
return targets
|
||
|
|
||
|
|
||
|
def enabled_push_ids():
|
||
|
"""Return a list of push enabled target push IDs."""
|
||
|
push_ids = list()
|
||
|
# pylint: disable=unused-variable
|
||
|
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
|
||
|
if device.get(ATTR_PUSH_ID) is not None:
|
||
|
push_ids.append(device.get(ATTR_PUSH_ID))
|
||
|
return push_ids
|
||
|
|
||
|
|
||
|
def devices():
|
||
|
"""Return a dictionary of all identified devices."""
|
||
|
return CONFIG_FILE[ATTR_DEVICES]
|
||
|
|
||
|
|
||
|
def device_name_for_push_id(push_id):
|
||
|
"""Return the device name for the push ID."""
|
||
|
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
|
||
|
if device.get(ATTR_PUSH_ID) is push_id:
|
||
|
return device_name
|
||
|
return None
|
||
|
|
||
|
|
||
|
def setup(hass, config):
|
||
|
"""Setup the iOS component."""
|
||
|
# pylint: disable=global-statement, import-error
|
||
|
global CONFIG_FILE
|
||
|
global CONFIG_FILE_PATH
|
||
|
|
||
|
CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE)
|
||
|
|
||
|
CONFIG_FILE = _load_config(CONFIG_FILE_PATH)
|
||
|
|
||
|
if CONFIG_FILE == {}:
|
||
|
CONFIG_FILE[ATTR_DEVICES] = {}
|
||
|
|
||
|
device_tracker = loader.get_component("device_tracker")
|
||
|
if device_tracker.DOMAIN not in hass.config.components:
|
||
|
device_tracker.setup(hass, {})
|
||
|
# Need this to enable requirements checking in the app.
|
||
|
hass.config.components.append(device_tracker.DOMAIN)
|
||
|
|
||
|
if "notify.ios" not in hass.config.components:
|
||
|
notify = loader.get_component("notify.ios")
|
||
|
notify.get_service(hass, {})
|
||
|
# Need this to enable requirements checking in the app.
|
||
|
if NotifyDomain not in hass.config.components:
|
||
|
hass.config.components.append(NotifyDomain)
|
||
|
|
||
|
zeroconf = loader.get_component("zeroconf")
|
||
|
if zeroconf.DOMAIN not in hass.config.components:
|
||
|
zeroconf.setup(hass, config)
|
||
|
# Need this to enable requirements checking in the app.
|
||
|
hass.config.components.append(zeroconf.DOMAIN)
|
||
|
|
||
|
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||
|
|
||
|
hass.wsgi.register_view(iOSIdentifyDeviceView(hass))
|
||
|
|
||
|
if config.get(DOMAIN) is not None:
|
||
|
app_config = config[DOMAIN]
|
||
|
if app_config.get(CONF_PUSH) is not None:
|
||
|
push_config = app_config[CONF_PUSH]
|
||
|
hass.wsgi.register_view(iOSPushConfigView(hass, push_config))
|
||
|
|
||
|
return True
|
||
|
|
||
|
|
||
|
# pylint: disable=invalid-name
|
||
|
class iOSPushConfigView(HomeAssistantView):
|
||
|
"""A view that provides the push categories configuration."""
|
||
|
|
||
|
url = "/api/ios/push"
|
||
|
name = "api:ios:push"
|
||
|
|
||
|
def __init__(self, hass, push_config):
|
||
|
"""Init the view."""
|
||
|
super().__init__(hass)
|
||
|
self.push_config = push_config
|
||
|
|
||
|
def get(self, request):
|
||
|
"""Handle the GET request for the push configuration."""
|
||
|
return self.json(self.push_config)
|
||
|
|
||
|
|
||
|
class iOSIdentifyDeviceView(HomeAssistantView):
|
||
|
"""A view that accepts device identification requests."""
|
||
|
|
||
|
url = "/api/ios/identify"
|
||
|
name = "api:ios:identify"
|
||
|
|
||
|
def __init__(self, hass):
|
||
|
"""Init the view."""
|
||
|
super().__init__(hass)
|
||
|
|
||
|
def post(self, request):
|
||
|
"""Handle the POST request for device identification."""
|
||
|
try:
|
||
|
data = IDENTIFY_SCHEMA(request.json)
|
||
|
except vol.Invalid as ex:
|
||
|
return self.json_message(humanize_error(request.json, ex),
|
||
|
HTTP_BAD_REQUEST)
|
||
|
|
||
|
name = data.get(ATTR_DEVICE_ID)
|
||
|
|
||
|
CONFIG_FILE[ATTR_DEVICES][name] = data
|
||
|
|
||
|
if not _save_config(CONFIG_FILE_PATH, CONFIG_FILE):
|
||
|
return self.json_message("Error saving device.",
|
||
|
HTTP_INTERNAL_SERVER_ERROR)
|
||
|
|
||
|
return self.json({"status": "registered"})
|