Add abode config entries and device registry (#26699)

* Adds support for config entries and device registry

* Fixing string formatting for logger

* Add unit test for abode config flow

* Fix for lights, only allow one config, add ability to unload entry

* Fix for subscribing to hass_events on adding abode component

* Several fixes from code review

* Several fixes from second code review

* Several fixes from third code review

* Update documentation url to fix branch conflict

* Fixes config flow and removes unused constants

* Fix for switches, polling entry option, improved tests

* Update .coveragerc, disable pylint W0611, remove polling from UI

* Multiple fixes and edits to adhere to style guidelines

* Removed unique_id

* Minor correction for formatting error in rebase

* Resolves issue causing CI to fail

* Bump abodepy version

* Add remove device callback and minor clean up

* Fix incorrect method name

* Docstring edits

* Fix duplicate import issues from rebase

* Add logout_listener attribute to AbodeSystem

* Add additional test for complete coverage
This commit is contained in:
shred86 2019-10-13 11:01:04 -07:00 committed by Martin Hjelmare
parent d96cd4c4ea
commit 1774a1427b
20 changed files with 433 additions and 194 deletions

View File

@ -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

View File

@ -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

View File

@ -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: {}<br />"
"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()

View File

@ -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."""

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -0,0 +1,3 @@
"""Constants for the Abode Security System component."""
DOMAIN = "abode"
ATTRIBUTION = "Data provided by goabode.com"

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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"
]
}

View File

@ -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):

View File

@ -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."
}
}
}

View File

@ -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):

View File

@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest
# fmt: off
FLOWS = [
"abode",
"adguard",
"airly",
"ambiclimate",

View File

@ -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

View File

@ -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

View File

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

View File

@ -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,
}