Skip to content

Commit

Permalink
Additional capabilities based on 1.0.123 firmware
Browse files Browse the repository at this point in the history
* Support arm away and arm home
* Added zone bypass switches
* Added panic buttons
  • Loading branch information
ufodone committed Feb 2, 2025
1 parent 69fa7d0 commit 10a53d2
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 31 deletions.
10 changes: 8 additions & 2 deletions custom_components/envisalink_new/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
)
from .helpers import find_yaml_info, generate_entity_setup_info, parse_range_string
from .models import EnvisalinkDevice
from .pyenvisalink.const import PANEL_TYPE_HONEYWELL, STATE_CHANGE_PARTITION
from .pyenvisalink.const import (
PANEL_TYPE_HONEYWELL,
PANEL_TYPE_UNO,
STATE_CHANGE_PARTITION,
)

SERVICE_ALARM_KEYPRESS = "alarm_keypress"
ATTR_KEYPRESS = "keypress"
Expand Down Expand Up @@ -124,7 +128,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
| AlarmControlPanelEntityFeature.TRIGGER
)

Expand Down Expand Up @@ -159,6 +162,9 @@ def __init__(
LOGGER.debug("Setting up alarm: %s", name)
super().__init__(name, controller, STATE_CHANGE_PARTITION, partition_number)

if self._controller.controller.panel_type is not PANEL_TYPE_UNO:
self._attr_supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT

async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
self._fixup_default_code()
Expand Down
26 changes: 22 additions & 4 deletions custom_components/envisalink_new/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,33 @@

from .const import DOMAIN
from .models import EnvisalinkDevice
from .pyenvisalink.const import PANEL_TYPE_DSC, PANEL_TYPE_HONEYWELL
from .pyenvisalink.const import PANEL_TYPE_DSC, PANEL_TYPE_HONEYWELL, PANEL_TYPE_UNO

_panel_buttons = [
{"type": "Fire", "label": {PANEL_TYPE_DSC: "Fire", PANEL_TYPE_HONEYWELL: "A"}},
{
"type": "Fire",
"label": {
PANEL_TYPE_DSC: "Fire",
PANEL_TYPE_HONEYWELL: "A",
PANEL_TYPE_UNO: "Fire",
},
},
{
"type": "Ambulance",
"label": {PANEL_TYPE_DSC: "Ambulance", PANEL_TYPE_HONEYWELL: "B"},
"label": {
PANEL_TYPE_DSC: "Ambulance",
PANEL_TYPE_HONEYWELL: "B",
PANEL_TYPE_UNO: "Medical Emergency",
},
},
{
"type": "Police",
"label": {
PANEL_TYPE_DSC: "Police",
PANEL_TYPE_HONEYWELL: "C",
PANEL_TYPE_UNO: "Silent Police Panic",
},
},
{"type": "Police", "label": {PANEL_TYPE_DSC: "Police", PANEL_TYPE_HONEYWELL: "C"}},
]


Expand Down
9 changes: 6 additions & 3 deletions custom_components/envisalink_new/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
)
from .helpers import extract_discovery_endpoint, parse_range_string
from .pyenvisalink.alarm_panel import EnvisalinkAlarmPanel
from .pyenvisalink.const import PANEL_TYPE_DSC, PANEL_TYPE_HONEYWELL
from .pyenvisalink.const import PANEL_TYPE_DSC, PANEL_TYPE_HONEYWELL, PANEL_TYPE_UNO


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
Expand Down Expand Up @@ -205,8 +205,8 @@ async def async_step_advanced(
): vol.Coerce(int),
}

# Add DSC-only options
if self.config_entry.data.get(CONF_PANEL_TYPE) == PANEL_TYPE_DSC:
# Zone bypass switches are available only for DSC and Uno systems
if self.config_entry.data.get(CONF_PANEL_TYPE) in [PANEL_TYPE_DSC, PANEL_TYPE_UNO]:
# Zone bypass switches are only available on DSC panels
options_schema[
vol.Optional(
Expand All @@ -217,6 +217,9 @@ async def async_step_advanced(
),
)
] = selector.BooleanSelector()

# Add DSC-only options
if self.config_entry.data.get(CONF_PANEL_TYPE) == PANEL_TYPE_DSC:
options_schema[
vol.Optional(
CONF_CODE_ARM_REQUIRED,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/envisalink_new/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,6 @@
DEFAULT_CODE_ARM_REQUIRED = {
PANEL_TYPE_DSC: False,
PANEL_TYPE_HONEYWELL: True,
PANEL_TYPE_UNO: True,
PANEL_TYPE_UNO: False,
None: True,
}
4 changes: 2 additions & 2 deletions custom_components/envisalink_new/pyenvisalink/alarm_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,12 +359,12 @@ async def panic_alarm(self, panic_type):
else:
_LOGGER.error(COMMAND_ERR)

async def toggle_zone_bypass(self, zone, partition):
async def bypass_zone(self, zone, partition, enable):
"""Public method to toggle a zone's bypass state."""
if not self._zoneBypassEnabled:
_LOGGER.error(COMMAND_ERR)
elif self._client:
await self._client.toggle_zone_bypass(zone, partition)
await self._client.bypass_zone(zone, partition, enable)
else:
_LOGGER.error(COMMAND_ERR)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async def panic_alarm(self, panicType):
"""Public method to raise a panic alarm."""
await self.queue_command(evl_Commands["Panic"], evl_PanicTypes[panicType])

async def toggle_zone_bypass(self, zone, partition):
async def bypass_zone(self, zone, partition, enable):
"""Public method to toggle a zone's bypass state."""
await self.keypresses_to_partition(partition, "*1%02d#" % zone)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ async def panic_alarm(self, panicType):
"""Public method to trigger the panic alarm."""
raise NotImplementedError()

async def toggle_zone_bypass(self, zone, partition):
async def bypass_zone(self, zone, partition, enable):
"""Public method to toggle a zone's bypass state."""
raise NotImplementedError()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class HoneywellClient(EnvisalinkClient):
def __init__(self, panel):
super().__init__(panel)
self._zoneTimers = {}
self._evl_ResponseTypes = evl_ResponseTypes

def detect(prompt):
"""Given the initial connection data, determine if this is a Honeywell panel."""
Expand Down Expand Up @@ -141,8 +142,8 @@ def parseHandler(self, rawInput):
_LOGGER.error("Unrecognized data recieved from the envisalink. Ignoring.")
return None
try:
cmd["handler"] = "handle_%s" % evl_ResponseTypes[code]["handler"]
cmd["state_change"] = evl_ResponseTypes[code].get("state_change", False)
cmd["handler"] = "handle_%s" % self._evl_ResponseTypes[code]["handler"]
cmd["state_change"] = self._evl_ResponseTypes[code].get("state_change", False)
except KeyError:
_LOGGER.warning(str.format("No handler defined in config for {0}, skipping...", code))

Expand Down
108 changes: 97 additions & 11 deletions custom_components/envisalink_new/pyenvisalink/uno_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,35 @@
import time

from .const import STATE_CHANGE_PARTITION, STATE_CHANGE_ZONE, STATE_CHANGE_ZONE_BYPASS
from .envisalink_base_client import EnvisalinkClient
from .honeywell_envisalinkdefs import (
IconLED_Flags,
evl_ArmDisarm_CIDs,
evl_CID_Events,
evl_CID_Qualifiers,
evl_ResponseTypes as honeywell_evl_ResponseTypes,
evl_Partition_Status_Codes,
)
from .honeywell_client import HoneywellClient
from .uno_envisalinkdefs import (
evl_Commands,
evl_PanicTypes,
evl_ResponseTypes,
evl_TPI_Response_Codes,
evl_Virtual_Keypad_How_To_Beep,
evl_Partition_Status_Codes,
)
from .honeywell_client import HoneywellClient

_LOGGER = logging.getLogger(__name__)


class UnoClient(HoneywellClient):
"""Represents an Uno alarm client."""
def __init__(self, panel):
super().__init__(panel)
self._evl_ResponseTypes = evl_ResponseTypes | honeywell_evl_ResponseTypes

def handle_login_success(self, code, data):
"""Handler for when the envisalink accepts our credentials."""
super().handle_login_success(code, data)

self.create_internal_task(self.complete_login(), name="complete_login")

async def complete_login(self):
await self.queue_command(evl_Commands["HostInfo"], "")
await self.queue_command(evl_Commands["InitialStateDump"], "")

def handle_keypad_update(self, code, data):
return None
Expand Down Expand Up @@ -52,8 +61,6 @@ def handle_zone_state_change(self, code, data):

return { STATE_CHANGE_ZONE: zone_updates }



def handle_partition_state_change(self, code, data):
"""Handle when the envisalink sends us a partition change."""
partition_updates = []
Expand Down Expand Up @@ -86,4 +93,83 @@ def handle_partition_state_change(self, code, data):

return { STATE_CHANGE_PARTITION: partition_updates }

def handle_zone_bypass_update(self, code, data):
updates= []
zoneNumber = 0
num_bytes = len(data)
idx = 0
while (idx < num_bytes):
byte = int(data[idx:idx+2], 16)
idx += 2
for bit in range(8):
bypassed= byte & (1 << bit) != 0
zoneNumber += 1

_LOGGER.debug(
str.format(
"(zone {0}) bypass state: {1}",
zoneNumber,
bypassed,
)
)

if self._alarmPanel.alarm_state['zone'][zoneNumber]['bypassed'] != bypassed:
updates.append(zoneNumber)
self._alarmPanel.alarm_state['zone'][zoneNumber]['bypassed'] = bypassed
updates.append(zoneNumber)


return { STATE_CHANGE_ZONE_BYPASS: updates}

def handle_host_information_report(self, code, data):
"""Process Host Information Report"""

host_info = data.split(',')
if len(host_info) != 3:
pass

mac_address = host_info[0]
device_type = host_info[1]
version = host_info[2]
_LOGGER.debug("Host info: MAC=%s, Device Type=%s, Version='%s'",
mac_address, device_type, version
)
return

def handle_partition_trouble_state_change(self, code, data):
"""Process Partition Trouble State Change"""
# TODO
return

async def arm_stay_partition(self, code, partitionNumber):
"""Public method to arm/stay a partition."""
await self.queue_command(evl_Commands["StayArm"], str(partitionNumber))

async def arm_away_partition(self, code, partitionNumber):
"""Public method to arm/away a partition."""
await self.queue_command(evl_Commands["AwayArm"], str(partitionNumber))

async def arm_max_partition(self, code, partitionNumber):
"""Public method to arm/max a partition."""
raise NotImplementedError()

async def arm_night_partition(self, code, partitionNumber, mode=None):
"""Public method to arm/max a partition."""
raise NotImplementedError()

async def disarm_partition(self, code, partitionNumber):
"""Public method to disarm a partition."""
await self.queue_command(evl_Commands["Disarm"], f"{partitionNumber},{code}", code)

async def panic_alarm(self, panicType):
"""Public method to raise a panic alarm."""
await self.queue_command(evl_Commands["PanicAlarm"], evl_PanicTypes[panicType])

async def bypass_zone(self, zone, partition, enable):
command = evl_Commands["BypassZone" if enable else "UnbypassZone"]
await self.queue_command(command, f"{zone:03}")

async def toggle_chime(self, code):
"""Public method to toggle a zone's bypass state."""
raise NotImplementedError()

Loading

0 comments on commit 10a53d2

Please sign in to comment.