Skip to content

Commit

Permalink
* Big re-factor of the handling of incoming data from the EVL to deal…
Browse files Browse the repository at this point in the history
… with the possibility of partial reads.

* Fixed issues with issuing commands on Honeywell system (only tested against a mock EVL).
  • Loading branch information
ufodone committed Jan 14, 2023
1 parent 577dd24 commit cbc63da
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 83 deletions.
64 changes: 37 additions & 27 deletions custom_components/envisalink_new/pyenvisalink/dsc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,37 +90,47 @@ async def command_output(self, code, partitionNumber, outputNumber):

def parseHandler(self, rawInput):
"""When the envisalink contacts us- parse out which command and data."""

end_idx = rawInput.find("\r\n")
if end_idx ==-1:
# We don't have a full command yet
return (None, rawInput)

remainder = rawInput[end_idx+2:]
rawInput = rawInput[:end_idx]

cmd = {}
dataoffset = 0
if rawInput != '':
if re.match('\d\d:\d\d:\d\d\s', rawInput):
dataoffset = dataoffset + 9
code = rawInput[dataoffset:dataoffset+3]
cmd['code'] = code
cmd['data'] = rawInput[dataoffset+3:][:-2]

try:
#Interpret the login command further to see what our handler is.
if evl_ResponseTypes[code]['handler'] == 'login':
if cmd['data'] == '3':
handler = 'login'
elif cmd['data'] == '2':
handler = 'login_timeout'
elif cmd['data'] == '1':
handler = 'login_success'
elif cmd['data'] == '0':
handler = 'login_failure'

cmd['handler'] = "handle_%s" % handler
cmd['callback'] = "callback_%s" % handler
if re.match('\d\d:\d\d:\d\d\s', rawInput):
dataoffset = dataoffset + 9
code = rawInput[dataoffset:dataoffset+3]
cmd['code'] = code
cmd['data'] = rawInput[dataoffset+3:][:-2]

try:
#Interpret the login command further to see what our handler is.
if evl_ResponseTypes[code]['handler'] == 'login':
if cmd['data'] == '3':
handler = 'login'
elif cmd['data'] == '2':
handler = 'login_timeout'
elif cmd['data'] == '1':
handler = 'login_success'
elif cmd['data'] == '0':
handler = 'login_failure'

cmd['handler'] = "handle_%s" % handler
cmd['callback'] = "callback_%s" % handler

else:
cmd['handler'] = "handle_%s" % evl_ResponseTypes[code]['handler']
cmd['callback'] = "callback_%s" % evl_ResponseTypes[code]['handler']
except KeyError:
_LOGGER.debug(str.format('No handler defined in config for {0}, skipping...', code))
else:
cmd['handler'] = "handle_%s" % evl_ResponseTypes[code]['handler']
cmd['callback'] = "callback_%s" % evl_ResponseTypes[code]['handler']
except KeyError:
_LOGGER.debug(str.format('No handler defined in config for {0}, skipping...', code))

return cmd
if len(remainder) == 0:
remainder = None
return (cmd, remainder)

def handle_login(self, code, data):
"""When the envisalink asks us for our password- send it."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,23 @@ async def read_loop(self):
if self._reader and self._writer:
# Connected to EVL; start reading data from the connection
try:
unprocessed_data = None
while not self._shutdown and self._reader:
_LOGGER.debug("Waiting for data from EVL")
data = await self._reader.read(n=256)
if not data or len(data) == 0 or self._reader.at_eof():
_LOGGER.error('The server closed the connection.')
await self.disconnect()
break
self.process_data(data)

data = data.decode('ascii')
_LOGGER.debug('----------------------------------------')
_LOGGER.debug(str.format('RX < {0}', data))

if unprocessed_data:
data = unprocessed_data + data

unprocessed_data = self.process_data(data)
except Exception as ex:
_LOGGER.error("Caught unexpected exception: %r", ex)
await self.disconnect()
Expand Down Expand Up @@ -231,41 +240,36 @@ def parseHandler(self, rawInput):
"""When the envisalink contacts us- parse out which command and data."""
raise NotImplementedError()

def process_data(self, data):
"""asyncio callback for any data recieved from the envisalink."""
if data != '':
def process_data(self, data) -> str:
while data is not None and len(data) > 0:
cmd, data = self.parseHandler(data)

if not cmd:
break

try:
fullData = data.decode('ascii').strip()
cmd = {}
result = ''
_LOGGER.debug('----------------------------------------')
_LOGGER.debug(str.format('RX < {0}', fullData))
lines = str.split(fullData, '\r\n')
except:
_LOGGER.error('Received invalid message. Skipping.')
return

for line in lines:
cmd = self.parseHandler(line)

try:
_LOGGER.debug(str.format('calling handler: {0} for code: {1} with data: {2}', cmd['handler'], cmd['code'], cmd['data']))
handlerFunc = getattr(self, cmd['handler'])
result = handlerFunc(cmd['code'], cmd['data'])

except (AttributeError, TypeError, KeyError) as err:
_LOGGER.debug("No handler configured for evl command.")
_LOGGER.debug(str.format("KeyError: {0}", err))

try:
_LOGGER.debug(str.format('Invoking callback: {0}', cmd['callback']))
callbackFunc = getattr(self._alarmPanel, cmd['callback'])
callbackFunc(result)

except (AttributeError, TypeError, KeyError) as err:
_LOGGER.debug("No callback configured for evl command.")

_LOGGER.debug('----------------------------------------')
_LOGGER.debug(str.format('calling handler: {0} for code: {1} with data: {2}', cmd['handler'], cmd['code'], cmd['data']))
handlerFunc = getattr(self, cmd['handler'])
result = handlerFunc(cmd['code'], cmd['data'])

except (AttributeError, TypeError, KeyError) as err:
_LOGGER.debug("No handler configured for evl command.")
_LOGGER.debug(str.format("KeyError: {0}", err))

try:
_LOGGER.debug(str.format('Invoking callback: {0}', cmd['callback']))
callbackFunc = getattr(self._alarmPanel, cmd['callback'])
callbackFunc(result)

except (AttributeError, TypeError, KeyError) as err:
_LOGGER.debug("No callback configured for evl command.")

_LOGGER.debug('----------------------------------------')

# Return any unprocessed data (uncomplete command)
if not data or len(data) == 0:
return None
return data

def convertZoneDump(self, theString):
"""Interpret the zone dump result, and convert to readable times."""
Expand Down
70 changes: 49 additions & 21 deletions custom_components/envisalink_new/pyenvisalink/honeywell_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ async def keep_alive(self):
"""Send a keepalive command to reset it's watchdog timer."""
while not self._shutdown:
if self._loggedin:
self.send_command(evl_Commands['KeepAlive'], '')
await self.queue_command(evl_Commands['KeepAlive'], '')
await asyncio.sleep(self._alarmPanel.keepalive_interval)

async def periodic_zone_timer_dump(self):
"""Used to periodically get the zone timers to make sure our zones are updated."""
while not self._shutdown:
if self._loggedin:
self.dump_zone_timers()
await self.dump_zone_timers()
await asyncio.sleep(self._alarmPanel.zone_timer_interval)

async def send_command(self, code, data):
Expand All @@ -32,12 +32,12 @@ async def send_command(self, code, data):

async def dump_zone_timers(self):
"""Send a command to dump out the zone timers."""
await self.send_command(evl_Commands['DumpZoneTimers'], '')
await self.queue_command(evl_Commands['DumpZoneTimers'], '')

async def keypresses_to_partition(self, partitionNumber, keypresses):
"""Send keypresses to a particular partition."""
for char in keypresses:
await self.send_command(evl_Commands['PartitionKeypress'], str.format("{0},{1}", partitionNumber, char))
await self.queue_command(evl_Commands['PartitionKeypress'], str.format("{0},{1}", partitionNumber, char))

async def arm_stay_partition(self, code, partitionNumber):
"""Public method to arm/stay a partition."""
Expand Down Expand Up @@ -69,44 +69,72 @@ async def panic_alarm(self, panicType):
def parseHandler(self, rawInput):
"""When the envisalink contacts us- parse out which command and data."""
cmd = {}
_LOGGER.debug(str.format("Data received:{0}", rawInput))

parse = re.match('([%\^].+)\$', rawInput)
if parse and parse.group(1):
# keep first sentinel char to tell difference between tpi and
# Envisalink command responses. Drop the trailing $ sentinel.
inputList = parse.group(1).split(',')
code = inputList[0]
cmd['code'] = code
cmd['data'] = ','.join(inputList[1:])
_LOGGER.debug(str.format("Code:{0} Data:{1}", code, cmd['data']))
elif not self._loggedin:
rawInput = re.sub("[\r\n]", "", rawInput)

if not self._loggedin:
# assume it is login info
code = rawInput
cmd['code'] = code
cmd['data'] = ''
rawInput = None
else:
_LOGGER.error("Unrecognized data recieved from the envisalink. Ignoring.")
return None
# Look for a sentinel
m = re.match("[%\^]", rawInput)
if m is None:
# No sentinels so ignore the data
_LOGGER.error("Unrecognized data recieved from the envisalink. Ignoring: '%s'", rawInput)
return (None, None)

start_idx = m.start(0)
if start_idx != 0:
# Ignore characters up to the sentinel
rawInput = rawInput[start_idx:]

# There's a command here; find the end of it
end_idx = rawInput.find("$")
if end_idx == -1:
# We don't have the full command yet
if len(rawInput) == 0:
rawInput = None
return (None, rawInput)

# A full command is present

# keep first sentinel char to tell difference between tpi and
# Envisalink command responses. Drop the trailing $ sentinel.
inputList = rawInput[start_idx:end_idx].split(",")
code = inputList[0]
cmd['code'] = code
cmd['data'] = inputList[1]
rawInput = rawInput[end_idx+1:]

_LOGGER.debug(str.format("Code:{0} Data:{1}", cmd['code'], cmd['data']))

try:
cmd['handler'] = "handle_%s" % evl_ResponseTypes[code]['handler']
cmd['callback'] = "callback_%s" % evl_ResponseTypes[code]['handler']
except KeyError:
_LOGGER.warning(str.format('No handler defined in config for {0}, skipping...', code))

return cmd

if rawInput and len(rawInput) == 0:
rawInput = None
return (cmd, rawInput)

def handle_login(self, code, data):
"""When the envisalink asks us for our password- send it."""
self.send_data(self._alarmPanel.password)
self.create_internal_task(self.queue_login_response(), name="queue_login_response")

async def queue_login_response(self):
await self.send_data(self._alarmPanel.password)

def handle_command_response(self, code, data):
"""Handle the envisalink's initial response to our commands."""
if data in evl_TPI_Response_Codes:
responseInfo = evl_TPI_Response_Codes[data]
_LOGGER.debug("Envisalink response: " + responseInfo["msg"])
if data == '00':
self.command_succeeded(code)
self.command_succeeded(code[1:])
else:
_LOGGER.error("error sending command to envisalink. Response was: " + responseInfo["msg"])
self.command_failed(retry=errorInfo['retry'])
Expand Down

0 comments on commit cbc63da

Please sign in to comment.