Skip to content

Commit

Permalink
Initial changes to support Uno panels.
Browse files Browse the repository at this point in the history
  • Loading branch information
ufodone committed Feb 26, 2024
1 parent 73d0b3a commit e7d45ff
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 14 deletions.
21 changes: 18 additions & 3 deletions custom_components/envisalink_new/pyenvisalink/alarm_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
MAX_PARTITIONS,
PANEL_TYPE_DSC,
PANEL_TYPE_HONEYWELL,
PANEL_TYPE_UNO,
)
from .dsc_client import DSCClient
from .honeywell_client import HoneywellClient
from .uno_client import UnoClient

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -262,7 +264,10 @@ async def start(self):
)
)
self._syncConnect: asyncio.Future[self.ConnectionResult] = asyncio.Future()
if self._panelType == PANEL_TYPE_HONEYWELL:
if self._panelType == PANEL_TYPE_UNO:
self._client = UnoClient(self)
self._client.start()
elif self._panelType == PANEL_TYPE_HONEYWELL:
self._client = HoneywellClient(self)
self._client.start()
elif self._panelType == PANEL_TYPE_DSC:
Expand Down Expand Up @@ -417,8 +422,14 @@ async def discover_device_details(self) -> bool:
_LOGGER.warn("Unable to determine panel type: raw HTML: %s", html)
else:
self._panelType = m.group(1).upper()
if self._panelType not in [PANEL_TYPE_DSC, PANEL_TYPE_HONEYWELL]:
if self._panelType not in [PANEL_TYPE_DSC, PANEL_TYPE_HONEYWELL, PANEL_TYPE_UNO]:
_LOGGER.warn("Unrecognized panel type: %s", self._panelType)

#TODO: Force this to be an Uno panel for now until it's
# possible to detect correctly.
if self._panelType == PANEL_TYPE_HONEYWELL:
self._panelType = PANEL_TYPE_UNO;

except Exception as ex:
_LOGGER.error("Unable to fetch panel information: %s", ex)
return self.ConnectionResult.CONNECTION_FAILED
Expand Down Expand Up @@ -495,7 +506,11 @@ async def discover_panel_type(self) -> ConnectionResult:
)
if data:
data = data.decode("ascii").strip()
if HoneywellClient.detect(data):
if UnoClient.detect(data):
self._panelType = PANEL_TYPE_UNO
_LOGGER.info("Panel type: %s", self._panelType)
return self.ConnectionResult.SUCCESS
elif HoneywellClient.detect(data):
self._panelType = PANEL_TYPE_HONEYWELL
_LOGGER.info("Panel type: %s", self._panelType)
return self.ConnectionResult.SUCCESS
Expand Down
1 change: 1 addition & 0 deletions custom_components/envisalink_new/pyenvisalink/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

PANEL_TYPE_DSC = "DSC"
PANEL_TYPE_HONEYWELL = "HONEYWELL"
PANEL_TYPE_UNO = "UNO"

# Maximum number of partitions supports by the EVL
MAX_PARTITIONS = 8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,42 +153,135 @@ class IconLED_Flags(ctypes.Union):
"name": "NOT_USED",
"description": "Partition is not used or doesn" "t exist",
},
"01": {"name": "READY", "description": "Ready", "pluginhandler": "disarmed"},
"01": {
"name": "READY",
"description": "Ready",
"status": {
"ready": True,
"alarm": False,
"armed": False,
"armed_stay": False,
"armed_zero_entry_delay": False,
"armed_away": False,
"exit_delay": False,
"entry_delay": False,
"alpha": "Ready",
},
},
"02": {
"name": "READY_BYPASS",
"description": "Ready to Arm (Zones are Bypasses)",
"pluginhandler": "disarmed",
"status": {
"ready": True,
"alarm": False,
"armed": False,
"armed_stay": False,
"armed_zero_entry_delay": False,
"armed_away": False,
"exit_delay": False,
"entry_delay": False,
"alpha": "Ready (Bypass)",
},
},
"03": {
"name": "NOT_READY",
"description": "Not Ready",
"pluginhandler": "disarmed",
"status": {
"ready": False,
"alarm": False,
"armed": False,
"armed_stay": False,
"armed_zero_entry_delay": False,
"armed_away": False,
"exit_delay": False,
"entry_delay": False,
"alpha": "Not Ready",
},
},
"04": {
"name": "ARMED_STAY",
"description": "Armed in Stay Mode",
"pluginhandler": "armedHome",
"status": {
"alarm": False,
"armed": True,
"armed_stay": True,
"armed_zero_entry_delay": False,
"armed_away": False,
"exit_delay": False,
"entry_delay": False,
"alpha": "Armed Stay",
},
},
"05": {
"name": "ARMED_AWAY",
"description": "Armed in Away Mode",
"pluginhandler": "armedAway",
"status": {
"alarm": False,
"armed": True,
"armed_stay": False,
"armed_zero_entry_delay": False,
"armed_away": True,
"exit_delay": False,
"entry_delay": False,
"alpha": "Armed Away",
},
},
"06": {
"name": "ARMED_MAX",
"description": "Armed in Away Mode",
"pluginhandler": "armedInstant",
"name": "ARMED_INSTANT",
"description": "Armed in Max Mode",
"status": {
"alarm": False,
"armed": True,
"armed_stay": False,
"armed_zero_entry_delay": True,
"armed_away": False,
"exit_delay": False,
"entry_delay": False,
"alpha": "Armed Instant",
},
},
"07": {
"name": "EXIT_ENTRY_DELAY",
"description": "Entry or Exit Delay",
"status": {
"alarm": False,
"armed_stay": False,
"armed_zero_entry_delay": False,
"armed_away": False,
"exit_delay": True,
"entry_delay": True,
"alpha": "Entry/Exit Delay",
},
},
"07": {"name": "EXIT_ENTRY_DELAY", "description": "Entry or Exit Delay"},
"08": {
"name": "IN_ALARM",
"description": "Partition is in Alarm",
"pluginhandler": "alarmTriggered",
"status": {
"alarm": True,
"alpha": "In Alarm",
},
},
"09": {
"name": "ALARM_IN_MEMORY",
"description": "Alarm Has Occurred (Alarm in Memory)",
"pluginhandler": "alarmCleared",
"status": {
"alarm": True,
"alpha": "Alarm in Memory",
},
},
"10": {
"name": "ARMED_MAX",
"description": "Armed in Max Mode",
"status": {
"alarm": False,
"armed": True,
"armed_stay": False,
"armed_zero_entry_delay": True,
"armed_away": False,
"exit_delay": False,
"entry_delay": False,
"alpha": "Armed Max",
},
},
}

Expand Down
107 changes: 107 additions & 0 deletions custom_components/envisalink_new/pyenvisalink/uno_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import json
import logging
import re
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_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 detect(prompt):
"""Given the initial connection data, determine if this is a Uno panel."""
#TODO
return prompt == "Login:"

def handle_keypad_update(self, code, data):
return None

def handle_zone_state_change(self, code, data):
"""Handle when the envisalink sends us a zone change."""
zone_updates = []
# Envisalink TPI is inconsistent at generating these
bigEndianHexString = ''
# every four characters
inputItems = re.findall('....', data)
for inputItem in inputItems:
# Swap the couples of every four bytes
# (little endian to big endian)
swapedBytes = []
swapedBytes.insert(0, inputItem[0:2])
swapedBytes.insert(0, inputItem[2:4])

# add swapped set of four bytes to our return items,
# converting from hex to int
bigEndianHexString += ''.join(swapedBytes)

# convert hex string to bitstring
bitfieldString = str(bin(int(bigEndianHexString, 16))[2:].zfill(self._alarmPanel.max_zones))

# reverse every 16 bits so "lowest" zone is on the left
zonefieldString = ''
inputItems = re.findall('.' * 16, bitfieldString)

for inputItem in inputItems:
zonefieldString += inputItem[::-1]

for zoneNumber, zoneBit in enumerate(zonefieldString, start=1):
self._alarmPanel.alarm_state['zone'][zoneNumber]['status'].update({'open': zoneBit == '1', 'fault': zoneBit == '1'})
if zoneBit == '1':
self._alarmPanel.alarm_state['zone'][zoneNumber]['last_fault'] = 0

_LOGGER.debug("(zone %i) is %s", zoneNumber, "Open/Faulted" if zoneBit == '1' else "Closed/Not Faulted")
zone_updates.append(zoneNumber)
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 = []
for currentIndex in range(0, 8):
partitionNumber = currentIndex + 1
partitionStateCode = data[currentIndex * 2:(currentIndex * 2) + 2]
partitionState = evl_Partition_Status_Codes.get(str(partitionStateCode))
if not partitionState:
_LOGGER.warn("Unrecognized partition state code (%s) received for partition %d",
str(partitionStateCode), partitionNumber)
continue

if not partitionState or partitionState['name'] == 'NOT_USED':
continue

previouslyArmed = self._alarmPanel.alarm_state['partition'][partitionNumber]['status'].get('armed', False)
armed = partitionState['status'].get('armed', False)
self._alarmPanel.alarm_state['partition'][partitionNumber]['status'].update(
partitionState['status'])

if partitionState['name'] == 'EXIT_ENTRY_DELAY':
self._alarmPanel.alarm_state['partition'][partitionNumber]['status'].update({
'exit_delay': not previouslyArmed,
'entry_delay': previouslyArmed,
})

_LOGGER.debug('Partition ' + str(partitionNumber) + ' is in state ' + partitionState['name'])
_LOGGER.debug(json.dumps(self._alarmPanel.alarm_state['partition'][partitionNumber]['status']))
partition_updates.append(partitionNumber)

return { STATE_CHANGE_PARTITION: partition_updates }


0 comments on commit e7d45ff

Please sign in to comment.