diff --git a/homeassistant/components/ios/.translations/en.json b/homeassistant/components/ios/.translations/en.json new file mode 100644 index 00000000000..ae2e4e03f74 --- /dev/null +++ b/homeassistant/components/ios/.translations/en.json @@ -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" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios/__init__.py similarity index 81% rename from homeassistant/components/ios.py rename to homeassistant/components/ios/__init__.py index 7f7377469fd..3766e00879d 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios/__init__.py @@ -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) diff --git a/homeassistant/components/ios/strings.json b/homeassistant/components/ios/strings.json new file mode 100644 index 00000000000..cbb63cf8229 --- /dev/null +++ b/homeassistant/components/ios/strings.json @@ -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." + } + } +} diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/notify/ios.py index 8609e1dabee..e6a37d707ad 100644 --- a/homeassistant/components/notify/ios.py +++ b/homeassistant/components/notify/ios.py @@ -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()) diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index f775381c4ec..a50d1161676 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -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] diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 15932f2c3f8..b06d6e7df55 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -140,6 +140,7 @@ FLOWS = [ 'deconz', 'homematicip_cloud', 'hue', + 'ios', 'nest', 'openuv', 'sonos', diff --git a/tests/components/ios/__init__.py b/tests/components/ios/__init__.py new file mode 100644 index 00000000000..a028090473e --- /dev/null +++ b/tests/components/ios/__init__.py @@ -0,0 +1 @@ +"""Tests for the iOS component.""" diff --git a/tests/components/ios/test_init.py b/tests/components/ios/test_init.py new file mode 100644 index 00000000000..9a45fac3ce1 --- /dev/null +++ b/tests/components/ios/test_init.py @@ -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