Create zone bypass switches for DSC panels (#63200)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
ufodone 2022-01-25 19:21:58 -08:00 committed by GitHub
parent 3c1d1bd060
commit 10efeb2935
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 167 additions and 52 deletions

View File

@ -39,6 +39,9 @@ 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
@ -50,6 +53,7 @@ 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(
{
@ -66,7 +70,7 @@ CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PANEL_TYPE): vol.All(
cv.string, vol.In(["HONEYWELL", "DSC"])
cv.string, vol.In([PANEL_TYPE_HONEYWELL, PANEL_TYPE_DSC])
),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASS): cv.string,
@ -137,14 +141,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.data[DATA_EVL] = controller
@callback
def login_fail_callback(data):
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 connection_fail_callback(data):
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():
@ -152,7 +156,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
sync_connect.set_result(True)
@callback
def connection_success_callback(data):
def async_connection_success_callback(data):
"""Handle a successful connection."""
_LOGGER.info("Established a connection with the Envisalink")
if not sync_connect.done():
@ -160,23 +164,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
sync_connect.set_result(True)
@callback
def zones_updated_callback(data):
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 alarm_data_updated_callback(data):
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 partition_updated_callback(data):
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 async_zone_bypass_update(data):
"""Handle zone bypass status updates."""
_LOGGER.debug("Envisalink sent a zone bypass update event. Updating zones")
async_dispatcher_send(hass, SIGNAL_ZONE_BYPASS_UPDATE, data)
@callback
def stop_envisalink(event):
"""Shutdown envisalink connection and thread on exit."""
@ -189,13 +199,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
partition = call.data.get(ATTR_PARTITION)
controller.command_output(code, partition, custom_function)
controller.callback_zone_timer_dump = zones_updated_callback
controller.callback_zone_state_change = zones_updated_callback
controller.callback_partition_state_change = partition_updated_callback
controller.callback_keypad_update = alarm_data_updated_callback
controller.callback_login_failure = login_fail_callback
controller.callback_login_timeout = connection_fail_callback
controller.callback_login_success = connection_success_callback
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
controller.callback_zone_bypass_update = async_zone_bypass_update
_LOGGER.info("Start envisalink")
controller.start()
@ -229,6 +240,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config
)
)
# Only DSC panels support getting zone bypass status
if panel_type == PANEL_TYPE_DSC:
hass.async_create_task(
async_load_platform(
hass, "switch", "envisalink", {CONF_ZONES: zones}, config
)
)
hass.services.async_register(
DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA

View File

@ -68,39 +68,39 @@ async def async_setup_platform(
code = discovery_info[CONF_CODE]
panic_type = discovery_info[CONF_PANIC]
devices = []
entities = []
for part_num in configured_partitions:
device_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
device = EnvisalinkAlarm(
entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
entity = EnvisalinkAlarm(
hass,
part_num,
device_config_data[CONF_PARTITIONNAME],
entity_config_data[CONF_PARTITIONNAME],
code,
panic_type,
hass.data[DATA_EVL].alarm_state["partition"][part_num],
hass.data[DATA_EVL],
)
devices.append(device)
entities.append(entity)
async_add_entities(devices)
async_add_entities(entities)
@callback
def alarm_keypress_handler(service: ServiceCall) -> None:
def async_alarm_keypress_handler(service: ServiceCall) -> None:
"""Map services to methods on Alarm."""
entity_ids = service.data[ATTR_ENTITY_ID]
keypress = service.data[ATTR_KEYPRESS]
target_devices = [
device for device in devices if device.entity_id in entity_ids
target_entities = [
entity for entity in entities if entity.entity_id in entity_ids
]
for device in target_devices:
device.async_alarm_keypress(keypress)
for entity in target_entities:
entity.async_alarm_keypress(keypress)
hass.services.async_register(
DOMAIN,
SERVICE_ALARM_KEYPRESS,
alarm_keypress_handler,
async_alarm_keypress_handler,
schema=ALARM_KEYPRESS_SCHEMA,
)
@ -123,17 +123,17 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
"""Register callbacks."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback
self.hass, SIGNAL_KEYPAD_UPDATE, self.async_update_callback
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback
self.hass, SIGNAL_PARTITION_UPDATE, self.async_update_callback
)
)
@callback
def _update_callback(self, partition):
def async_update_callback(self, partition):
"""Update Home Assistant state, if needed."""
if partition is None or int(partition) == self._partition_number:
self.async_write_ha_state()

View File

@ -30,25 +30,25 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Envisalink binary sensor devices."""
"""Set up the Envisalink binary sensor entities."""
if not discovery_info:
return
configured_zones = discovery_info["zones"]
devices = []
entities = []
for zone_num in configured_zones:
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
device = EnvisalinkBinarySensor(
entity_config_data = ZONE_SCHEMA(configured_zones[zone_num])
entity = EnvisalinkBinarySensor(
hass,
zone_num,
device_config_data[CONF_ZONENAME],
device_config_data[CONF_ZONETYPE],
entity_config_data[CONF_ZONENAME],
entity_config_data[CONF_ZONETYPE],
hass.data[DATA_EVL].alarm_state["zone"][zone_num],
hass.data[DATA_EVL],
)
devices.append(device)
entities.append(entity)
async_add_entities(devices)
async_add_entities(entities)
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity):
@ -64,7 +64,11 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity):
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_UPDATE, self.async_update_callback
)
)
@property
def extra_state_attributes(self):
@ -102,7 +106,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity):
return self._zone_type
@callback
def _update_callback(self, zone):
def async_update_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self.async_write_ha_state()

View File

@ -2,7 +2,7 @@
"domain": "envisalink",
"name": "Envisalink",
"documentation": "https://www.home-assistant.io/integrations/envisalink",
"requirements": ["pyenvisalink==4.0"],
"requirements": ["pyenvisalink==4.3"],
"codeowners": [],
"iot_class": "local_push"
}

View File

@ -27,25 +27,25 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Perform the setup for Envisalink sensor devices."""
"""Perform the setup for Envisalink sensor entities."""
if not discovery_info:
return
configured_partitions = discovery_info["partitions"]
devices = []
entities = []
for part_num in configured_partitions:
device_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
device = EnvisalinkSensor(
entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
entity = EnvisalinkSensor(
hass,
device_config_data[CONF_PARTITIONNAME],
entity_config_data[CONF_PARTITIONNAME],
part_num,
hass.data[DATA_EVL].alarm_state["partition"][part_num],
hass.data[DATA_EVL],
)
devices.append(device)
entities.append(entity)
async_add_entities(devices)
async_add_entities(entities)
class EnvisalinkSensor(EnvisalinkDevice, SensorEntity):
@ -61,9 +61,15 @@ class EnvisalinkSensor(EnvisalinkDevice, SensorEntity):
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
async_dispatcher_connect(
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_KEYPAD_UPDATE, self.async_update_callback
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_PARTITION_UPDATE, self.async_update_callback
)
)
@property
@ -82,7 +88,7 @@ class EnvisalinkSensor(EnvisalinkDevice, SensorEntity):
return self._info["status"]
@callback
def _update_callback(self, partition):
def async_update_callback(self, partition):
"""Update the partition state in HA, if needed."""
if partition is None or int(partition) == self._partition_number:
self.async_write_ha_state()

View File

@ -0,0 +1,87 @@
"""Support for Envisalink zone bypass switches."""
from __future__ import annotations
import logging
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
CONF_ZONENAME,
DATA_EVL,
SIGNAL_ZONE_BYPASS_UPDATE,
ZONE_SCHEMA,
EnvisalinkDevice,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Envisalink switch entities."""
if not discovery_info:
return
configured_zones = discovery_info["zones"]
entities = []
for zone_num in configured_zones:
entity_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_name = f"{entity_config_data[CONF_ZONENAME]}_bypass"
_LOGGER.debug("Setting up zone_bypass switch: %s", zone_name)
entity = EnvisalinkSwitch(
hass,
zone_num,
zone_name,
hass.data[DATA_EVL].alarm_state["zone"][zone_num],
hass.data[DATA_EVL],
)
entities.append(entity)
async_add_entities(entities)
class EnvisalinkSwitch(EnvisalinkDevice, SwitchEntity):
"""Representation of an Envisalink switch."""
def __init__(self, hass, zone_number, zone_name, info, controller):
"""Initialize the switch."""
self._zone_number = zone_number
super().__init__(zone_name, info, controller)
async def async_added_to_hass(self):
"""Register callbacks."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_BYPASS_UPDATE, self.async_update_callback
)
)
@property
def is_on(self):
"""Return the boolean response if the zone is bypassed."""
return self._info["bypassed"]
async def async_turn_on(self, **kwargs):
"""Send the bypass keypress sequence to toggle the zone bypass."""
self._controller.toggle_zone_bypass(self._zone_number)
async def async_turn_off(self, **kwargs):
"""Send the bypass keypress sequence to toggle the zone bypass."""
self._controller.toggle_zone_bypass(self._zone_number)
@callback
def async_update_callback(self, bypass_map):
"""Update the zone bypass state in HA, if needed."""
if bypass_map is None or self._zone_number in bypass_map:
_LOGGER.debug("Bypass state changed for zone %d", self._zone_number)
self.async_write_ha_state()

View File

@ -1500,7 +1500,7 @@ pyeight==0.2.0
pyemby==1.8
# homeassistant.components.envisalink
pyenvisalink==4.0
pyenvisalink==4.3
# homeassistant.components.ephember
pyephember==0.3.1