diff --git a/.coveragerc b/.coveragerc index d241260fdf0..145350b6b19 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,7 +10,15 @@ omit = homeassistant/util/async.py # omit pieces of code that rely on external devices being present - homeassistant/components/abode/* + homeassistant/components/abode/__init__.py + homeassistant/components/abode/alarm_control_panel.py + homeassistant/components/abode/binary_sensor.py + homeassistant/components/abode/camera.py + homeassistant/components/abode/cover.py + homeassistant/components/abode/light.py + homeassistant/components/abode/lock.py + homeassistant/components/abode/sensor.py + homeassistant/components/abode/switch.py homeassistant/components/acer_projector/switch.py homeassistant/components/actiontec/device_tracker.py homeassistant/components/adguard/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 070151d01e0..ea50d24095c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,6 +13,7 @@ homeassistant/util/* @home-assistant/core homeassistant/scripts/check_config.py @kellerza # Integrations +homeassistant/components/abode/* @shred86 homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index b7f13d49b69..aeba437aceb 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -1,49 +1,36 @@ -"""Support for Abode Home Security system.""" -import logging +"""Support for the Abode Security System.""" +from asyncio import gather +from copy import deepcopy from functools import partial -from requests.exceptions import HTTPError, ConnectTimeout -import abodepy -import abodepy.helpers.constants as CONST +import logging + +from abodepy import Abode from abodepy.exceptions import AbodeException import abodepy.helpers.timeline as TIMELINE - +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, - ATTR_TIME, ATTR_ENTITY_ID, - CONF_USERNAME, + ATTR_TIME, CONF_PASSWORD, - CONF_EXCLUDE, - CONF_NAME, - CONF_LIGHTS, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity +from .const import ATTRIBUTION, DOMAIN + _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by goabode.com" - CONF_POLLING = "polling" -DOMAIN = "abode" DEFAULT_CACHEDB = "./abodepy_cache.pickle" -NOTIFICATION_ID = "abode_notification" -NOTIFICATION_TITLE = "Abode Security Setup" - -EVENT_ABODE_ALARM = "abode_alarm" -EVENT_ABODE_ALARM_END = "abode_alarm_end" -EVENT_ABODE_AUTOMATION = "abode_automation" -EVENT_ABODE_FAULT = "abode_panel_fault" -EVENT_ABODE_RESTORE = "abode_panel_restore" - SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_TRIGGER = "trigger_quick_action" @@ -67,10 +54,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_POLLING, default=False): cv.boolean, - vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, - vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, } ) }, @@ -100,73 +84,80 @@ ABODE_PLATFORMS = [ class AbodeSystem: """Abode System class.""" - def __init__(self, username, password, cache, name, polling, exclude, lights): + def __init__(self, abode, polling): """Initialize the system.""" - self.abode = abodepy.Abode( - username, - password, - auto_login=True, - get_devices=True, - get_automations=True, - cache_path=cache, - ) - self.name = name + self.abode = abode self.polling = polling - self.exclude = exclude - self.lights = lights self.devices = [] - - def is_excluded(self, device): - """Check if a device is configured to be excluded.""" - return device.device_id in self.exclude - - def is_automation_excluded(self, automation): - """Check if an automation is configured to be excluded.""" - return automation.automation_id in self.exclude - - def is_light(self, device): - """Check if a switch device is configured as a light.""" - - return device.generic_type == CONST.TYPE_LIGHT or ( - device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights - ) + self.logout_listener = None -def setup(hass, config): - """Set up Abode component.""" +async def async_setup(hass, config): + """Set up Abode integration.""" + if DOMAIN not in config: + return True conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - name = conf.get(CONF_NAME) - polling = conf.get(CONF_POLLING) - exclude = conf.get(CONF_EXCLUDE) - lights = conf.get(CONF_LIGHTS) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf) + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Abode integration from a config entry.""" + username = config_entry.data.get(CONF_USERNAME) + password = config_entry.data.get(CONF_PASSWORD) + polling = config_entry.data.get(CONF_POLLING) try: cache = hass.config.path(DEFAULT_CACHEDB) - hass.data[DOMAIN] = AbodeSystem( - username, password, cache, name, polling, exclude, lights + abode = await hass.async_add_executor_job( + Abode, username, password, True, True, True, cache ) + hass.data[DOMAIN] = AbodeSystem(abode, polling) + except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) - - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) return False - setup_hass_services(hass) - setup_hass_events(hass) - setup_abode_events(hass) + for platform in ABODE_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + await setup_hass_events(hass) + await hass.async_add_executor_job(setup_hass_services, hass) + await hass.async_add_executor_job(setup_abode_events, hass) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + hass.services.async_remove(DOMAIN, SERVICE_SETTINGS) + hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE) + hass.services.async_remove(DOMAIN, SERVICE_TRIGGER) + + tasks = [] for platform in ABODE_PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) + tasks.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) + + await gather(*tasks) + + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout) + + hass.data[DOMAIN].logout_listener() + hass.data.pop(DOMAIN) return True @@ -223,13 +214,9 @@ def setup_hass_services(hass): ) -def setup_hass_events(hass): +async def setup_hass_events(hass): """Home Assistant start and stop callbacks.""" - def startup(event): - """Listen for push events.""" - hass.data[DOMAIN].abode.events.start() - def logout(event): """Logout of Abode.""" if not hass.data[DOMAIN].polling: @@ -239,9 +226,11 @@ def setup_hass_events(hass): _LOGGER.info("Logged out of Abode") if not hass.data[DOMAIN].polling: - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout) + hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, logout + ) def setup_abode_events(hass): @@ -282,30 +271,36 @@ class AbodeDevice(Entity): """Representation of an Abode device.""" def __init__(self, data, device): - """Initialize a sensor for Abode device.""" + """Initialize Abode device.""" self._data = data self._device = device async def async_added_to_hass(self): - """Subscribe Abode events.""" + """Subscribe to device events.""" self.hass.async_add_job( self._data.abode.events.add_device_callback, self._device.device_id, self._update_callback, ) + async def async_will_remove_from_hass(self): + """Unsubscribe from device events.""" + self.hass.async_add_job( + self._data.abode.events.remove_all_device_callbacks, self._device.device_id + ) + @property def should_poll(self): """Return the polling state.""" return self._data.polling def update(self): - """Update automation state.""" + """Update device and automation states.""" self._device.refresh() @property def name(self): - """Return the name of the sensor.""" + """Return the name of the device.""" return self._device.name @property @@ -319,6 +314,21 @@ class AbodeDevice(Entity): "device_type": self._device.type, } + @property + def unique_id(self): + """Return a unique ID to use for this device.""" + return self._device.device_uuid + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "manufacturer": "Abode", + "name": self._device.name, + "device_type": self._device.type, + } + def _update_callback(self, device): """Update the device state.""" self.schedule_update_ha_state() @@ -353,7 +363,7 @@ class AbodeAutomation(Entity): @property def name(self): - """Return the name of the sensor.""" + """Return the name of the automation.""" return self._automation.name @property @@ -367,6 +377,6 @@ class AbodeAutomation(Entity): } def _update_callback(self, device): - """Update the device state.""" + """Update the automation state.""" self._automation.refresh() self.schedule_update_ha_state() diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index c5c10e65302..f774e773cb5 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -9,32 +9,31 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) -from . import ATTRIBUTION, DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import ATTRIBUTION, DOMAIN _LOGGER = logging.getLogger(__name__) ICON = "mdi:security" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up an alarm control panel for an Abode device.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] - alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)] - - data.devices.extend(alarm_devices) - - add_entities(alarm_devices) + async_add_entities( + [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] + ) class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """An alarm_control_panel implementation for Abode.""" - def __init__(self, data, device, name): - """Initialize the alarm control panel.""" - super().__init__(data, device) - self._name = name - @property def icon(self): """Return the icon.""" @@ -65,11 +64,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """Send arm away command.""" self._device.set_away() - @property - def name(self): - """Return the name of the alarm.""" - return self._name or super().name - @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 3ae7f41d84e..31f74448496 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -6,15 +6,20 @@ import abodepy.helpers.timeline as TIMELINE from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for an Abode device.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a sensor for an Abode device.""" + data = hass.data[DOMAIN] device_types = [ CONST.TYPE_CONNECTIVITY, @@ -25,25 +30,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] devices = [] - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device): - continue + for device in data.abode.get_devices(generic_type=device_types): devices.append(AbodeBinarySensor(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION): - if data.is_automation_excluded(automation): - continue - devices.append( AbodeQuickActionBinarySensor( data, automation, TIMELINE.AUTOMATION_EDIT_GROUP ) ) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index f52bbe17475..e98a59a985c 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -2,35 +2,36 @@ from datetime import timedelta import logging -import requests import abodepy.helpers.constants as CONST import abodepy.helpers.timeline as TIMELINE +import requests from homeassistant.components.camera import Camera from homeassistant.util import Throttle -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode camera devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a camera for an Abode device.""" + + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - if data.is_excluded(device): - continue - devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeCamera(AbodeDevice, Camera): diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py new file mode 100644 index 00000000000..d8d914f7998 --- /dev/null +++ b/homeassistant/components/abode/config_flow.py @@ -0,0 +1,79 @@ +"""Config flow for the Abode Security System component.""" +import logging + +from abodepy import Abode +from abodepy.exceptions import AbodeException +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import DOMAIN # pylint: disable=W0611 + +CONF_POLLING = "polling" + +_LOGGER = logging.getLogger(__name__) + + +class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Abode.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize.""" + self.data_schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if not user_input: + return self._show_form() + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + polling = user_input.get(CONF_POLLING, False) + + try: + await self.hass.async_add_executor_job(Abode, username, password, True) + + except (AbodeException, ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Abode: %s", str(ex)) + if ex.errcode == 400: + return self._show_form({"base": "invalid_credentials"}) + return self._show_form({"base": "connection_error"}) + + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_POLLING: polling, + }, + ) + + @callback + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(self.data_schema), + errors=errors if errors else {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + if self._async_current_entries(): + _LOGGER.warning("Only one configuration of abode is allowed.") + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_user(import_config) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py new file mode 100644 index 00000000000..35e74e154cf --- /dev/null +++ b/homeassistant/components/abode/const.py @@ -0,0 +1,3 @@ +"""Constants for the Abode Security System component.""" +DOMAIN = "abode" +ATTRIBUTION = "Data provided by goabode.com" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 13d46c53f73..ebe59ee45c7 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -5,26 +5,27 @@ import abodepy.helpers.constants as CONST from homeassistant.components.cover import CoverDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode cover devices.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - if data.is_excluded(device): - continue - devices.append(AbodeCover(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeCover(AbodeDevice, CoverDevice): diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 6551cba2ef1..163982d040e 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -18,30 +18,27 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin, ) -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode light devices.""" - - data = hass.data[ABODE_DOMAIN] - - device_types = [CONST.TYPE_LIGHT, CONST.TYPE_SWITCH] + data = hass.data[DOMAIN] devices = [] - # Get all regular lights that are not excluded or switches marked as lights - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device) or not data.is_light(device): - continue - + for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): devices.append(AbodeLight(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeLight(AbodeDevice, Light): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index ff069751605..11f792f88fd 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,30 +1,31 @@ -"""Support for Abode Security System locks.""" +"""Support for the Abode Security System locks.""" import logging import abodepy.helpers.constants as CONST from homeassistant.components.lock import LockDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode lock devices.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - if data.is_excluded(device): - continue - devices.append(AbodeLock(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeLock(AbodeDevice, LockDevice): diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 793c19cc466..8316691f701 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -1,10 +1,13 @@ { "domain": "abode", "name": "Abode", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ - "abodepy==0.15.0" + "abodepy==0.16.5" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@shred86" + ] +} \ No newline at end of file diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index fca32b8dc43..e25921f295f 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -9,7 +9,8 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ) -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,22 +22,22 @@ SENSOR_TYPES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for an Abode device.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): - if data.is_excluded(device): - continue - for sensor_type in SENSOR_TYPES: devices.append(AbodeSensor(data, device, sensor_type)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeSensor(AbodeDevice): diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json new file mode 100644 index 00000000000..bf7e768f6e3 --- /dev/null +++ b/homeassistant/components/abode/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Abode", + "step": { + "user": { + "title": "Fill in your Abode login information", + "data": { + "username": "Email Address", + "password": "Password" + } + } + }, + "error": { + "identifier_exists": "Account already registered.", + "invalid_credentials": "Invalid credentials.", + "connection_error": "Unable to connect to Abode." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Abode is allowed." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 4192ebb4485..7bd7f394d30 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -6,37 +6,32 @@ import abodepy.helpers.timeline as TIMELINE from homeassistant.components.switch import SwitchDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode switch devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode switch devices.""" + data = hass.data[DOMAIN] devices = [] - # Get all regular switches that are not excluded or marked as lights for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): - if data.is_excluded(device) or data.is_light(device): - continue - devices.append(AbodeSwitch(data, device)) - # Get all Abode automations that can be enabled/disabled for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): - if data.is_automation_excluded(automation): - continue - devices.append( AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) ) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeSwitch(AbodeDevice, SwitchDevice): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4a4effc36ce..664e83fba33 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest # fmt: off FLOWS = [ + "abode", "adguard", "airly", "ambiclimate", diff --git a/requirements_all.txt b/requirements_all.txt index a4623fe8bfb..9a9540c8747 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -103,7 +103,7 @@ WazeRouteCalculator==0.10 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.15.0 +abodepy==0.16.5 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c9f80e408c..4c3a6e7721b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,6 +45,9 @@ RtmAPI==0.7.2 # homeassistant.components.yessssms YesssSMS==0.4.1 +# homeassistant.components.abode +abodepy==0.16.5 + # homeassistant.components.androidtv adb-shell==0.0.4 diff --git a/tests/components/abode/__init__.py b/tests/components/abode/__init__.py new file mode 100644 index 00000000000..a34320c21de --- /dev/null +++ b/tests/components/abode/__init__.py @@ -0,0 +1 @@ +"""Tests for the Abode component.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py new file mode 100644 index 00000000000..c3f5d170767 --- /dev/null +++ b/tests/components/abode/test_config_flow.py @@ -0,0 +1,120 @@ +"""Tests for the Abode config flow.""" +from unittest.mock import patch + +from abodepy.exceptions import AbodeAuthenticationException + +from homeassistant import data_entry_flow +from homeassistant.components.abode import config_flow +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.common import MockConfigEntry + +CONF_POLLING = "polling" + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_one_config_allowed(hass): + """Test that only one Abode configuration is allowed.""" + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + MockConfigEntry( + domain="abode", + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ).add_to_hass(hass) + + step_user_result = await flow.async_step_user() + + assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step_user_result["reason"] == "single_instance_allowed" + + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + import_config_result = await flow.async_step_import(conf) + + assert import_config_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert import_config_result["reason"] == "single_instance_allowed" + + +async def test_invalid_credentials(hass): + """Test that invalid credentials throws an error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((400, "auth error")), + ): + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "invalid_credentials"} + + +async def test_connection_error(hass): + """Test other than invalid credentials throws an error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((500, "connection error")), + ): + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "connection_error"} + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch("homeassistant.components.abode.config_flow.Abode"): + result = await flow.async_step_import(import_config=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await flow.async_step_user(user_input=result["data"]) + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch("homeassistant.components.abode.config_flow.Abode"): + result = await flow.async_step_user(user_input=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + }