263 lines
8.6 KiB
Python
263 lines
8.6 KiB
Python
"""Support for Envisalink devices."""
|
|
import asyncio
|
|
import logging
|
|
|
|
from pyenvisalink import EnvisalinkAlarmPanel
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import (
|
|
CONF_CODE,
|
|
CONF_HOST,
|
|
CONF_TIMEOUT,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
Platform,
|
|
)
|
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.discovery import async_load_platform
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DOMAIN = "envisalink"
|
|
|
|
DATA_EVL = "envisalink"
|
|
|
|
CONF_EVL_KEEPALIVE = "keepalive_interval"
|
|
CONF_EVL_PORT = "port"
|
|
CONF_EVL_VERSION = "evl_version"
|
|
CONF_PANEL_TYPE = "panel_type"
|
|
CONF_PANIC = "panic_type"
|
|
CONF_PARTITIONNAME = "name"
|
|
CONF_PARTITIONS = "partitions"
|
|
CONF_PASS = "password"
|
|
CONF_USERNAME = "user_name"
|
|
CONF_ZONEDUMP_INTERVAL = "zonedump_interval"
|
|
CONF_ZONENAME = "name"
|
|
CONF_ZONES = "zones"
|
|
CONF_ZONETYPE = "type"
|
|
|
|
PANEL_TYPE_HONEYWELL = "HONEYWELL"
|
|
PANEL_TYPE_DSC = "DSC"
|
|
|
|
DEFAULT_PORT = 4025
|
|
DEFAULT_EVL_VERSION = 3
|
|
DEFAULT_KEEPALIVE = 60
|
|
DEFAULT_ZONEDUMP_INTERVAL = 30
|
|
DEFAULT_ZONETYPE = "opening"
|
|
DEFAULT_PANIC = "Police"
|
|
DEFAULT_TIMEOUT = 10
|
|
|
|
SIGNAL_ZONE_UPDATE = "envisalink.zones_updated"
|
|
SIGNAL_PARTITION_UPDATE = "envisalink.partition_updated"
|
|
SIGNAL_KEYPAD_UPDATE = "envisalink.keypad_updated"
|
|
SIGNAL_ZONE_BYPASS_UPDATE = "envisalink.zone_bypass_updated"
|
|
|
|
ZONE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(CONF_ZONENAME): cv.string,
|
|
vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string,
|
|
}
|
|
)
|
|
|
|
PARTITION_SCHEMA = vol.Schema({vol.Required(CONF_PARTITIONNAME): cv.string})
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_HOST): cv.string,
|
|
vol.Required(CONF_PANEL_TYPE): vol.All(
|
|
cv.string, vol.In([PANEL_TYPE_HONEYWELL, PANEL_TYPE_DSC])
|
|
),
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
vol.Required(CONF_PASS): cv.string,
|
|
vol.Optional(CONF_CODE): cv.string,
|
|
vol.Optional(CONF_PANIC, default=DEFAULT_PANIC): cv.string,
|
|
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
|
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
|
|
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): cv.port,
|
|
vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION): vol.All(
|
|
vol.Coerce(int), vol.Range(min=3, max=4)
|
|
),
|
|
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(
|
|
vol.Coerce(int), vol.Range(min=15)
|
|
),
|
|
vol.Optional(
|
|
CONF_ZONEDUMP_INTERVAL, default=DEFAULT_ZONEDUMP_INTERVAL
|
|
): vol.Coerce(int),
|
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
SERVICE_CUSTOM_FUNCTION = "invoke_custom_function"
|
|
ATTR_CUSTOM_FUNCTION = "pgm"
|
|
ATTR_PARTITION = "partition"
|
|
|
|
SERVICE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_CUSTOM_FUNCTION): cv.string,
|
|
vol.Required(ATTR_PARTITION): cv.string,
|
|
}
|
|
)
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up for Envisalink devices."""
|
|
conf = config[DOMAIN]
|
|
|
|
host = conf.get(CONF_HOST)
|
|
port = conf.get(CONF_EVL_PORT)
|
|
code = conf.get(CONF_CODE)
|
|
panel_type = conf.get(CONF_PANEL_TYPE)
|
|
panic_type = conf.get(CONF_PANIC)
|
|
version = conf.get(CONF_EVL_VERSION)
|
|
user = conf.get(CONF_USERNAME)
|
|
password = conf.get(CONF_PASS)
|
|
keep_alive = conf.get(CONF_EVL_KEEPALIVE)
|
|
zone_dump = conf.get(CONF_ZONEDUMP_INTERVAL)
|
|
zones = conf.get(CONF_ZONES)
|
|
partitions = conf.get(CONF_PARTITIONS)
|
|
connection_timeout = conf.get(CONF_TIMEOUT)
|
|
sync_connect: asyncio.Future[bool] = hass.loop.create_future()
|
|
|
|
controller = EnvisalinkAlarmPanel(
|
|
host,
|
|
port,
|
|
panel_type,
|
|
version,
|
|
user,
|
|
password,
|
|
zone_dump,
|
|
keep_alive,
|
|
hass.loop,
|
|
connection_timeout,
|
|
False,
|
|
)
|
|
hass.data[DATA_EVL] = controller
|
|
|
|
@callback
|
|
def async_login_fail_callback(data):
|
|
"""Handle when the evl rejects our login."""
|
|
_LOGGER.error("The Envisalink rejected your credentials")
|
|
if not sync_connect.done():
|
|
sync_connect.set_result(False)
|
|
|
|
@callback
|
|
def async_connection_fail_callback(data):
|
|
"""Network failure callback."""
|
|
_LOGGER.error("Could not establish a connection with the Envisalink- retrying")
|
|
if not sync_connect.done():
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
|
sync_connect.set_result(True)
|
|
|
|
@callback
|
|
def async_connection_success_callback(data):
|
|
"""Handle a successful connection."""
|
|
_LOGGER.info("Established a connection with the Envisalink")
|
|
if not sync_connect.done():
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
|
sync_connect.set_result(True)
|
|
|
|
@callback
|
|
def async_zones_updated_callback(data):
|
|
"""Handle zone timer updates."""
|
|
_LOGGER.debug("Envisalink sent a zone update event. Updating zones")
|
|
async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data)
|
|
|
|
@callback
|
|
def async_alarm_data_updated_callback(data):
|
|
"""Handle non-alarm based info updates."""
|
|
_LOGGER.debug("Envisalink sent new alarm info. Updating alarms")
|
|
async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data)
|
|
|
|
@callback
|
|
def async_partition_updated_callback(data):
|
|
"""Handle partition changes thrown by evl (including alarms)."""
|
|
_LOGGER.debug("The envisalink sent a partition update event")
|
|
async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
|
|
|
|
@callback
|
|
def stop_envisalink(event):
|
|
"""Shutdown envisalink connection and thread on exit."""
|
|
_LOGGER.info("Shutting down Envisalink")
|
|
controller.stop()
|
|
|
|
async def handle_custom_function(call: ServiceCall) -> None:
|
|
"""Handle custom/PGM service."""
|
|
custom_function = call.data.get(ATTR_CUSTOM_FUNCTION)
|
|
partition = call.data.get(ATTR_PARTITION)
|
|
controller.command_output(code, partition, custom_function)
|
|
|
|
controller.callback_zone_timer_dump = async_zones_updated_callback
|
|
controller.callback_zone_state_change = async_zones_updated_callback
|
|
controller.callback_partition_state_change = async_partition_updated_callback
|
|
controller.callback_keypad_update = async_alarm_data_updated_callback
|
|
controller.callback_login_failure = async_login_fail_callback
|
|
controller.callback_login_timeout = async_connection_fail_callback
|
|
controller.callback_login_success = async_connection_success_callback
|
|
|
|
_LOGGER.info("Start envisalink")
|
|
controller.start()
|
|
|
|
if not await sync_connect:
|
|
return False
|
|
|
|
# Load sub-components for Envisalink
|
|
if partitions:
|
|
hass.async_create_task(
|
|
async_load_platform(
|
|
hass,
|
|
Platform.ALARM_CONTROL_PANEL,
|
|
"envisalink",
|
|
{CONF_PARTITIONS: partitions, CONF_CODE: code, CONF_PANIC: panic_type},
|
|
config,
|
|
)
|
|
)
|
|
hass.async_create_task(
|
|
async_load_platform(
|
|
hass,
|
|
Platform.SENSOR,
|
|
"envisalink",
|
|
{CONF_PARTITIONS: partitions, CONF_CODE: code},
|
|
config,
|
|
)
|
|
)
|
|
if zones:
|
|
hass.async_create_task(
|
|
async_load_platform(
|
|
hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config
|
|
)
|
|
)
|
|
|
|
# Zone bypass switches are not currently created due to an issue with some panels.
|
|
# These switches will be re-added in the future after some further refactoring of the integration.
|
|
|
|
hass.services.async_register(
|
|
DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
class EnvisalinkDevice(Entity):
|
|
"""Representation of an Envisalink device."""
|
|
|
|
_attr_should_poll = False
|
|
|
|
def __init__(self, name, info, controller):
|
|
"""Initialize the device."""
|
|
self._controller = controller
|
|
self._info = info
|
|
self._name = name
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device."""
|
|
return self._name
|