From 72abdb47c6f563f08283add085474f77aa7ddc75 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Fri, 13 Oct 2023 13:13:18 -0400 Subject: [PATCH] WIP initial code --- py_vista_turbo_serial/communicator.py | 87 +++++ py_vista_turbo_serial/events.py | 215 +++++++++++ py_vista_turbo_serial/messages.py | 15 +- py_vista_turbo_serial/state_monitor.py | 371 +++++++++++++++++++ py_vista_turbo_serial/tests/test_events.py | 37 ++ py_vista_turbo_serial/tests/test_messages.py | 5 +- setup.py | 4 +- 7 files changed, 722 insertions(+), 12 deletions(-) create mode 100644 py_vista_turbo_serial/communicator.py create mode 100644 py_vista_turbo_serial/events.py create mode 100644 py_vista_turbo_serial/state_monitor.py create mode 100644 py_vista_turbo_serial/tests/test_events.py diff --git a/py_vista_turbo_serial/communicator.py b/py_vista_turbo_serial/communicator.py new file mode 100644 index 0000000..7e1d3ea --- /dev/null +++ b/py_vista_turbo_serial/communicator.py @@ -0,0 +1,87 @@ +""" +The latest version of this package is available at: + + +################################################################################## +Copyright 2018 Jason Antman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +################################################################################## +While not legally required, I sincerely request that anyone who finds +bugs please submit them at or +to me via email, and that you send any contributions or improvements +either as a pull request on GitHub, or to me via email. +################################################################################## + +AUTHORS: +Jason Antman +################################################################################## +""" + +import logging +from typing import List, Generator + +from serial import Serial + +from py_vista_turbo_serial.messages import MessagePacket + +logger = logging.getLogger(__name__) + + +class Communicator: + """ + Class for handling communication with the alarm. + """ + + def __init__(self, port: str, timeout_sec: int = 1): + self._port: str = port + logger.debug('Opening serial connection on %s', port) + self.serial: Serial = Serial( + port, baudrate=9600, timeout=timeout_sec + ) # default 8N1 + logger.debug('Serial is connected') + self.outgoing: List[str] = [] + + def __del__(self): + logger.debug('Closing serial port') + self.serial.close() + logger.debug('Serial port closed') + + def send_message(self, msg: str): + logger.debug('Enqueueing message: %s', msg) + self.outgoing.append(msg) + + def communicate(self) -> Generator[MessagePacket, None, None]: + logger.info('Entering communicate() loop') + # at start, if we have a message to send, send it + if self.outgoing: + msg = self.outgoing.pop(0) + logger.info('Sending message: %s', msg) + self.serial.write(msg + '\r\n') + # this might be better with select(), but let's try this... + while True: + # @TODO handle exception on timeout + line = self.serial.readline().decode().strip() + logger.debug('Got line: %s', line) + yield MessagePacket.parse(line) + if self.outgoing: + msg = self.outgoing.pop(0) + logger.info('Sending message: %s', msg) + self.serial.write(msg + '\r\n') diff --git a/py_vista_turbo_serial/events.py b/py_vista_turbo_serial/events.py new file mode 100644 index 0000000..a6e0016 --- /dev/null +++ b/py_vista_turbo_serial/events.py @@ -0,0 +1,215 @@ +""" +The latest version of this package is available at: + + +################################################################################## +Copyright 2018 Jason Antman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +################################################################################## +While not legally required, I sincerely request that anyone who finds +bugs please submit them at or +to me via email, and that you send any contributions or improvements +either as a pull request on GitHub, or to me via email. +################################################################################## + +AUTHORS: +Jason Antman +################################################################################## +""" + + +class UnknownEventException(Exception): + pass + + +class SystemEvent: + + NAME: str = 'Unknown Event' + + CODE: int = 0 + + def __init__(self, zone_or_user: int): + self.zone_or_user: int = zone_or_user + + def __repr__(self) -> str: + return f'<{self.NAME}(zone_or_user={self.zone_or_user})>' + + @classmethod + def event_for_code( + cls, event_code: int, zone_or_user: int + ) -> 'SystemEvent': + for klass in cls.__subclasses__(): + if klass.CODE == event_code: + return klass(zone_or_user) + raise UnknownEventException(f'Unknown event code: {event_code}') + + +class AlarmEvent(SystemEvent): + pass + + +class PerimeterAlarm(AlarmEvent): + NAME: str = 'Perimeter Alarm' + CODE: int = 0 + + +class EntryExitAlarm(AlarmEvent): + NAME: str = 'Entry/Exit Alarm' + CODE: int = 1 + + +class InteriorFollowerAlarm(AlarmEvent): + NAME: str = 'Interior Follower Alarm' + CODE: int = 4 + + +class FireAlarm(AlarmEvent): + NAME: str = 'Fire Alarm' + CODE: int = 6 + + +class AudiblePanicAlarm(AlarmEvent): + NAME: str = 'Audible Panic Alarm' + CODE: int = 7 + + +class SilentPanicAlarm(AlarmEvent): + NAME: str = 'Silent Panic Alarm' + CODE: int = 8 + + +class Aux24hrAlarm(AlarmEvent): + NAME: str = '24-Hour Auxiliary' + CODE: int = 9 + + +class DuressAlarm(AlarmEvent): + NAME: str = 'Duress Alarm' + CODE: int = 0x0c + + +class OtherAlarmRestore(SystemEvent): + NAME: str = 'Other Alarm Restore' + CODE: int = 0x0e + + +class RfLowBattery(SystemEvent): + NAME: str = 'RF Low Battery' + CODE: int = 0x0f + + +class RfLowBatteryRestore(SystemEvent): + NAME: str = 'RF Low Battery Restore' + CODE: int = 0x10 + + +class OtherTrouble(SystemEvent): + NAME: str = 'Other Trouble' + CODE: int = 0x11 + + +class OtherTroubleRestore(SystemEvent): + NAME: str = 'Other Trouble Restore' + CODE: int = 0x12 + + +class ArmDisarmEvent(SystemEvent): + pass + + +class ArmStay(ArmDisarmEvent): + NAME: str = 'Arm Stay/Home' + CODE: int = 0x15 + + +class Disarm(ArmDisarmEvent): + NAME: str = 'Disarm' + CODE: int = 0x16 + + +class Arm(ArmDisarmEvent): + NAME: str = 'Arm' + CODE: int = 0x18 + + +class LowBattery(SystemEvent): + NAME: str = 'Low Battery' + CODE: int = 0x1a + + +class LowBatteryRestore(SystemEvent): + NAME: str = 'Low Battery Restore' + CODE: int = 0x1b + + +class AcFail(SystemEvent): + NAME: str = 'AC Fail' + CODE: int = 0x1c + + +class AcRestore(SystemEvent): + NAME: str = 'AC Restore' + CODE: int = 0x1d + + +class AlarmCancel(SystemEvent): + NAME: str = 'Alarm Cancel' + CODE: int = 0x20 + + +class OtherBypass(SystemEvent): + NAME: str = 'Other Bypass' + CODE: int = 0x21 + + +class OtherUnbypass(SystemEvent): + NAME: str = 'Other Unbypass' + CODE: int = 0x22 + + +class DayNightAlarm(SystemEvent): + NAME: str = 'Day/Night Alarm' + CODE: int = 0x23 + + +class DayNightRestore(SystemEvent): + NAME: str = 'Day/Night Restore' + CODE: int = 0x24 + + +class FailToDisarm(SystemEvent): + NAME: str = 'Fail to Disarm' + CODE: int = 0x27 + + +class FailToArm(SystemEvent): + NAME: str = 'Fail to Arm' + CODE: int = 0x28 + + +class FaultEvent(SystemEvent): + NAME: str = 'Fault' + CODE: int = 0x2b + + +class FaultRestoreEvent(SystemEvent): + NAME: str = 'Fault Restore' + CODE: int = 0x2c diff --git a/py_vista_turbo_serial/messages.py b/py_vista_turbo_serial/messages.py index 83630c1..1067b00 100644 --- a/py_vista_turbo_serial/messages.py +++ b/py_vista_turbo_serial/messages.py @@ -41,6 +41,8 @@ from enum import Enum from functools import total_ordering +from py_vista_turbo_serial.events import SystemEvent + logger = logging.getLogger(__name__) @@ -51,13 +53,6 @@ class PartitionState(Enum): AWAY = 3 -class SystemEventType(Enum): - - PERIMETER_ALARM = 0 - ENTRY_EXIT_ALARM = 1 - FAULT = 0x2b - - @total_ordering class ZoneState: @@ -380,14 +375,16 @@ class SystemEventNotification(MessagePacket): def __init__(self, raw_message: str, data: str, from_panel: bool): super().__init__(raw_message, data, from_panel) self._event_type: int = int(data[0:2], 16) - self.event_type: SystemEventType = SystemEventType(self._event_type) self.zone_or_user: int = int(data[2:4]) + self.event_type: SystemEvent = SystemEvent.event_for_code( + self._event_type, self.zone_or_user + ) self.minute: int = int(data[4:6]) self.hour: int = int(data[6:8]) self.day: int = int(data[8:10]) self.month: int = int(data[10:12]) def __repr__(self): - return (f'') diff --git a/py_vista_turbo_serial/state_monitor.py b/py_vista_turbo_serial/state_monitor.py new file mode 100644 index 0000000..61b0557 --- /dev/null +++ b/py_vista_turbo_serial/state_monitor.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python +""" +The latest version of this package is available at: + + +################################################################################## +Copyright 2018 Jason Antman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +################################################################################## +While not legally required, I sincerely request that anyone who finds +bugs please submit them at or +to me via email, and that you send any contributions or improvements +either as a pull request on GitHub, or to me via email. +################################################################################## + +AUTHORS: +Jason Antman +################################################################################## +""" + +import sys +import argparse +import logging +from typing import Dict, List, Set + +from py_vista_turbo_serial.communicator import Communicator +from py_vista_turbo_serial.messages import ( + MessagePacket, ArmAwayMessage, ArmHomeMessage, DisarmMessage, + ArmingStatusRequest, ArmingStatusReport, PartitionState, ZoneStatusRequest, + ZoneStatusReport, ZoneState, ZonePartitionRequest, ZonePartitionReport, + SystemEventNotification +) +from py_vista_turbo_serial.events import ( + AlarmEvent, OtherAlarmRestore, RfLowBattery, RfLowBatteryRestore, + OtherTrouble, OtherTroubleRestore, ArmDisarmEvent, ArmStay, Arm, Disarm, + LowBattery, LowBatteryRestore, AcFail, AcRestore, AlarmCancel, + OtherBypass, OtherUnbypass, FaultEvent, FaultRestoreEvent, SystemEvent +) + +logging.basicConfig( + level=logging.INFO, + format="[%(asctime)s %(levelname)s] %(message)s" +) +logger: logging.Logger = logging.getLogger() + + +class StateMonitor: + + def __init__(self, port: str): + self.panel: Communicator = Communicator(port=port) + self.arming_status: Dict[int, PartitionState] = {} + self.zone_status: Dict[int, ZoneState] = {} + self.zone_partitions: Dict[int, int] = {} + self.zone_troubles: Dict[int, Set[str]] = {} + + def _handle_alarm(self, evt: AlarmEvent): + logger.debug( + 'Event: %s in zone %d', evt.NAME, evt.zone_or_user + ) + if self.zone_status[evt.zone_or_user].alarm: + logger.warning( + 'Got %s event for zone %d but zone is already in alarm', + evt.NAME, evt.zone_or_user + ) + else: + logger.info( + 'CHANGE zone %d alarm state to True', evt.zone_or_user + ) + self.zone_status[evt.zone_or_user].alarm = True + + def _handle_alarm_restore(self, evt: OtherAlarmRestore): + logger.debug( + 'Got OtherAlarmRestore event for zone %d', evt.zone_or_user + ) + if self.zone_status[evt.zone_or_user].alarm: + logger.info( + 'CHANGE zone %d alarm state to False', evt.zone_or_user + ) + self.zone_status[evt.zone_or_user].alarm = False + else: + logger.warning( + 'Got %s event for zone %d but zone is not in alarm', + evt.NAME, evt.zone_or_user + ) + + def _handle_zone_trouble(self, evt: SystemEvent): + logger.debug( + 'Got %s event for zone %d', + evt.NAME, evt.zone_or_user + ) + if self.zone_status[evt.zone_or_user].trouble: + if evt.NAME in self.zone_troubles[evt.zone_or_user]: + logger.debug( + 'Got another %s message for zone %d', + evt.NAME, evt.zone_or_user + ) + else: + logger.info( + 'ADD zone %s trouble cause: %s', + evt.zone_or_user, evt.NAME + ) + self.zone_troubles[evt.zone_or_user].add(evt.NAME) + else: + logger.info( + 'CHANGE zone %d is in trouble: %s', + evt.zone_or_user, evt.NAME + ) + self.zone_troubles[evt.zone_or_user].add(evt.NAME) + self.zone_status[evt.zone_or_user].trouble = True + + def _handle_zone_trouble_restore(self, evt: SystemEvent): + logger.debug( + 'Got %s event for zone %d', + evt.NAME, evt.zone_or_user + ) + if self.zone_status[evt.zone_or_user].trouble: + if evt.NAME not in self.zone_troubles[evt.zone_or_user]: + logger.warning( + 'Got %s for zone %d but zone trouble causes are: %s', + evt.NAME, evt.zone_or_user, + self.zone_troubles[evt.zone_or_user] + ) + else: + logger.info( + 'REMOVE zone %s trouble cause: %s', + evt.zone_or_user, evt.NAME + ) + self.zone_troubles[evt.zone_or_user].remove(evt.NAME) + if not self.zone_troubles[evt.zone_or_user]: + logger.info( + 'CHANGE zone %d is no longer in trouble', evt.zone_or_user + ) + self.zone_status[evt.zone_or_user].trouble = False + else: + logger.warning( + 'Got %s for zone %d but zone is not in trouble', + evt.NAME, evt.zone_or_user + ) + + def _handle_zone_bypass(self, evt: SystemEvent): + if self.zone_status[evt.zone_or_user].bypassed: + logger.debug( + 'Got another %s message for zone %d', + evt.NAME, evt.zone_or_user + ) + return + logger.info( + 'CHANGE zone %d to Bypassed', evt.zone_or_user + ) + self.zone_status[evt.zone_or_user].bypassed = True + + def _handle_zone_unbypass(self, evt: SystemEvent): + if not self.zone_status[evt.zone_or_user].bypassed: + logger.debug( + 'Got another %s message for zone %d', + evt.NAME, evt.zone_or_user + ) + return + logger.info( + 'CHANGE zone %d to Unbypassed', evt.zone_or_user + ) + self.zone_status[evt.zone_or_user].bypassed = False + + def _handle_zone_fault(self, evt: SystemEvent): + raise NotImplementedError( + 'Not implemented; also how to handle NC/NO zones?' + ) + if self.zone_status[evt.zone_or_user].closed: + logger.debug( + 'Got another %s message for zone %d', + evt.NAME, evt.zone_or_user + ) + return + logger.info( + 'CHANGE zone %d to Bypassed', evt.zone_or_user + ) + self.zone_status[evt.zone_or_user].bypassed = True + + def _handle_zone_fault_restore(self, evt: SystemEvent): + raise NotImplementedError('not implemented - also NC/NO?') + if not self.zone_status[evt.zone_or_user].bypassed: + logger.debug( + 'Got another %s message for zone %d', + evt.NAME, evt.zone_or_user + ) + return + logger.info( + 'CHANGE zone %d to Unbypassed', evt.zone_or_user + ) + self.zone_status[evt.zone_or_user].bypassed = False + + def _handle_event(self, evt: SystemEvent): + logger.debug( + 'Got System Event Notification: %s', evt + ) + if isinstance(evt, AlarmEvent): + return self._handle_alarm(evt) + if isinstance(evt, OtherAlarmRestore): + return self._handle_alarm_restore(evt) + if ( + isinstance(evt, RfLowBattery) or + isinstance(evt, OtherTrouble) or + isinstance(evt, LowBattery) + ): + return self._handle_zone_trouble(evt) + if ( + isinstance(evt, RfLowBatteryRestore) or + isinstance(evt, OtherTroubleRestore) or + isinstance(evt, LowBatteryRestore) + ): + return self._handle_zone_trouble_restore(evt) + if isinstance(evt, OtherBypass): + return self._handle_zone_bypass(evt) + if isinstance(evt, OtherUnbypass): + return self._handle_zone_unbypass(evt) + if isinstance(evt, FaultEvent): + return self._handle_zone_fault(evt) + if isinstance(evt, FaultRestoreEvent): + return self._handle_zone_fault_restore(evt) + # @TODO ArmDisarmEvent (ArmStay, Disarm, Arm) + # @TODO AcFail, AcRestore + # @TODO AlarmCancel + # @TODO DayNightAlarm, DayNightDestore + # @TODO FailToDisarm + # @TODO FailToArm + logger.warning( + 'Got un-handled System Event Notification: %s', evt + ) + + def run(self): + # @TODO every N minutes/hours we want to re-send the status requests + # and re-populate our stored state + # queue commands to get initial state + self.panel.send_message(ArmingStatusRequest.generate()) + self.panel.send_message(ZoneStatusRequest.generate()) + self.panel.send_message(ZonePartitionRequest.generate()) + is_ready: bool = False + message: MessagePacket + for message in self.panel.communicate(): + if isinstance(message, ArmingStatusReport): + self.arming_status.update(message.partition_state) + logger.info( + 'Got initial partition arming status: %s', + self.arming_status + ) + elif isinstance(message, ZoneStatusReport): + self.zone_status.update(message.zones) + logger.info( + 'Got initial zone status: %s', self.zone_status + ) + self.zone_troubles = { + x: set() for x in self.zone_status.keys() + } + elif isinstance(message, ZonePartitionReport): + self.zone_partitions.update(message.partitions) + logger.info( + 'Got zone partition state: %s', self.zone_partitions + ) + elif isinstance(message, ArmAwayMessage): + logger.info( + 'Got Arm Away for user %s (code %s)', + message.user, message.user_code + ) + # @TODO how to handle arm away message + logger.error( + 'NOT IMPLEMENTED: how to handle ArmAwayMessage?' + ) + elif isinstance(message, ArmHomeMessage): + logger.info( + 'Got Arm Home for user %s (code %s)', + message.user, message.user_code + ) + # @TODO how to handle arm home message + logger.error( + 'NOT IMPLEMENTED: how to handle ArmHomeMessage?' + ) + elif isinstance(message, DisarmMessage): + logger.info( + 'Got Disarm for user %s (code %s)', + message.user, message.user_code + ) + # @TODO how to handle disarm message + logger.error( + 'NOT IMPLEMENTED: how to handle DisarmMessage?' + ) + elif isinstance(message, SystemEventNotification): + self._handle_event(message.event_type) + else: + logger.error( + 'Got un-handled message: %s', message + ) + if ( + (not is_ready) and + self.arming_status and self.zone_status and self.zone_partitions + ): + is_ready = True + logger.info('Initial state data is populated.') + + +def parse_args(argv): + p = argparse.ArgumentParser(description='Alarm state monitor logger') + p.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', + default=False, help='verbose output' + ) + p.add_argument( + 'PORT', action='store', type=str, default='/dev/ttyUSB0', + help='Serial port to connect to (default: /dev/ttyUSB0)' + ) + args = p.parse_args(argv) + return args + + +def set_log_info(l: logging.Logger): + """set logger level to INFO""" + set_log_level_format( + l, + logging.INFO, + '%(asctime)s %(levelname)s:%(name)s:%(message)s' + ) + + +def set_log_debug(l: logging.Logger): + """set logger level to DEBUG, and debug-level output format""" + set_log_level_format( + l, + logging.DEBUG, + "%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - " + "%(name)s.%(funcName)s() ] %(message)s" + ) + + +def set_log_level_format(lgr: logging.Logger, level: int, fmt: str): + """Set logger level and format.""" + formatter = logging.Formatter(fmt=fmt) + lgr.handlers[0].setFormatter(formatter) + lgr.setLevel(level) + + +def main(): + args = parse_args(sys.argv[1:]) + + # set logging level + if args.verbose: + set_log_debug(logger) + else: + set_log_info(logger) + + StateMonitor(args.PORT).run() + + +if __name__ == "__main__": + main() diff --git a/py_vista_turbo_serial/tests/test_events.py b/py_vista_turbo_serial/tests/test_events.py new file mode 100644 index 0000000..5b4c00c --- /dev/null +++ b/py_vista_turbo_serial/tests/test_events.py @@ -0,0 +1,37 @@ +""" +The latest version of this package is available at: + + +################################################################################## +Copyright 2018 Jason Antman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +################################################################################## +While not legally required, I sincerely request that anyone who finds +bugs please submit them at or +to me via email, and that you send any contributions or improvements +either as a pull request on GitHub, or to me via email. +################################################################################## + +AUTHORS: +Jason Antman +################################################################################## +""" + diff --git a/py_vista_turbo_serial/tests/test_messages.py b/py_vista_turbo_serial/tests/test_messages.py index 0db7b63..9cb5c7b 100644 --- a/py_vista_turbo_serial/tests/test_messages.py +++ b/py_vista_turbo_serial/tests/test_messages.py @@ -42,8 +42,9 @@ UnknownMessage, ArmAwayMessage, ArmHomeMessage, DisarmMessage, ArmingStatusRequest, ArmingStatusReport, PartitionState, ZoneStatusRequest, ZoneStatusReport, ZoneState, ZonePartitionRequest, ZonePartitionReport, - SystemEventNotification, SystemEventType + SystemEventNotification ) +from py_vista_turbo_serial.events import FaultEvent class TestZoneState: @@ -299,4 +300,4 @@ def test_zone_14_open(self): assert res.hour == 10 assert res.day == 21 assert res.month == 2 - assert res.event_type == SystemEventType.FAULT + assert isinstance(res.event_type, FaultEvent) diff --git a/setup.py b/setup.py index 2fc1f5b..2c09d1e 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,9 @@ with open('README.rst') as file: long_description = file.read() -requires = [] +requires = [ + 'pyserial==3.5', +] # @TODO - see: https://pypi.org/pypi?%3Aaction=list_classifiers classifiers = [