Add config entry to iOS (#16580)

* Add config entry to iOS

* Add translation
This commit is contained in:
Paulus Schoutsen 2018-09-12 20:17:52 +02:00 committed by GitHub
parent 2682d26f79
commit 3824582e68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 153 additions and 40 deletions

View File

@ -0,0 +1,14 @@
{
"config": {
"abort": {
"single_instance_allowed": "Only a single configuration of Home Assistant iOS is necessary."
},
"step": {
"confirm": {
"description": "Do you want to set up the Home Assistant iOS component?",
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@ -11,13 +11,14 @@ import datetime
import voluptuous as vol
# from voluptuous.humanize import humanize_error
from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
HTTP_BAD_REQUEST)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers import (
config_validation as cv, discovery, config_entry_flow)
from homeassistant.util.json import load_json, save_json
@ -164,62 +165,70 @@ IDENTIFY_SCHEMA = vol.Schema({
CONFIGURATION_FILE = '.ios.conf'
CONFIG_FILE = {ATTR_DEVICES: {}}
CONFIG_FILE_PATH = ""
def devices_with_push():
def devices_with_push(hass):
"""Return a dictionary of push enabled targets."""
targets = {}
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
for device_name, device in hass.data[DOMAIN][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():
def enabled_push_ids(hass):
"""Return a list of push enabled target push IDs."""
push_ids = list()
for device in CONFIG_FILE[ATTR_DEVICES].values():
for device in hass.data[DOMAIN][ATTR_DEVICES].values():
if device.get(ATTR_PUSH_ID) is not None:
push_ids.append(device.get(ATTR_PUSH_ID))
return push_ids
def devices():
def devices(hass):
"""Return a dictionary of all identified devices."""
return CONFIG_FILE[ATTR_DEVICES]
return hass.data[DOMAIN][ATTR_DEVICES]
def device_name_for_push_id(push_id):
def device_name_for_push_id(hass, push_id):
"""Return the device name for the push ID."""
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items():
if device.get(ATTR_PUSH_ID) is push_id:
return device_name
return None
def setup(hass, config):
async def async_setup(hass, config):
"""Set up the iOS component."""
global CONFIG_FILE
global CONFIG_FILE_PATH
conf = config.get(DOMAIN)
CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE)
ios_config = await hass.async_add_executor_job(
load_json, hass.config.path(CONFIGURATION_FILE))
CONFIG_FILE = load_json(CONFIG_FILE_PATH)
if ios_config == {}:
ios_config[ATTR_DEVICES] = {}
if CONFIG_FILE == {}:
CONFIG_FILE[ATTR_DEVICES] = {}
ios_config[CONF_PUSH] = (conf or {}).get(CONF_PUSH, {})
hass.data[DOMAIN] = ios_config
# No entry support for notify component yet
discovery.load_platform(hass, 'notify', DOMAIN, {}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
if conf is not None:
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
hass.http.register_view(iOSIdentifyDeviceView)
return True
app_config = config.get(DOMAIN, {})
hass.http.register_view(iOSPushConfigView(app_config.get(CONF_PUSH, {})))
async def async_setup_entry(hass, entry):
"""Set up an iOS entry."""
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, 'sensor'))
hass.http.register_view(
iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE)))
hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_PUSH]))
return True
@ -247,6 +256,10 @@ class iOSIdentifyDeviceView(HomeAssistantView):
url = '/api/ios/identify'
name = 'api:ios:identify'
def __init__(self, config_path):
"""Initiliaze the view."""
self._config_path = config_path
@asyncio.coroutine
def post(self, request):
"""Handle the POST request for device identification."""
@ -255,6 +268,8 @@ class iOSIdentifyDeviceView(HomeAssistantView):
except ValueError:
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
hass = request.app['hass']
# Commented for now while iOS app is getting frequent updates
# try:
# data = IDENTIFY_SCHEMA(req_data)
@ -266,12 +281,16 @@ class iOSIdentifyDeviceView(HomeAssistantView):
name = data.get(ATTR_DEVICE_ID)
CONFIG_FILE[ATTR_DEVICES][name] = data
hass.data[DOMAIN][ATTR_DEVICES][name] = data
try:
save_json(CONFIG_FILE_PATH, CONFIG_FILE)
save_json(self._config_path, hass.data[DOMAIN])
except HomeAssistantError:
return self.json_message("Error saving device.",
HTTP_INTERNAL_SERVER_ERROR)
return self.json({"status": "registered"})
config_entry_flow.register_discovery_flow(
DOMAIN, 'Home Assistant iOS', lambda *_: True)

View File

@ -0,0 +1,14 @@
{
"config": {
"title": "Home Assistant iOS",
"step": {
"confirm": {
"title": "Home Assistant iOS",
"description": "Do you want to set up the Home Assistant iOS component?"
}
},
"abort": {
"single_instance_allowed": "Only a single configuration of Home Assistant iOS is necessary."
}
}
}

View File

@ -24,7 +24,7 @@ DEPENDENCIES = ["ios"]
# pylint: disable=invalid-name
def log_rate_limits(target, resp, level=20):
def log_rate_limits(hass, target, resp, level=20):
"""Output rate limit log line at given level."""
rate_limits = resp["rateLimits"]
resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"])
@ -33,7 +33,7 @@ def log_rate_limits(target, resp, level=20):
"%d sent, %d allowed, %d errors, "
"resets in %s")
_LOGGER.log(level, rate_limit_msg,
ios.device_name_for_push_id(target),
ios.device_name_for_push_id(hass, target),
rate_limits["successful"],
rate_limits["maximum"], rate_limits["errors"],
str(resetsAtTime).split(".")[0])
@ -45,7 +45,7 @@ def get_service(hass, config, discovery_info=None):
# Need this to enable requirements checking in the app.
hass.config.components.add("notify.ios")
if not ios.devices_with_push():
if not ios.devices_with_push(hass):
_LOGGER.error("The notify.ios platform was loaded but no "
"devices exist! Please check the documentation at "
"https://home-assistant.io/ecosystem/ios/notifications"
@ -64,7 +64,7 @@ class iOSNotificationService(BaseNotificationService):
@property
def targets(self):
"""Return a dictionary of registered targets."""
return ios.devices_with_push()
return ios.devices_with_push(self.hass)
def send_message(self, message="", **kwargs):
"""Send a message to the Lambda APNS gateway."""
@ -78,13 +78,13 @@ class iOSNotificationService(BaseNotificationService):
targets = kwargs.get(ATTR_TARGET)
if not targets:
targets = ios.enabled_push_ids()
targets = ios.enabled_push_ids(self.hass)
if kwargs.get(ATTR_DATA) is not None:
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
for target in targets:
if target not in ios.enabled_push_ids():
if target not in ios.enabled_push_ids(self.hass):
_LOGGER.error("The target (%s) does not exist in .ios.conf.",
targets)
return
@ -102,8 +102,8 @@ class iOSNotificationService(BaseNotificationService):
message = req.json().get("message", fallback_message)
if req.status_code == 429:
_LOGGER.warning(message)
log_rate_limits(target, req.json(), 30)
log_rate_limits(self.hass, target, req.json(), 30)
else:
_LOGGER.error(message)
else:
log_rate_limits(target, req.json())
log_rate_limits(self.hass, target, req.json())

View File

@ -21,14 +21,17 @@ DEFAULT_ICON_STATE = 'mdi:power-plug'
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the iOS sensor."""
if discovery_info is None:
return
# Leave here for if someone accidentally adds platform: ios to config
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up iOS from a config entry."""
dev = list()
for device_name, device in ios.devices().items():
for device_name, device in ios.devices(hass).items():
for sensor_type in ('level', 'state'):
dev.append(IOSSensor(sensor_type, device_name, device))
add_entities(dev, True)
async_add_entities(dev, True)
class IOSSensor(Entity):
@ -102,5 +105,5 @@ class IOSSensor(Entity):
def update(self):
"""Get the latest state of the sensor."""
self._device = ios.devices().get(self._device_name)
self._device = ios.devices(self.hass).get(self._device_name)
self._state = self._device[ios.ATTR_BATTERY][self.type]

View File

@ -140,6 +140,7 @@ FLOWS = [
'deconz',
'homematicip_cloud',
'hue',
'ios',
'nest',
'openuv',
'sonos',

View File

@ -0,0 +1 @@
"""Tests for the iOS component."""

View File

@ -0,0 +1,61 @@
"""Tests for the iOS init file."""
from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.setup import async_setup_component
from homeassistant.components import ios
from tests.common import mock_component, mock_coro
@pytest.fixture(autouse=True)
def mock_load_json():
"""Mock load_json."""
with patch('homeassistant.components.ios.load_json', return_value={}):
yield
@pytest.fixture(autouse=True)
def mock_dependencies(hass):
"""Mock dependencies loaded."""
mock_component(hass, 'zeroconf')
mock_component(hass, 'device_tracker')
async def test_creating_entry_sets_up_sensor(hass):
"""Test setting up iOS loads the sensor component."""
with patch('homeassistant.components.sensor.ios.async_setup_entry',
return_value=mock_coro(True)) as mock_setup:
result = await hass.config_entries.flow.async_init(
ios.DOMAIN, context={'source': config_entries.SOURCE_USER})
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
async def test_configuring_ios_creates_entry(hass):
"""Test that specifying config will create an entry."""
with patch('homeassistant.components.ios.async_setup_entry',
return_value=mock_coro(True)) as mock_setup:
await async_setup_component(hass, ios.DOMAIN, {
'ios': {
'push': {}
}
})
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
async def test_not_configuring_ios_not_creates_entry(hass):
"""Test that no config will not create an entry."""
with patch('homeassistant.components.ios.async_setup_entry',
return_value=mock_coro(True)) as mock_setup:
await async_setup_component(hass, ios.DOMAIN, {})
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 0