Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add abode config entries and device registry #26699

Merged
merged 23 commits into from
Oct 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
086b4e8
Adds support for config entries and device registry
shred86 Sep 17, 2019
f8b6dad
Fixing string formatting for logger
shred86 Sep 18, 2019
d51a61c
Add unit test for abode config flow
shred86 Sep 29, 2019
31a44cd
Fix for lights, only allow one config, add ability to unload entry
shred86 Sep 30, 2019
d3d6096
Fix for subscribing to hass_events on adding abode component
shred86 Oct 1, 2019
0a48428
Several fixes from code review
shred86 Oct 5, 2019
246e1aa
Several fixes from second code review
shred86 Oct 5, 2019
c788ce7
Several fixes from third code review
shred86 Oct 6, 2019
c78a069
Update documentation url to fix branch conflict
shred86 Oct 6, 2019
65beaca
Fixes config flow and removes unused constants
shred86 Oct 6, 2019
c782105
Fix for switches, polling entry option, improved tests
shred86 Oct 6, 2019
d7a92aa
Update .coveragerc, disable pylint W0611, remove polling from UI
shred86 Oct 6, 2019
34189c0
Multiple fixes and edits to adhere to style guidelines
shred86 Oct 9, 2019
8d6cf4f
Removed unique_id
shred86 Oct 9, 2019
7125088
Minor correction for formatting error in rebase
shred86 Oct 10, 2019
653f534
Resolves issue causing CI to fail
shred86 Oct 10, 2019
af2e6dc
Bump abodepy version
shred86 Oct 11, 2019
f40fad4
Add remove device callback and minor clean up
shred86 Oct 12, 2019
57e4b5a
Fix incorrect method name
shred86 Oct 12, 2019
fab1b16
Docstring edits
shred86 Oct 12, 2019
f3e98f5
Fix duplicate import issues from rebase
shred86 Oct 12, 2019
9f01395
Add logout_listener attribute to AbodeSystem
shred86 Oct 13, 2019
2b75f78
Add additional test for complete coverage
shred86 Oct 13, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
180 changes: 95 additions & 85 deletions homeassistant/components/abode/__init__.py
Original file line number Diff line number Diff line change
@@ -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

_LOGGER = logging.getLogger(__name__)
from .const import ATTRIBUTION, DOMAIN

ATTRIBUTION = "Data provided by goabode.com"
_LOGGER = logging.getLogger(__name__)

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"
Expand All @@ -67,10 +54,7 @@
{
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,
}
)
},
Expand Down Expand Up @@ -100,73 +84,80 @@
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 = []
self.logout_listener = None

shred86 marked this conversation as resolved.
Show resolved Hide resolved
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
async def async_setup(hass, config):
"""Set up Abode integration."""
if DOMAIN not in config:
return True

def is_light(self, device):
"""Check if a switch device is configured as a light."""
conf = config[DOMAIN]

return device.generic_type == CONST.TYPE_LIGHT or (
device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf)
)
)

return True

def setup(hass, config):
"""Set up Abode component."""

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)
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)
shred86 marked this conversation as resolved.
Show resolved Hide resolved
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))
return False

hass.components.persistent_notification.create(
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
for platform in ABODE_PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return False

setup_hass_services(hass)
setup_hass_events(hass)
setup_abode_events(hass)
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

Expand Down Expand Up @@ -223,13 +214,9 @@ def trigger_quick_action(call):
)


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:
Expand All @@ -239,9 +226,11 @@ def logout(event):
_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(
shred86 marked this conversation as resolved.
Show resolved Hide resolved
EVENT_HOMEASSISTANT_STOP, logout
)


def setup_abode_events(hass):
Expand Down Expand Up @@ -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
Expand All @@ -319,6 +314,21 @@ def device_state_attributes(self):
"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()
Expand Down Expand Up @@ -353,7 +363,7 @@ def update(self):

@property
def name(self):
"""Return the name of the sensor."""
"""Return the name of the automation."""
return self._automation.name

@property
Expand All @@ -367,6 +377,6 @@ def device_state_attributes(self):
}

def _update_callback(self, device):
"""Update the device state."""
"""Update the automation state."""
self._automation.refresh()
self.schedule_update_ha_state()
28 changes: 11 additions & 17 deletions homeassistant/components/abode/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,31 @@
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):
"""Set up an alarm control panel for an Abode device."""
data = hass.data[ABODE_DOMAIN]
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Platform uses config entry setup."""
pass

alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)]

data.devices.extend(alarm_devices)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up an alarm control panel for an Abode device."""
data = hass.data[DOMAIN]

add_entities(alarm_devices)
async_add_entities(
[AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
shred86 marked this conversation as resolved.
Show resolved Hide resolved
)


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."""
Expand Down Expand Up @@ -65,11 +64,6 @@ def alarm_arm_away(self, code=None):
"""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."""
Expand Down
Loading