| @@ -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() | |||