From d69a21056c992834b356b572004ba84c840b73aa Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Tue, 28 Feb 2017 12:59:17 -0800 Subject: [PATCH 1/4] A few more fixes for supporting Python 2 and 3. --- alarmdecoder/devices.py | 21 ++++++++++++++------- alarmdecoder/util.py | 8 ++++---- bin/ad2-firmwareupload | 13 +++++++------ setup.py | 2 +- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/alarmdecoder/devices.py b/alarmdecoder/devices.py index 3df9733..4345a60 100644 --- a/alarmdecoder/devices.py +++ b/alarmdecoder/devices.py @@ -800,6 +800,10 @@ class SerialDevice(Device): :raises: py:class:`~alarmdecoder.util.CommError` """ try: + # Hack to support unicode under Python 2.x + if isinstance(data, str) or (sys.version_info < (3,) and isinstance(data, unicode)): + data = data.encode('utf-8') + self._device.write(data) except serial.SerialTimeoutException: @@ -821,7 +825,7 @@ class SerialDevice(Device): ret = None try: - ret = self._device.read(1) + ret = self._device.read(1).decode('utf-8') except serial.SerialException as err: raise CommError('Error reading from device: {0}'.format(str(err)), err) @@ -890,7 +894,7 @@ class SerialDevice(Device): finally: timer.cancel() - return ret + return ret.decode('utf-8') def purge(self): """ @@ -1096,6 +1100,9 @@ class SocketDevice(Device): data_sent = None try: + if isinstance(data, str): + data = data.encode('utf-8') + data_sent = self._device.send(data) if data_sent == 0: @@ -1118,7 +1125,7 @@ class SocketDevice(Device): data = None try: - read_ready, _, _ = select.select([self._device], [], [], 0) + read_ready, _, _ = select.select([self._device], [], []) if (len(read_ready) != 0): data = self._device.recv(1) @@ -1126,7 +1133,7 @@ class SocketDevice(Device): except socket.error as err: raise CommError('Error while reading from device: {0}'.format(str(err)), err) - return data + return data.decode('utf-8') def read_line(self, timeout=0.0, purge_buffer=False): """ @@ -1158,7 +1165,7 @@ class SocketDevice(Device): try: while timeout_event.reading: - read_ready, _, _ = select.select([self._device], [], [], 0) + read_ready, _, _ = select.select([self._device], [], []) if (len(read_ready) == 0): time.sleep(0.01) @@ -1166,7 +1173,7 @@ class SocketDevice(Device): buf = self._device.recv(1) - if buf != b'': + if buf != b'' and buf != b"\xff": ub = bytes_hack(buf) self._buffer += ub @@ -1203,7 +1210,7 @@ class SocketDevice(Device): finally: timer.cancel() - return ret + return ret.decode('utf-8') def purge(self): """ diff --git a/alarmdecoder/util.py b/alarmdecoder/util.py index a57c911..94d64d4 100644 --- a/alarmdecoder/util.py +++ b/alarmdecoder/util.py @@ -94,7 +94,7 @@ class Firmware(object): if line[0] == ':': dev.write(line + "\r") - response = dev.read_line(timeout=5.0, purge_buffer=True) + response = dev.read_line(timeout=5.0, purge_buffer=True) #.decode('utf-8') if debug: stage_callback(Firmware.STAGE_DEBUG, data="line={0} - line={1} response={2}".format(line_cnt, line, response)); @@ -135,7 +135,7 @@ class Firmware(object): while timeout_event.reading: try: - char = dev.read() + char = dev.read() #.decode('utf-8') if char is not None and char != '': if char == pattern[position]: @@ -179,13 +179,13 @@ class Firmware(object): try: stage_callback(Firmware.STAGE_BOOT) dev.write("=") - read_until('!boot', timeout=15.0) + read_until(u'!boot', timeout=15.0) # Get ourselves into the boot loader and wait for indication # that it's ready for the firmware upload. stage_callback(Firmware.STAGE_LOAD) dev.write("=") - read_until('!load', timeout=15.0) + read_until(u'!load', timeout=15.0) except TimeoutError as err: retry -= 1 diff --git a/bin/ad2-firmwareupload b/bin/ad2-firmwareupload index 507bb5e..228312c 100755 --- a/bin/ad2-firmwareupload +++ b/bin/ad2-firmwareupload @@ -2,6 +2,7 @@ import os import sys, time +import traceback import alarmdecoder def handle_firmware(stage, **kwargs): @@ -68,12 +69,12 @@ def main(): time.sleep(3) alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware, debug=debug) - except alarmdecoder.util.NoDeviceError, ex: - print "Error: Could not find device: {0}".format(ex) - except alarmdecoder.util.UploadError, ex: - print "Error: Error uploading firmware: {0}".format(ex) - except Exception, ex: - print "Error: {0}".format(ex) + except alarmdecoder.util.NoDeviceError as ex: + print("Error: Could not find device: {0}".format(ex)) + except alarmdecoder.util.UploadError as ex: + print("Error: Error uploading firmware: {0}".format(ex)) + except Exception as ex: + print("Error: {0}: {1}".format(ex, traceback.format_exc())) finally: if dev is not None: dev.close() diff --git a/setup.py b/setup.py index e053e1b..1d907c9 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ if sys.version_info < (3,): extra_requirements.append('future==0.14.3') setup(name='alarmdecoder', - version='0.10.3', + version='0.12.2', description='Python interface for the AlarmDecoder (AD2) family ' 'of alarm devices which includes the AD2USB, AD2SERIAL and AD2PI.', long_description=readme(), From 358a950aac8d732473afe66c6d9064ef352084a3 Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Mon, 20 Mar 2017 16:16:14 -0700 Subject: [PATCH 2/4] Reworked SerialDevice to use select as well as associated test patches and 2/3 support. --- alarmdecoder/devices.py | 72 ++++++++++++++++------------ test/test_devices.py | 102 ++++++++++++++++++++++------------------ 2 files changed, 100 insertions(+), 74 deletions(-) diff --git a/alarmdecoder/devices.py b/alarmdecoder/devices.py index 4345a60..586ece2 100644 --- a/alarmdecoder/devices.py +++ b/alarmdecoder/devices.py @@ -822,15 +822,18 @@ class SerialDevice(Device): :returns: character read from the device :raises: :py:class:`~alarmdecoder.util.CommError` """ - ret = None + data = '' try: - ret = self._device.read(1).decode('utf-8') + read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) + + if len(read_ready) != 0: + data = self._device.read(1) except serial.SerialException as err: raise CommError('Error reading from device: {0}'.format(str(err)), err) - return ret + return data.decode('utf-8') def read_line(self, timeout=0.0, purge_buffer=False): """ @@ -854,39 +857,54 @@ class SerialDevice(Device): if purge_buffer: self._buffer = b'' - got_line, ret = False, None + got_line, data = False, '' timer = threading.Timer(timeout, timeout_event) if timeout > 0: timer.start() + leftovers = b'' try: - while timeout_event.reading: - buf = self._device.read(1) + while timeout_event.reading and not got_line: + read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) + if len(read_ready) == 0: + continue - # NOTE: AD2SERIAL apparently sends down \xFF on boot. - if buf != b'' and buf != b"\xff": - ub = bytes_hack(buf) + bytes_avail = 0 + if hasattr(self._device, "in_waiting"): + bytes_avail = self._device.in_waiting + else: + bytes_avail = self._device.inWaiting() - self._buffer += ub + buf = self._device.read(bytes_avail) - if ub == b"\n": - self._buffer = self._buffer.rstrip(b"\r\n") + for idx in range(len(buf)): + c = buf[idx] - if len(self._buffer) > 0: - got_line = True - break - else: - time.sleep(0.01) + ub = bytes_hack(c) + if sys.version_info > (3,): + ub = bytes([ub]) + + # 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 except (OSError, serial.SerialException) as err: raise CommError('Error reading from device: {0}'.format(str(err)), err) else: if got_line: - ret, self._buffer = self._buffer, b'' + data, self._buffer = self._buffer, leftovers - self.on_read(data=ret) + self.on_read(data=data) else: raise TimeoutError('Timeout while waiting for line terminator.') @@ -894,7 +912,7 @@ class SerialDevice(Device): finally: timer.cancel() - return ret.decode('utf-8') + return data.decode('utf-8') def purge(self): """ @@ -1122,12 +1140,12 @@ class SocketDevice(Device): :returns: character read from the device :raises: :py:class:`~alarmdecoder.util.CommError` """ - data = None + data = '' try: - read_ready, _, _ = select.select([self._device], [], []) + read_ready, _, _ = select.select([self._device], [], [], 0.5) - if (len(read_ready) != 0): + if len(read_ready) != 0: data = self._device.recv(1) except socket.error as err: @@ -1165,10 +1183,9 @@ class SocketDevice(Device): try: while timeout_event.reading: - read_ready, _, _ = select.select([self._device], [], []) + read_ready, _, _ = select.select([self._device], [], [], 0.5) - if (len(read_ready) == 0): - time.sleep(0.01) + if len(read_ready) == 0: continue buf = self._device.recv(1) @@ -1185,9 +1202,6 @@ class SocketDevice(Device): got_line = True break - else: - time.sleep(0.01) - except socket.error as err: raise CommError('Error reading from device: {0}'.format(str(err)), err) diff --git a/test/test_devices.py b/test/test_devices.py index 554c336..8c47719 100644 --- a/test/test_devices.py +++ b/test/test_devices.py @@ -6,6 +6,7 @@ try: except: from pyftdi.ftdi import Ftdi, FtdiError from usb.core import USBError, Device as USBCoreDevice +import sys import socket import time import tempfile @@ -14,6 +15,24 @@ import select from alarmdecoder.devices import USBDevice, SerialDevice, SocketDevice from alarmdecoder.util import NoDeviceError, CommError, TimeoutError +# Optional FTDI tests +try: + from pyftdi.pyftdi.ftdi import Ftdi, FtdiError + from usb.core import USBError, Device as USBCoreDevice + + have_pyftdi = True + +except ImportError: + have_pyftdi = False + +# Optional SSL tests +try: + from OpenSSL import SSL, crypto + + have_openssl = True +except ImportError: + have_openssl = False + class TestUSBDevice(TestCase): def setUp(self): @@ -120,31 +139,6 @@ class TestUSBDevice(TestCase): mock.assert_called_with('test') -from unittest import TestCase -from mock import Mock, MagicMock, patch -from serial import Serial, SerialException - -from alarmdecoder.devices import USBDevice, SerialDevice, SocketDevice -from alarmdecoder.util import NoDeviceError, CommError, TimeoutError - -# Optional FTDI tests -try: - from pyftdi.pyftdi.ftdi import Ftdi, FtdiError - from usb.core import USBError, Device as USBCoreDevice - - have_pyftdi = True - -except ImportError: - have_pyftdi = False - -# Optional SSL tests -try: - from OpenSSL import SSL, crypto - - have_openssl = True -except ImportError: - have_openssl = False - class TestSerialDevice(TestCase): def setUp(self): @@ -198,39 +192,53 @@ class TestSerialDevice(TestCase): self._device.open(no_reader_thread=True) with patch.object(self._device._device, 'read') as mock: - self._device.read() + with patch('serial.Serial.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + ret = self._device.read() mock.assert_called_with(1) def test_read_exception(self): with patch.object(self._device._device, 'read', side_effect=SerialException): - with self.assertRaises(CommError): - self._device.read() + with patch('serial.Serial.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + with self.assertRaises(CommError): + self._device.read() def test_read_line(self): - with patch.object(self._device._device, 'read', side_effect=list("testing\r\n")): - ret = None - try: - ret = self._device.read_line() - except StopIteration: - pass + side_effect = list("testing\r\n") + if sys.version_info > (3,): + side_effect = [chr(x).encode('utf-8') for x in b"testing\r\n"] - self.assertEquals(ret, b"testing") + with patch.object(self._device._device, 'read', side_effect=side_effect): + with patch('serial.Serial.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + ret = None + try: + ret = self._device.read_line() + except StopIteration: + pass + + self.assertEquals(ret, "testing") def test_read_line_timeout(self): - with patch.object(self._device._device, 'read', return_value='a') as mock: - with self.assertRaises(TimeoutError): - self._device.read_line(timeout=0.1) + with patch.object(self._device._device, 'read', return_value=b'a') as mock: + with patch('serial.Serial.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + with self.assertRaises(TimeoutError): + self._device.read_line(timeout=0.1) self.assertIn('a', self._device._buffer.decode('utf-8')) def test_read_line_exception(self): with patch.object(self._device._device, 'read', side_effect=[OSError, SerialException]): - with self.assertRaises(CommError): - self._device.read_line() + with patch('serial.Serial.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + with self.assertRaises(CommError): + self._device.read_line() - with self.assertRaises(CommError): - self._device.read_line() + with self.assertRaises(CommError): + self._device.read_line() class TestSocketDevice(TestCase): @@ -292,21 +300,25 @@ class TestSocketDevice(TestCase): self._device.read() def test_read_line(self): + side_effect = list("testing\r\n") + if sys.version_info > (3,): + side_effect = [chr(x).encode('utf-8') for x in b"testing\r\n"] + with patch('socket.socket.fileno', return_value=1): with patch.object(select, 'select', return_value=[[1], [], []]): - with patch.object(self._device._device, 'recv', side_effect=list("testing\r\n")): + with patch.object(self._device._device, 'recv', side_effect=side_effect): ret = None try: ret = self._device.read_line() except StopIteration: pass - self.assertEquals(ret, b"testing") + self.assertEquals(ret, "testing") def test_read_line_timeout(self): with patch('socket.socket.fileno', return_value=1): with patch.object(select, 'select', return_value=[[1], [], []]): - with patch.object(self._device._device, 'recv', return_value='a') as mock: + with patch.object(self._device._device, 'recv', return_value=b'a') as mock: with self.assertRaises(TimeoutError): self._device.read_line(timeout=0.1) From 8c9b86343acb4379c2fa5d944df36b871f31ca0c Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Mon, 20 Mar 2017 16:17:10 -0700 Subject: [PATCH 3/4] Reworked firmware upload into something that's more manageable and less flakey. --- alarmdecoder/util.py | 244 ++++++++++++++++++++++------------------- bin/ad2-firmwareupload | 11 +- 2 files changed, 136 insertions(+), 119 deletions(-) diff --git a/alarmdecoder/util.py b/alarmdecoder/util.py index 94d64d4..7a0a0ac 100644 --- a/alarmdecoder/util.py +++ b/alarmdecoder/util.py @@ -8,7 +8,11 @@ Provides utility classes for the `AlarmDecoder`_ (AD2) devices. import time import threading +import select +import alarmdecoder + from io import open +from collections import deque class NoDeviceError(Exception): @@ -53,6 +57,30 @@ class UploadChecksumError(UploadError): pass +def bytes_available(device): + bytes_avail = 0 + + if isinstance(device, alarmdecoder.devices.SerialDevice): + if hasattr(device._device, "in_waiting"): + bytes_avail = device._device.in_waiting + else: + bytes_avail = device._device.inWaiting() + elif isinstance(device, alarmdecoder.devices.SocketDevice): + bytes_avail = 4096 + + return bytes_avail + +def read_firmware_file(file_path): + data_queue = deque() + + with open(file_path) as firmware_handle: + for line in firmware_handle: + line = line.rstrip() + if line != '' and line[0] == ':': + data_queue.append(line + "\r") + + return data_queue + class Firmware(object): """ Represents firmware for the `AlarmDecoder`_ devices. @@ -62,144 +90,134 @@ class Firmware(object): STAGE_START = 0 STAGE_WAITING = 1 STAGE_BOOT = 2 + STAGE_WAITING_ON_LOADER = 2.5 STAGE_LOAD = 3 STAGE_UPLOADING = 4 STAGE_DONE = 5 STAGE_ERROR = 98 STAGE_DEBUG = 99 - # FIXME: Rewrite this monstrosity. @staticmethod - def upload(dev, filename, progress_callback=None, debug=False): + def read(device): + response = None + bytes_avail = bytes_available(device) + + if isinstance(device, alarmdecoder.devices.SerialDevice): + response = device._device.read(bytes_avail) + elif isinstance(device, alarmdecoder.devices.SocketDevice): + response = device._device.recv(bytes_avail) + + return response + + @staticmethod + def upload(device, file_path, progress_callback=None, debug=False): """ Uploads firmware to an `AlarmDecoder`_ device. - :param filename: firmware filename - :type filename: string + :param file_path: firmware file path + :type file_path: string :param progress_callback: callback function used to report progress :type progress_callback: function :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.TimeoutError` """ - def do_upload(): - """ - Perform the actual firmware upload to the device. - """ - with open(filename) as upload_file: - line_cnt = 0 - for line in upload_file: - line_cnt += 1 - line = line.rstrip() - - if line[0] == ':': - dev.write(line + "\r") - response = dev.read_line(timeout=5.0, purge_buffer=True) #.decode('utf-8') - if debug: - stage_callback(Firmware.STAGE_DEBUG, data="line={0} - line={1} response={2}".format(line_cnt, line, response)); - - if '!ce' in response: - raise UploadChecksumError("Checksum error on line " + str(line_cnt) + " of " + filename); - - elif '!no' in response: - raise UploadError("Incorrect data sent to bootloader.") - - elif '!ok' in response: - break - - else: - if progress_callback is not None: - progress_callback(Firmware.STAGE_UPLOADING) - - time.sleep(0.0) - - def read_until(pattern, timeout=0.0): - """ - Read characters until a specific pattern is found or the timeout is - hit. - """ - def timeout_event(): - """Handles the read timeout event.""" - timeout_event.reading = False - - timeout_event.reading = True - - timer = None - if timeout > 0: - timer = threading.Timer(timeout, timeout_event) - timer.start() - - position = 0 - - dev.purge() - - while timeout_event.reading: - try: - char = dev.read() #.decode('utf-8') - - if char is not None and char != '': - if char == pattern[position]: - position = position + 1 - if position == len(pattern): - break - else: - position = 0 - - except Exception as err: - pass - - if timer: - if timer.is_alive(): - timer.cancel() - else: - raise TimeoutError('Timeout while waiting for line terminator.') - - def stage_callback(stage, **kwargs): + def progress_stage(stage, **kwargs): """Callback to update progress for the specified stage.""" if progress_callback is not None: progress_callback(stage, **kwargs) - if dev is None: + return stage + + if device is None: raise NoDeviceError('No device specified for firmware upload.') - stage_callback(Firmware.STAGE_START) + fds = [device._device.fileno()] + + # Read firmware file into memory + try: + write_queue = read_firmware_file(file_path) + except IOError as err: + stage = progress_stage(Firmware.STAGE_ERROR, error=str(err)) + return - if dev.is_reader_alive(): + data_read = '' + got_response = False + running = True + stage = progress_stage(Firmware.STAGE_START) + + if device.is_reader_alive(): # Close the reader thread and wait for it to die, otherwise # it interferes with our reading. - dev.stop_reader() - while dev._read_thread.is_alive(): - stage_callback(Firmware.STAGE_WAITING) + device.stop_reader() + while device._read_thread.is_alive(): + stage = progress_stage(Firmware.STAGE_WAITING) time.sleep(0.5) - # Reboot the device and wait for the boot loader. - retry = 3 - found_loader = False - while retry > 0: - try: - stage_callback(Firmware.STAGE_BOOT) - dev.write("=") - read_until(u'!boot', timeout=15.0) - - # Get ourselves into the boot loader and wait for indication - # that it's ready for the firmware upload. - stage_callback(Firmware.STAGE_LOAD) - dev.write("=") - read_until(u'!load', timeout=15.0) - - except TimeoutError as err: - retry -= 1 - else: - retry = 0 - found_loader = True - - # And finally do the upload. - if found_loader: - try: - do_upload() - except UploadError as err: - stage_callback(Firmware.STAGE_ERROR, error=str(err)) - else: - stage_callback(Firmware.STAGE_DONE) + time.sleep(3) + + try: + while running: + rr, wr, _ = select.select(fds, fds, [], 0.5) + + if len(rr) != 0: + response = Firmware.read(device) + + for c in response: + # HACK: Python 3 / PySerial hack. + if isinstance(c, int): + c = chr(c) + + if c == '\xff' or c == '\r': # HACK: odd case for our mystery \xff byte. + # Boot started, start looking for the !boot message + if data_read.startswith("!sn"): + stage = progress_stage(Firmware.STAGE_BOOT) + # Entered bootloader upload mode, start uploading + elif data_read.startswith("!load"): + got_response = True + stage = progress_stage(Firmware.STAGE_UPLOADING) + # Checksum error + elif data_read == '!ce': + running = False + raise UploadChecksumError("Checksum error in {0}".format(file_path)) + # Bad data + elif data_read == '!no': + running = False + raise UploadError("Incorrect data sent to bootloader.") + # Firmware upload complete + elif data_read == '!ok': + running = False + stage = progress_stage(Firmware.STAGE_DONE) + # All other responses are valid during upload. + else: + got_response = True + if stage == Firmware.STAGE_UPLOADING: + progress_stage(stage) + + data_read = '' + elif c == '\n': + pass + else: + data_read += c + + if len(wr) != 0: + # Reboot device + if stage in [Firmware.STAGE_START, Firmware.STAGE_WAITING]: + device.write('=') + stage = progress_stage(Firmware.STAGE_WAITING_ON_LOADER) + + # Enter bootloader + elif stage == Firmware.STAGE_BOOT: + device.write('=') + stage = progress_stage(Firmware.STAGE_LOAD) + + # Upload firmware + elif stage == Firmware.STAGE_UPLOADING: + if len(write_queue) > 0 and got_response == True: + got_response = False + device.write(write_queue.popleft()) + + except UploadError as err: + stage = progress_stage(Firmware.STAGE_ERROR, error=str(err)) else: - stage_callback(Firmware.STAGE_ERROR, error="Error entering bootloader.") + stage = progress_stage(Firmware.STAGE_DONE) diff --git a/bin/ad2-firmwareupload b/bin/ad2-firmwareupload index 228312c..68a9752 100755 --- a/bin/ad2-firmwareupload +++ b/bin/ad2-firmwareupload @@ -42,13 +42,12 @@ def main(): firmware = None baudrate = 115200 - if len(sys.argv) < 2: - print("Syntax: {0} [device path or hostname:port] [baudrate]".format(sys.argv[0])) + if len(sys.argv) < 3: + print("Syntax: {0} [device path or hostname:port] [baudrate=115200]".format(sys.argv[0])) sys.exit(1) firmware = sys.argv[1] - if len(sys.argv) > 2: - device = sys.argv[2] + device = sys.argv[2] if len(sys.argv) > 3: baudrate = sys.argv[3] @@ -61,10 +60,10 @@ def main(): if ':' in device: hostname, port = device.split(':') dev = alarmdecoder.devices.SocketDevice(interface=(hostname, int(port))) - dev.open(no_reader_thread=True) else: dev = alarmdecoder.devices.SerialDevice(interface=device) - dev.open(baudrate=baudrate, no_reader_thread=True) + + dev.open(baudrate=baudrate, no_reader_thread=True) time.sleep(3) alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware, debug=debug) From 615548f780ca07534e34cce870d26cc7fd96bd43 Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Thu, 23 Mar 2017 13:03:39 -0700 Subject: [PATCH 4/4] Added ability to differentiate between stay and away in on_arm events. --- alarmdecoder/decoder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/alarmdecoder/decoder.py b/alarmdecoder/decoder.py index 667ac34..5a88ba2 100644 --- a/alarmdecoder/decoder.py +++ b/alarmdecoder/decoder.py @@ -116,6 +116,7 @@ class AlarmDecoder(object): self._alarm_status = None self._bypass_status = None self._armed_status = None + self._armed_stay = False self._fire_status = (False, 0) self._battery_status = (False, 0) self._panic_status = False @@ -615,10 +616,11 @@ class AlarmDecoder(object): message_status = message.armed_away | message.armed_home if message_status != self._armed_status: self._armed_status, old_status = message_status, self._armed_status + self._armed_stay = message.armed_home if old_status is not None: if self._armed_status: - self.on_arm() + self.on_arm(stay=message.armed_home) else: self.on_disarm()