@@ -14,7 +14,7 @@ import serial.tools.list_ports | |||
import select | |||
import sys | |||
from .base_device import Device | |||
from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack | |||
from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack, filter_ad2prot_byte | |||
class SerialDevice(Device): | |||
@@ -141,7 +141,7 @@ class SerialDevice(Device): | |||
def fileno(self): | |||
""" | |||
Returns the file number associated with the device | |||
:returns: int | |||
""" | |||
return self._device.fileno() | |||
@@ -178,13 +178,13 @@ class SerialDevice(Device): | |||
:returns: character read from the device | |||
:raises: :py:class:`~alarmdecoder.util.CommError` | |||
""" | |||
data = '' | |||
data = b'' | |||
try: | |||
read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | |||
if len(read_ready) != 0: | |||
data = self._device.read(1) | |||
data = filter_ad2prot_byte(self._device.read(1)) | |||
except serial.SerialException as err: | |||
raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
@@ -213,54 +213,38 @@ class SerialDevice(Device): | |||
if purge_buffer: | |||
self._buffer = b'' | |||
got_line, data = False, '' | |||
got_line, ret = False, None | |||
timer = threading.Timer(timeout, timeout_event) | |||
if timeout > 0: | |||
timer.start() | |||
leftovers = b'' | |||
try: | |||
while timeout_event.reading and not got_line: | |||
while timeout_event.reading: | |||
read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | |||
if len(read_ready) == 0: | |||
continue | |||
bytes_avail = 0 | |||
if hasattr(self._device, "in_waiting"): | |||
bytes_avail = self._device.in_waiting | |||
else: | |||
bytes_avail = self._device.inWaiting() | |||
buf = self._device.read(bytes_avail) | |||
for idx in range(len(buf)): | |||
c = buf[idx] | |||
buf = filter_ad2prot_byte(self._device.read(1)) | |||
ub = bytes_hack(c) | |||
if sys.version_info > (3,): | |||
ub = bytes([ub]) | |||
if buf != b'': | |||
self._buffer += buf | |||
# NOTE: AD2SERIAL and AD2PI apparently sends down \xFF on boot. | |||
if ub != b'' and ub != b"\xff": | |||
self._buffer += ub | |||
if ub == b"\n": | |||
self._buffer = self._buffer.strip(b"\r\n") | |||
if len(self._buffer) > 0: | |||
got_line = True | |||
leftovers = buf[idx:] | |||
break | |||
if buf == b"\n": | |||
self._buffer = self._buffer.rstrip(b"\r\n") | |||
if len(self._buffer) > 0: | |||
got_line = True | |||
break | |||
except (OSError, serial.SerialException) as err: | |||
raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
else: | |||
if got_line: | |||
data, self._buffer = self._buffer, leftovers | |||
ret, self._buffer = self._buffer, b'' | |||
self.on_read(data=data) | |||
self.on_read(data=ret) | |||
else: | |||
raise TimeoutError('Timeout while waiting for line terminator.') | |||
@@ -268,7 +252,7 @@ class SerialDevice(Device): | |||
finally: | |||
timer.cancel() | |||
return data.decode('utf-8') | |||
return ret.decode('utf-8') | |||
def purge(self): | |||
""" | |||
@@ -93,6 +93,22 @@ def bytes_hack(buf): | |||
return ub | |||
def filter_ad2prot_byte(buf): | |||
""" | |||
Return the byte sent in back if valid visible terminal characters or line terminators. | |||
""" | |||
if sys.version_info > (3,): | |||
c = buf[0] | |||
else: | |||
c = ord(buf) | |||
if (c == 10 or c == 13): | |||
return buf | |||
if (c > 31 and c < 127): | |||
return buf | |||
else: | |||
return b'' | |||
def read_firmware_file(file_path): | |||
""" | |||
Reads a firmware file into a dequeue for processing. | |||
@@ -151,9 +151,7 @@ class Zonetracker(object): | |||
status = Zone.CHECK | |||
# NOTE: Expander zone faults are handled differently than | |||
# regular messages. We don't include them in | |||
# self._zones_faulted because they are not reported | |||
# by the panel in it's rolling list of faults. | |||
# regular messages. | |||
try: | |||
self._update_zone(zone, status=status) | |||
@@ -198,6 +196,9 @@ class Zonetracker(object): | |||
self._update_zone(zone) | |||
self._clear_zones(zone) | |||
# Save our spot for the next message. | |||
self._last_zone_fault = zone | |||
else: | |||
status = Zone.FAULT | |||
if message.check_zone: | |||
@@ -207,8 +208,8 @@ class Zonetracker(object): | |||
self._zones_faulted.append(zone) | |||
self._zones_faulted.sort() | |||
# Save our spot for the next message. | |||
self._last_zone_fault = zone | |||
# A new zone fault, so it is out of sequence. | |||
self._last_zone_fault = 0 | |||
self._clear_expired_zones() | |||
@@ -245,6 +246,11 @@ class Zonetracker(object): | |||
:param zone: current zone being processed | |||
:type zone: int | |||
""" | |||
if self._last_zone_fault == 0: | |||
# We don't know what the last faulted zone was, nothing to do | |||
return | |||
cleared_zones = [] | |||
found_last_faulted = found_current = at_end = False | |||
@@ -296,7 +302,9 @@ class Zonetracker(object): | |||
# Actually remove the zones and trigger the restores. | |||
for z in cleared_zones: | |||
self._update_zone(z, Zone.CLEAR) | |||
# Don't clear expander zones, expander messages will fix this | |||
if self._zones[z].expander is False: | |||
self._update_zone(z, Zone.CLEAR) | |||
def _clear_expired_zones(self): | |||
""" | |||
@@ -14,7 +14,7 @@ if sys.version_info < (3,): | |||
extra_requirements.append('future>=0.14.3') | |||
setup(name='alarmdecoder', | |||
version='1.13.9', | |||
version='1.13.10', | |||
description='Python interface for the AlarmDecoder (AD2) family ' | |||
'of alarm devices which includes the AD2USB, AD2SERIAL and AD2PI.', | |||
long_description=readme(), | |||
@@ -362,5 +362,7 @@ class TestAlarmDecoder(TestCase): | |||
self._decoder._on_read(self, data=b'[00010001000000000A--],005,[f70000051003000008020000000000],"FAULT 05 "') | |||
self.assertEquals(self._zone_faulted, 5) | |||
self._decoder._on_read(self, data=b'[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 04 "') | |||
self._decoder._on_read(self, data=b'[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 05 "') | |||
self._decoder._on_read(self, data=b'[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 04 "') | |||
self.assertEquals(self._zone_restored, 3) |
@@ -80,8 +80,11 @@ class TestSerialDevice(TestCase): | |||
def test_read(self): | |||
self._device.interface = '/dev/ttyS0' | |||
self._device.open(no_reader_thread=True) | |||
side_effect = ["t"] | |||
if sys.version_info > (3,): | |||
side_effect = ["t".encode('utf-8')] | |||
with patch.object(self._device._device, 'read') as mock: | |||
with patch.object(self._device._device, 'read', side_effect=side_effect) as mock: | |||
with patch('serial.Serial.fileno', return_value=1): | |||
with patch.object(select, 'select', return_value=[[1], [], []]): | |||
ret = self._device.read() | |||