Add config entry to iOS (#16580)
* Add config entry to iOS * Add translation
This commit is contained in:
parent
2682d26f79
commit
3824582e68
8 changed files with 153 additions and 40 deletions
14
homeassistant/components/ios/.translations/en.json
Normal file
14
homeassistant/components/ios/.translations/en.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,13 +11,14 @@ import datetime
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
# from voluptuous.humanize import humanize_error
|
# from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
|
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
|
||||||
HTTP_BAD_REQUEST)
|
HTTP_BAD_REQUEST)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import (
|
||||||
from homeassistant.helpers import discovery
|
config_validation as cv, discovery, config_entry_flow)
|
||||||
from homeassistant.util.json import load_json, save_json
|
from homeassistant.util.json import load_json, save_json
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,62 +165,70 @@ IDENTIFY_SCHEMA = vol.Schema({
|
||||||
|
|
||||||
CONFIGURATION_FILE = '.ios.conf'
|
CONFIGURATION_FILE = '.ios.conf'
|
||||||
|
|
||||||
CONFIG_FILE = {ATTR_DEVICES: {}}
|
|
||||||
|
|
||||||
CONFIG_FILE_PATH = ""
|
def devices_with_push(hass):
|
||||||
|
|
||||||
|
|
||||||
def devices_with_push():
|
|
||||||
"""Return a dictionary of push enabled targets."""
|
"""Return a dictionary of push enabled targets."""
|
||||||
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:
|
if device.get(ATTR_PUSH_ID) is not None:
|
||||||
targets[device_name] = device.get(ATTR_PUSH_ID)
|
targets[device_name] = device.get(ATTR_PUSH_ID)
|
||||||
return targets
|
return targets
|
||||||
|
|
||||||
|
|
||||||
def enabled_push_ids():
|
def enabled_push_ids(hass):
|
||||||
"""Return a list of push enabled target push IDs."""
|
"""Return a list of push enabled target push IDs."""
|
||||||
push_ids = list()
|
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:
|
if device.get(ATTR_PUSH_ID) is not None:
|
||||||
push_ids.append(device.get(ATTR_PUSH_ID))
|
push_ids.append(device.get(ATTR_PUSH_ID))
|
||||||
return push_ids
|
return push_ids
|
||||||
|
|
||||||
|
|
||||||
def devices():
|
def devices(hass):
|
||||||
"""Return a dictionary of all identified devices."""
|
"""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."""
|
"""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:
|
if device.get(ATTR_PUSH_ID) is push_id:
|
||||||
return device_name
|
return device_name
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the iOS component."""
|
"""Set up the iOS component."""
|
||||||
global CONFIG_FILE
|
conf = config.get(DOMAIN)
|
||||||
global CONFIG_FILE_PATH
|
|
||||||
|
|
||||||
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 == {}:
|
ios_config[CONF_PUSH] = (conf or {}).get(CONF_PUSH, {})
|
||||||
CONFIG_FILE[ATTR_DEVICES] = {}
|
|
||||||
|
|
||||||
|
hass.data[DOMAIN] = ios_config
|
||||||
|
|
||||||
|
# No entry support for notify component yet
|
||||||
discovery.load_platform(hass, 'notify', DOMAIN, {}, config)
|
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
|
return True
|
||||||
|
|
||||||
|
@ -247,6 +256,10 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
||||||
url = '/api/ios/identify'
|
url = '/api/ios/identify'
|
||||||
name = 'api:ios:identify'
|
name = 'api:ios:identify'
|
||||||
|
|
||||||
|
def __init__(self, config_path):
|
||||||
|
"""Initiliaze the view."""
|
||||||
|
self._config_path = config_path
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Handle the POST request for device identification."""
|
"""Handle the POST request for device identification."""
|
||||||
|
@ -255,6 +268,8 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
|
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
|
hass = request.app['hass']
|
||||||
|
|
||||||
# Commented for now while iOS app is getting frequent updates
|
# Commented for now while iOS app is getting frequent updates
|
||||||
# try:
|
# try:
|
||||||
# data = IDENTIFY_SCHEMA(req_data)
|
# data = IDENTIFY_SCHEMA(req_data)
|
||||||
|
@ -266,12 +281,16 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
||||||
|
|
||||||
name = data.get(ATTR_DEVICE_ID)
|
name = data.get(ATTR_DEVICE_ID)
|
||||||
|
|
||||||
CONFIG_FILE[ATTR_DEVICES][name] = data
|
hass.data[DOMAIN][ATTR_DEVICES][name] = data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
save_json(CONFIG_FILE_PATH, CONFIG_FILE)
|
save_json(self._config_path, hass.data[DOMAIN])
|
||||||
except HomeAssistantError:
|
except HomeAssistantError:
|
||||||
return self.json_message("Error saving device.",
|
return self.json_message("Error saving device.",
|
||||||
HTTP_INTERNAL_SERVER_ERROR)
|
HTTP_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
return self.json({"status": "registered"})
|
return self.json({"status": "registered"})
|
||||||
|
|
||||||
|
|
||||||
|
config_entry_flow.register_discovery_flow(
|
||||||
|
DOMAIN, 'Home Assistant iOS', lambda *_: True)
|
14
homeassistant/components/ios/strings.json
Normal file
14
homeassistant/components/ios/strings.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ DEPENDENCIES = ["ios"]
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# 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."""
|
"""Output rate limit log line at given level."""
|
||||||
rate_limits = resp["rateLimits"]
|
rate_limits = resp["rateLimits"]
|
||||||
resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"])
|
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, "
|
"%d sent, %d allowed, %d errors, "
|
||||||
"resets in %s")
|
"resets in %s")
|
||||||
_LOGGER.log(level, rate_limit_msg,
|
_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["successful"],
|
||||||
rate_limits["maximum"], rate_limits["errors"],
|
rate_limits["maximum"], rate_limits["errors"],
|
||||||
str(resetsAtTime).split(".")[0])
|
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.
|
# Need this to enable requirements checking in the app.
|
||||||
hass.config.components.add("notify.ios")
|
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 "
|
_LOGGER.error("The notify.ios platform was loaded but no "
|
||||||
"devices exist! Please check the documentation at "
|
"devices exist! Please check the documentation at "
|
||||||
"https://home-assistant.io/ecosystem/ios/notifications"
|
"https://home-assistant.io/ecosystem/ios/notifications"
|
||||||
|
@ -64,7 +64,7 @@ class iOSNotificationService(BaseNotificationService):
|
||||||
@property
|
@property
|
||||||
def targets(self):
|
def targets(self):
|
||||||
"""Return a dictionary of registered targets."""
|
"""Return a dictionary of registered targets."""
|
||||||
return ios.devices_with_push()
|
return ios.devices_with_push(self.hass)
|
||||||
|
|
||||||
def send_message(self, message="", **kwargs):
|
def send_message(self, message="", **kwargs):
|
||||||
"""Send a message to the Lambda APNS gateway."""
|
"""Send a message to the Lambda APNS gateway."""
|
||||||
|
@ -78,13 +78,13 @@ class iOSNotificationService(BaseNotificationService):
|
||||||
targets = kwargs.get(ATTR_TARGET)
|
targets = kwargs.get(ATTR_TARGET)
|
||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
targets = ios.enabled_push_ids()
|
targets = ios.enabled_push_ids(self.hass)
|
||||||
|
|
||||||
if kwargs.get(ATTR_DATA) is not None:
|
if kwargs.get(ATTR_DATA) is not None:
|
||||||
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
|
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
|
||||||
|
|
||||||
for target in targets:
|
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.",
|
_LOGGER.error("The target (%s) does not exist in .ios.conf.",
|
||||||
targets)
|
targets)
|
||||||
return
|
return
|
||||||
|
@ -102,8 +102,8 @@ class iOSNotificationService(BaseNotificationService):
|
||||||
message = req.json().get("message", fallback_message)
|
message = req.json().get("message", fallback_message)
|
||||||
if req.status_code == 429:
|
if req.status_code == 429:
|
||||||
_LOGGER.warning(message)
|
_LOGGER.warning(message)
|
||||||
log_rate_limits(target, req.json(), 30)
|
log_rate_limits(self.hass, target, req.json(), 30)
|
||||||
else:
|
else:
|
||||||
_LOGGER.error(message)
|
_LOGGER.error(message)
|
||||||
else:
|
else:
|
||||||
log_rate_limits(target, req.json())
|
log_rate_limits(self.hass, target, req.json())
|
||||||
|
|
|
@ -21,14 +21,17 @@ DEFAULT_ICON_STATE = 'mdi:power-plug'
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the iOS sensor."""
|
"""Set up the iOS sensor."""
|
||||||
if discovery_info is None:
|
# Leave here for if someone accidentally adds platform: ios to config
|
||||||
return
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up iOS from a config entry."""
|
||||||
dev = list()
|
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'):
|
for sensor_type in ('level', 'state'):
|
||||||
dev.append(IOSSensor(sensor_type, device_name, device))
|
dev.append(IOSSensor(sensor_type, device_name, device))
|
||||||
|
|
||||||
add_entities(dev, True)
|
async_add_entities(dev, True)
|
||||||
|
|
||||||
|
|
||||||
class IOSSensor(Entity):
|
class IOSSensor(Entity):
|
||||||
|
@ -102,5 +105,5 @@ class IOSSensor(Entity):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""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]
|
self._state = self._device[ios.ATTR_BATTERY][self.type]
|
||||||
|
|
|
@ -140,6 +140,7 @@ FLOWS = [
|
||||||
'deconz',
|
'deconz',
|
||||||
'homematicip_cloud',
|
'homematicip_cloud',
|
||||||
'hue',
|
'hue',
|
||||||
|
'ios',
|
||||||
'nest',
|
'nest',
|
||||||
'openuv',
|
'openuv',
|
||||||
'sonos',
|
'sonos',
|
||||||
|
|
1
tests/components/ios/__init__.py
Normal file
1
tests/components/ios/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the iOS component."""
|
61
tests/components/ios/test_init.py
Normal file
61
tests/components/ios/test_init.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue