* Remove unnecessary exception re-wraps * Preserve exception chains on re-raise We slap "from cause" to almost all possible cases here. In some cases it could conceivably be better to do "from None" if we really want to hide the cause. However those should be in the minority, and "from cause" should be an improvement over the corresponding raise without a "from" in all cases anyway. The only case where we raise from None here is in plex, where the exception for an original invalid SSL cert is not the root cause for failure to validate a newly fetched one. Follow local convention on exception variable names if there is a consistent one, otherwise `err` to match with majority of codebase. * Fix mistaken re-wrap in homematicip_cloud/hap.py Missed the difference between HmipConnectionError and HmipcConnectionError. * Do not hide original error on plex new cert validation error Original is not the cause for the new one, but showing old in the traceback is useful nevertheless.
405 lines
12 KiB
Python
405 lines
12 KiB
Python
"""Support for the Abode Security System."""
|
|
from asyncio import gather
|
|
from copy import deepcopy
|
|
from functools import partial
|
|
|
|
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_ENTITY_ID,
|
|
ATTR_TIME,
|
|
CONF_PASSWORD,
|
|
CONF_USERNAME,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
)
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
from .const import ATTRIBUTION, DEFAULT_CACHEDB, DOMAIN, LOGGER
|
|
|
|
CONF_POLLING = "polling"
|
|
|
|
SERVICE_SETTINGS = "change_setting"
|
|
SERVICE_CAPTURE_IMAGE = "capture_image"
|
|
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
|
|
|
ATTR_DEVICE_ID = "device_id"
|
|
ATTR_DEVICE_NAME = "device_name"
|
|
ATTR_DEVICE_TYPE = "device_type"
|
|
ATTR_EVENT_CODE = "event_code"
|
|
ATTR_EVENT_NAME = "event_name"
|
|
ATTR_EVENT_TYPE = "event_type"
|
|
ATTR_EVENT_UTC = "event_utc"
|
|
ATTR_SETTING = "setting"
|
|
ATTR_USER_NAME = "user_name"
|
|
ATTR_APP_TYPE = "app_type"
|
|
ATTR_EVENT_BY = "event_by"
|
|
ATTR_VALUE = "value"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
vol.Optional(CONF_POLLING, default=False): cv.boolean,
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
CHANGE_SETTING_SCHEMA = vol.Schema(
|
|
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
|
)
|
|
|
|
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
|
|
|
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
|
|
|
ABODE_PLATFORMS = [
|
|
"alarm_control_panel",
|
|
"binary_sensor",
|
|
"lock",
|
|
"switch",
|
|
"cover",
|
|
"camera",
|
|
"light",
|
|
"sensor",
|
|
]
|
|
|
|
|
|
class AbodeSystem:
|
|
"""Abode System class."""
|
|
|
|
def __init__(self, abode, polling):
|
|
"""Initialize the system."""
|
|
self.abode = abode
|
|
self.polling = polling
|
|
self.entity_ids = set()
|
|
self.logout_listener = None
|
|
|
|
|
|
async def async_setup(hass, config):
|
|
"""Set up Abode integration."""
|
|
if DOMAIN not in config:
|
|
return True
|
|
|
|
conf = config[DOMAIN]
|
|
|
|
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)
|
|
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))
|
|
raise ConfigEntryNotReady from ex
|
|
|
|
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_AUTOMATION)
|
|
|
|
tasks = []
|
|
|
|
for platform in ABODE_PLATFORMS:
|
|
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
|
|
|
|
|
|
def setup_hass_services(hass):
|
|
"""Home Assistant services."""
|
|
|
|
def change_setting(call):
|
|
"""Change an Abode system setting."""
|
|
setting = call.data.get(ATTR_SETTING)
|
|
value = call.data.get(ATTR_VALUE)
|
|
|
|
try:
|
|
hass.data[DOMAIN].abode.set_setting(setting, value)
|
|
except AbodeException as ex:
|
|
LOGGER.warning(ex)
|
|
|
|
def capture_image(call):
|
|
"""Capture a new image."""
|
|
entity_ids = call.data.get(ATTR_ENTITY_ID)
|
|
|
|
target_entities = [
|
|
entity_id
|
|
for entity_id in hass.data[DOMAIN].entity_ids
|
|
if entity_id in entity_ids
|
|
]
|
|
|
|
for entity_id in target_entities:
|
|
signal = f"abode_camera_capture_{entity_id}"
|
|
dispatcher_send(hass, signal)
|
|
|
|
def trigger_automation(call):
|
|
"""Trigger an Abode automation."""
|
|
entity_ids = call.data.get(ATTR_ENTITY_ID)
|
|
|
|
target_entities = [
|
|
entity_id
|
|
for entity_id in hass.data[DOMAIN].entity_ids
|
|
if entity_id in entity_ids
|
|
]
|
|
|
|
for entity_id in target_entities:
|
|
signal = f"abode_trigger_automation_{entity_id}"
|
|
dispatcher_send(hass, signal)
|
|
|
|
hass.services.register(
|
|
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
|
|
)
|
|
|
|
hass.services.register(
|
|
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
|
)
|
|
|
|
hass.services.register(
|
|
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
|
|
)
|
|
|
|
|
|
async def setup_hass_events(hass):
|
|
"""Home Assistant start and stop callbacks."""
|
|
|
|
def logout(event):
|
|
"""Logout of Abode."""
|
|
if not hass.data[DOMAIN].polling:
|
|
hass.data[DOMAIN].abode.events.stop()
|
|
|
|
hass.data[DOMAIN].abode.logout()
|
|
LOGGER.info("Logged out of Abode")
|
|
|
|
if not hass.data[DOMAIN].polling:
|
|
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start)
|
|
|
|
hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once(
|
|
EVENT_HOMEASSISTANT_STOP, logout
|
|
)
|
|
|
|
|
|
def setup_abode_events(hass):
|
|
"""Event callbacks."""
|
|
|
|
def event_callback(event, event_json):
|
|
"""Handle an event callback from Abode."""
|
|
data = {
|
|
ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ""),
|
|
ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ""),
|
|
ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ""),
|
|
ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ""),
|
|
ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ""),
|
|
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""),
|
|
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""),
|
|
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""),
|
|
ATTR_APP_TYPE: event_json.get(ATTR_APP_TYPE, ""),
|
|
ATTR_EVENT_BY: event_json.get(ATTR_EVENT_BY, ""),
|
|
ATTR_DATE: event_json.get(ATTR_DATE, ""),
|
|
ATTR_TIME: event_json.get(ATTR_TIME, ""),
|
|
}
|
|
|
|
hass.bus.fire(event, data)
|
|
|
|
events = [
|
|
TIMELINE.ALARM_GROUP,
|
|
TIMELINE.ALARM_END_GROUP,
|
|
TIMELINE.PANEL_FAULT_GROUP,
|
|
TIMELINE.PANEL_RESTORE_GROUP,
|
|
TIMELINE.AUTOMATION_GROUP,
|
|
TIMELINE.DISARM_GROUP,
|
|
TIMELINE.ARM_GROUP,
|
|
TIMELINE.ARM_FAULT_GROUP,
|
|
TIMELINE.TEST_GROUP,
|
|
TIMELINE.CAPTURE_GROUP,
|
|
TIMELINE.DEVICE_GROUP,
|
|
]
|
|
|
|
for event in events:
|
|
hass.data[DOMAIN].abode.events.add_event_callback(
|
|
event, partial(event_callback, event)
|
|
)
|
|
|
|
|
|
class AbodeEntity(Entity):
|
|
"""Representation of an Abode entity."""
|
|
|
|
def __init__(self, data):
|
|
"""Initialize Abode entity."""
|
|
self._data = data
|
|
self._available = True
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return the available state."""
|
|
return self._available
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Return the polling state."""
|
|
return self._data.polling
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Subscribe to Abode connection status updates."""
|
|
await self.hass.async_add_executor_job(
|
|
self._data.abode.events.add_connection_status_callback,
|
|
self.unique_id,
|
|
self._update_connection_status,
|
|
)
|
|
|
|
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
|
|
|
|
async def async_will_remove_from_hass(self):
|
|
"""Unsubscribe from Abode connection status updates."""
|
|
await self.hass.async_add_executor_job(
|
|
self._data.abode.events.remove_connection_status_callback, self.unique_id
|
|
)
|
|
|
|
def _update_connection_status(self):
|
|
"""Update the entity available property."""
|
|
self._available = self._data.abode.events.connected
|
|
self.schedule_update_ha_state()
|
|
|
|
|
|
class AbodeDevice(AbodeEntity):
|
|
"""Representation of an Abode device."""
|
|
|
|
def __init__(self, data, device):
|
|
"""Initialize Abode device."""
|
|
super().__init__(data)
|
|
self._device = device
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Subscribe to device events."""
|
|
await super().async_added_to_hass()
|
|
await self.hass.async_add_executor_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."""
|
|
await super().async_will_remove_from_hass()
|
|
await self.hass.async_add_executor_job(
|
|
self._data.abode.events.remove_all_device_callbacks, self._device.device_id
|
|
)
|
|
|
|
def update(self):
|
|
"""Update device state."""
|
|
self._device.refresh()
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device."""
|
|
return self._device.name
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the state attributes."""
|
|
return {
|
|
ATTR_ATTRIBUTION: ATTRIBUTION,
|
|
"device_id": self._device.device_id,
|
|
"battery_low": self._device.battery_low,
|
|
"no_response": self._device.no_response,
|
|
"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()
|
|
|
|
|
|
class AbodeAutomation(AbodeEntity):
|
|
"""Representation of an Abode automation."""
|
|
|
|
def __init__(self, data, automation):
|
|
"""Initialize for Abode automation."""
|
|
super().__init__(data)
|
|
self._automation = automation
|
|
|
|
def update(self):
|
|
"""Update automation state."""
|
|
self._automation.refresh()
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the automation."""
|
|
return self._automation.name
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the state attributes."""
|
|
return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"}
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return a unique ID to use for this automation."""
|
|
return self._automation.automation_id
|