diff --git a/alarmdecoder/devices/serial_device.py b/alarmdecoder/devices/serial_device.py index 96bc636..7d38240 100644 --- a/alarmdecoder/devices/serial_device.py +++ b/alarmdecoder/devices/serial_device.py @@ -14,7 +14,7 @@ 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 @@ def close(self): def fileno(self): """ Returns the file number associated with the device - + :returns: int """ return self._device.fileno() @@ -178,13 +178,13 @@ def read(self): :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 @@ def timeout_event(): 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 @@ def timeout_event(): finally: timer.cancel() - return data.decode('utf-8') + return ret.decode('utf-8') def purge(self): """ diff --git a/alarmdecoder/util.py b/alarmdecoder/util.py index ad6291b..2702a13 100644 --- a/alarmdecoder/util.py +++ b/alarmdecoder/util.py @@ -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. diff --git a/alarmdecoder/zonetracking.py b/alarmdecoder/zonetracking.py index e56ec12..13be3c3 100644 --- a/alarmdecoder/zonetracking.py +++ b/alarmdecoder/zonetracking.py @@ -151,9 +151,7 @@ def update(self, message): 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 @@ def update(self, message): 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 @@ def update(self, message): 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 @@ def _clear_zones(self, zone): :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 @@ def _clear_zones(self, zone): # 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): """ diff --git a/setup.py b/setup.py index ec58ed4..c803fc9 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def readme(): 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(), diff --git a/test/test_ad2.py b/test/test_ad2.py index 716fc49..34feb84 100644 --- a/test/test_ad2.py +++ b/test/test_ad2.py @@ -362,5 +362,7 @@ def test_zone_fault_and_restore(self): 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) diff --git a/test/test_devices.py b/test/test_devices.py index 326374a..d418780 100644 --- a/test/test_devices.py +++ b/test/test_devices.py @@ -80,8 +80,11 @@ def test_write_exception(self): 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()