mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
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:
parent
d96cd4c4ea
commit
1774a1427b
10
.coveragerc
10
.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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
79
homeassistant/components/abode/config_flow.py
Normal file
79
homeassistant/components/abode/config_flow.py
Normal 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)
|
3
homeassistant/components/abode/const.py
Normal file
3
homeassistant/components/abode/const.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Constants for the Abode Security System component."""
|
||||
DOMAIN = "abode"
|
||||
ATTRIBUTION = "Data provided by goabode.com"
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
@ -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):
|
||||
|
22
homeassistant/components/abode/strings.json
Normal file
22
homeassistant/components/abode/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
@ -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):
|
||||
|
@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest
|
||||
# fmt: off
|
||||
|
||||
FLOWS = [
|
||||
"abode",
|
||||
"adguard",
|
||||
"airly",
|
||||
"ambiclimate",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
1
tests/components/abode/__init__.py
Normal file
1
tests/components/abode/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Abode component."""
|
120
tests/components/abode/test_config_flow.py
Normal file
120
tests/components/abode/test_config_flow.py
Normal 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,
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user