├── requirements.txt ├── alarmserver.conf ├── README.md ├── alarmtest.py └── alarmrx.py /requirements.txt: -------------------------------------------------------------------------------- 1 | daemon==1.2 2 | lockfile==0.12.2 3 | paho-mqtt==1.5.0 4 | pkg-resources==0.0.0 5 | -------------------------------------------------------------------------------- /alarmserver.conf: -------------------------------------------------------------------------------- 1 | [server] 2 | host = 0.0.0.0 3 | port = 10500 4 | log_file = /tmp/alarmserver.log 5 | pid_file = /tmp/alarmserver.pid 6 | 7 | [alarm] 8 | account_file = /etc/alarmserver/accounts.conf 9 | polling_interval = 2 10 | max_misses = 1 11 | 12 | [mqtt] 13 | host = 127.0.0.1 14 | port = 8883 15 | username = user 16 | password = password 17 | cafile = /etc/alarmserver/ca.crt 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alarm-server Configuration 2 | ## Installing 3 | alarm-server requires some python dependencies to run. 4 | 5 | ```bash 6 | pip install paho-mqtt 7 | pip install python-daemon 8 | ``` 9 | 10 | ## Configuring the Server 11 | ### MQTT Server Configuration 12 | - If your MQTT server is running locally or on the same box as the server listener, you can likely leave the settings as default. 13 | - If you need to adjust anything, you can do so in `alarmserver.conf` 14 | 15 | ### Configuring allowed panel addresses 16 | - Allowed panel addresses should be entered in tab separated format innto `/etc/alarmserver/accounts.conf` (or wherever you configure this file to be - see `alarmserver.conf`) 17 | - Example: `1234 192.168.1.2` where `1234` is the account number you configure in Wintex/On Panel. 18 | 19 | ## Configuring the Alarm Panel 20 | The simplest method to configure the server and panel communication is to use Wintex to configure the ARC settings. 21 | ### Using Wintex 22 | > Disclaimer! It's easy to overwrite configurations, misconfigure or break your panel's current working configuration using Wintex. Please be sure you're confident in using wintex before proceeding - and always remember to take a backup, or at the very least receive all data first! 23 | - Connect to your Panel 24 | - Navigate to the Comms page 25 | - Select the `ARCs` tab 26 | - Find an empty ARC slot 27 | - Enter the IP address and port of your server in the `Pri Tel No` field in the format `ip/port` (For example `192.168.1.92/10500`) 28 | - Ensure `Connect via IP` is checked 29 | - Save / send current page. 30 | 31 | ## Running the Server 32 | ```bash 33 | python alarmrx.py 34 | ``` 35 | 36 | ## Subscribing to Messages 37 | Topics: 38 | - /alarms/{account-num}/event 39 | - /alarms/{account-num}/message 40 | - /alarms/{account-num}/status 41 | -------------------------------------------------------------------------------- /alarmtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Texecom Alarm Receiving Server - Test Script 4 | # Copyright 2016-2020 Mike Stirling 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import socket 20 | from argparse import ArgumentParser 21 | from functools import reduce 22 | 23 | parser = ArgumentParser(description='Test tool for SIA and ContactID') 24 | parser.add_argument('test', type=str, nargs=1, 25 | help='Name of test to execute: poll, arm, disarm, alarm, panic') 26 | parser.add_argument('-H', dest='host', type=str, 27 | help='ARC host address', default='127.0.0.1') 28 | parser.add_argument('-P', dest='port', type=int, 29 | help='ARC port number', default=10500) 30 | parser.add_argument('-A', dest='account', type=int, 31 | help='User account number', default='1000') 32 | parser.add_argument('-n', dest='number', type=int, 33 | help='User or zone number', default=0) 34 | parser.add_argument('-N', dest='name', type=str, 35 | help='User or zone name', default='') 36 | parser.add_argument('-m', dest='mode', type=int, 37 | help='Operating mode: 2=ContactID, 3=SIA', default=2) 38 | parser.add_argument('-f', dest='flags', type=int, 39 | help='Poller flags', default=0) 40 | 41 | args = parser.parse_args() 42 | print(args) 43 | 44 | def poll(sock, account, flags): 45 | sock.send(("POLL%04u#%c\0\0\0\r\n" % (account, chr(flags))).encode('ascii')) 46 | 47 | reply = sock.recv(1024) 48 | if reply[0:3] == b'[P]' and reply[5:] == b'\x06\r\n': 49 | interval = reply[4] 50 | print("POLL OK") 51 | print("Server requested polling interval %u minutes" % (interval)) 52 | else: 53 | print("Bad reply to poll: %s" % (reply)) 54 | 55 | def contactid(sock, account, qualifier, event, zone_or_user): 56 | account = ("%04u" % (account)).replace('0','A') 57 | msg = account + "18%01u%03u01%03u" % (qualifier, event, zone_or_user) 58 | 59 | # Calculate check digit (0 is valued as 10) 60 | checksum = 0 61 | for c in msg: 62 | if c == '0': 63 | checksum += 10 64 | else: 65 | checksum += int(c, 16) 66 | checkdigit = "%01X" % (15 - (checksum % 15)) 67 | if checkdigit == 'A': 68 | checkdigit = '0' 69 | 70 | # Wrap in Texecom wrapper 71 | sock.send(('2' + msg + checkdigit + '\r\n').encode('ascii')) 72 | 73 | # Wait for ACK 74 | reply = sock.recv(1024) 75 | if reply == b'2\x06\r\n': 76 | print("Ack received OK") 77 | else: 78 | print("Bad reply to message: %s" % (reply)) 79 | 80 | 81 | def sia(sock, account, event, zone_or_user, name): 82 | recs = [ 83 | "#%04u" % (account), 84 | "Nri1%2s%03u" % (event, zone_or_user), 85 | ] 86 | if name: 87 | recs = recs + [ "A%s" % (name) ] 88 | 89 | # Add start byte and checksum for each record 90 | msg = b'' 91 | recs = [r.encode('ascii') for r in recs] # Convert records from str to binary 92 | for rec in recs: 93 | rec = bytes([0xc0 + len(rec) - 1]) + rec 94 | checksum = 0xff ^ reduce(lambda x,y: x^y, rec) 95 | msg += rec + bytes([checksum]) 96 | 97 | # Add terminator and wrap in Texecom wrapper for sending 98 | sock.send(b'3' + msg + b'\x40\x30\x8f\r\n') 99 | 100 | # Wait for ACK 101 | reply = sock.recv(1024) 102 | if reply == b'3\x06\r\n': 103 | print("Ack received OK") 104 | else: 105 | print("Bad reply to message: %s" % (reply)) 106 | 107 | 108 | # List of tests along with ContactID and SIA event codes 109 | TESTS = { 110 | 'arm' : (3, 401, 'CL'), 111 | 'disarm' : (1, 401, 'OP'), 112 | 'alarm' : (1, 130, 'BA'), 113 | 'panic' : (1, 123, 'PA'), 114 | } 115 | 116 | if __name__=='__main__': 117 | # Open socket 118 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 119 | sock.connect((args.host, args.port)) 120 | sock.settimeout(2.0) 121 | 122 | test = args.test[0] 123 | if test == 'poll': 124 | poll(sock, args.account, args.flags) 125 | else: 126 | try: 127 | (cid_qual, cid_event, sia_event) = TESTS[test] 128 | 129 | if args.mode == 2: 130 | contactid(sock, args.account, cid_qual, cid_event, args.number) 131 | elif args.mode == 3: 132 | sia(sock, args.account, sia_event, args.number, args.name) 133 | else: 134 | print("Bad mode:", args.mode) 135 | except KeyError: 136 | print("Unknown test:", test) 137 | 138 | sock.close() 139 | -------------------------------------------------------------------------------- /alarmrx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Texecom Alarm Receiving Server 4 | # Copyright 2016-2020 Mike Stirling 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import socketserver 20 | import configparser 21 | import logging 22 | import sys 23 | import daemon 24 | import lockfile 25 | import json 26 | import paho.mqtt.client as mqtt 27 | import time 28 | from datetime import datetime, timedelta 29 | from threading import Thread, Lock 30 | from binascii import hexlify 31 | from functools import reduce 32 | 33 | APP_NAME = 'alarmserver' 34 | CONFIG_FILE = '/etc/alarmserver/alarmserver.conf' 35 | 36 | # Configuration defaults 37 | DEFAULTS = { 38 | 'server' : { 39 | 'host' : '0.0.0.0', 40 | 'port' : 10500, 41 | 'log_file' : '/tmp/alarmserver.log', 42 | 'pid_file' : '/tmp/alarmserver.pid', 43 | }, 44 | 'alarm' : { 45 | 'polling_interval' : 2, 46 | 'max_misses' : 1, 47 | 'account_file' : '/etc/alarmserver/accounts.conf' 48 | }, 49 | 'mqtt' : { 50 | 'host' : '127.0.0.1', 51 | 'port' : 1883, 52 | 'username' : None, 53 | 'password' : None, 54 | 'cafile' : None 55 | }, 56 | } 57 | 58 | config = configparser.ConfigParser() 59 | config.read(CONFIG_FILE) 60 | 61 | def get_config(section, option): 62 | try: 63 | value = config.get(section, option) 64 | except (configparser.NoOptionError, configparser.NoSectionError): 65 | value = DEFAULTS[section][option] 66 | return value 67 | 68 | LOG_FILE = get_config('server', 'log_file') 69 | PID_FILE = get_config('server', 'pid_file') 70 | 71 | # Configure logging 72 | LOG_FORMAT = '%(asctime)-15s %(levelname)-6s %(tag)-15s %(message)s' 73 | logging.basicConfig(level=logging.DEBUG, format = LOG_FORMAT) 74 | logger = logging.getLogger(APP_NAME) 75 | 76 | fh = logging.FileHandler(LOG_FILE, 'a') 77 | fh.setLevel(logging.DEBUG) 78 | fh.setFormatter(logging.Formatter(LOG_FORMAT)) 79 | logger.addHandler(fh) 80 | 81 | class AccountManager(object): 82 | error = lambda self, acc, msg: logger.error(msg, extra={'tag': acc}) 83 | info = lambda self, acc, msg: logger.info(msg, extra={'tag': acc}) 84 | debug = lambda self, acc, msg: logger.debug(msg, extra={'tag': acc}) 85 | 86 | # Polling timer tolerance (seconds) 87 | POLLING_TOLERANCE = 30 88 | 89 | def __init__(self): 90 | self.MAX_MISSES = int(get_config('alarm', 'max_misses')) 91 | self.ACCOUNT_FILENAME = get_config('alarm', 'account_file') 92 | self.POLLING_INTERVAL = int(get_config('alarm', 'polling_interval')) 93 | 94 | self.account_lock = Lock() 95 | self.watchdog_running = False 96 | self.polling_timeout = timedelta(minutes=self.POLLING_INTERVAL, seconds=self.POLLING_TOLERANCE) 97 | 98 | # Open and load list of accounts 99 | # File should be formatted as 100 | self.accounts = {} 101 | try: 102 | with open(self.ACCOUNT_FILENAME) as f: 103 | while True: 104 | entry = f.readline().strip() 105 | if not entry: 106 | break 107 | if entry[0] == '#': 108 | continue 109 | fields = entry.split() 110 | if len(fields) < 2: 111 | # Skip if row has less than 2 columns 112 | continue 113 | (account, ip) = fields[:2] 114 | self.accounts[account] = dict(ip=ip, last_poll=None, timeout=None, missed=0) 115 | except IOError: 116 | # Missing account file just means no accounts 117 | self.error('', 'Failed to open account file') 118 | 119 | def publish_polling_event(self, account, state): 120 | # Post event message to broker (not retained) 121 | info = { 122 | 'client_ip': '', 123 | 'client_port': 0, 124 | 'account': int(account), 125 | 'timestamp': datetime.now().isoformat(), 126 | 'area': 0, 127 | 'value': 0, 128 | 'value_type': '', 129 | 'extra_text': '', 130 | } 131 | if state: 132 | info['event'] = 'Polling restored' 133 | msg = 'Polling restored' 134 | else: 135 | info['event'] = 'Polling timeout' 136 | msg = 'Polling timeout' 137 | self.mqtt_client.publish("alarms/%s/event" % (account), json.dumps(info), qos=1, retain=False) 138 | self.mqtt_client.publish("alarms/%s/message" % (account), msg, qos=1, retain=False) 139 | 140 | def start_polling_watchdog(self): 141 | # Initialise polling timeouts 142 | for k,acc in self.accounts.items(): 143 | acc['timeout'] = datetime.now() + self.polling_timeout 144 | 145 | # Start timer for polling monitor 146 | self.watchdog_running = True 147 | self.watchdog_thread = Thread(target = self.watchdog_func) 148 | self.watchdog_thread.daemon = True 149 | self.watchdog_thread.start() 150 | 151 | def stop_polling_watchdog(self): 152 | self.watchdog_running = False 153 | self.watchdog_thread.join() 154 | 155 | def watchdog_func(self): 156 | while self.watchdog_running: 157 | self.account_lock.acquire() 158 | 159 | # Check for missed polls every few seconds. Doesn't really 160 | # matter if we are a few seconds late 161 | now = datetime.now() 162 | for k,acc in self.accounts.items(): 163 | if acc['timeout'] < now: 164 | # Account has missed a poll event 165 | acc['missed'] = acc['missed'] + 1 166 | acc['timeout'] = now + self.polling_timeout 167 | self.info(k, "Polling deadline missed (%d/%d)" % (acc['missed'], self.MAX_MISSES)) 168 | if acc['missed'] == self.MAX_MISSES: 169 | # Generate polling error event 170 | self.error(k, "Polling alert") 171 | self.publish_polling_event(k, False) 172 | 173 | self.account_lock.release() 174 | time.sleep(5) 175 | 176 | def polled(self, account): 177 | self.account_lock.acquire() 178 | 179 | try: 180 | self.info(account, 'Poll received') 181 | acc = self.accounts[account] 182 | if acc['missed'] > 0: 183 | # Generate polling restored event 184 | self.info(account, "Polling recovered after %d misses" % (acc['missed'])) 185 | self.publish_polling_event(account, True) 186 | 187 | now = datetime.now() 188 | acc['last_poll'] = now 189 | acc['timeout'] = now + self.polling_timeout 190 | acc['missed'] = 0 191 | except KeyError: 192 | self.error(account, 'Unknown account') 193 | 194 | self.account_lock.release() 195 | 196 | def get_ip(self, account): 197 | try: 198 | return self.accounts[account]['ip'] 199 | except KeyError: 200 | self.error(account, 'Unknown account') 201 | return None 202 | 203 | def get_polling_interval(self, account): 204 | return self.POLLING_INTERVAL 205 | 206 | 207 | class EventParser(object): 208 | error = lambda self, msg: logger.error(type(self).__name__ + ': ' + msg, extra={'tag': self.client_ip}) 209 | info = lambda self, msg: logger.info(type(self).__name__ + ': ' + msg, extra={'tag': self.client_ip}) 210 | debug = lambda self, msg: logger.debug(type(self).__name__ + ': ' + msg, extra={'tag': self.client_ip}) 211 | 212 | # Defaults 213 | client_ip = '0.0.0.0' 214 | account_number = '' 215 | area = 0 216 | event = '' 217 | description = '' 218 | value = 0 219 | value_name = '' 220 | extra_text = '' 221 | 222 | def __init__(self): 223 | pass 224 | 225 | class ContactId(EventParser): 226 | QUALIFIERS = { 227 | 1 : 'Event/Activated', 228 | 3 : 'Restore/Secured', 229 | 6 : 'Status' 230 | } 231 | EVENTS = { 232 | 100: 'Medical', 233 | 110: 'Fire', 234 | 120: 'Panic', 235 | 121: 'Duress', 236 | 122: 'Silent Attack', 237 | 123: 'Audible Attack', 238 | 130: 'Intruder', 239 | 131: 'Perimeter', 240 | 132: 'Interior', 241 | 133: '24 Hour', 242 | 134: 'Entry/Exit', 243 | 135: 'Day/Night', 244 | 136: 'Outdoor', 245 | 137: 'Zone Tamper', 246 | 139: 'Confirmed Alarm', 247 | 145: 'System Tamper', 248 | 249 | 300: 'System Trouble', 250 | 301: 'AC Lost', 251 | 302: 'Low Battery', 252 | 305: 'System Power Up', 253 | 320: 'Mains Over-voltage', 254 | 333: 'Network Failure', 255 | 351: 'ATS Path Fault', 256 | 354: 'Failed to Communicate', 257 | 258 | 400: 'Arm/Disarm', 259 | 401: 'Arm/Disarm by User', 260 | 403: 'Automatic Arm/Disarm', 261 | 406: 'Alarm Abort', 262 | 407: 'Remote Arm/Disarm', 263 | 408: 'Quick Arm', 264 | 265 | 411: 'Download Start', 266 | 412: 'Download End', 267 | 441: 'Part Arm', 268 | 269 | 457: 'Exit Error', 270 | 459: 'Recent Closing', 271 | 570: 'Zone Locked Out', 272 | 273 | 601: 'Manual Test', 274 | 602: 'Periodic Test', 275 | 607: 'User Walk Test', 276 | 277 | 623: 'Log Capacity Alert', 278 | 625: 'Date/Time Changed', 279 | 627: 'Program Mode Entry', 280 | 628: 'Program Mode Exit', 281 | } 282 | 283 | def __init__(self, client_ip, msg): 284 | self.client_ip = client_ip 285 | 286 | # Validate 287 | if len(msg) != 16: 288 | self.error("Invalid message size %u" % (len(msg))) 289 | return 290 | if msg[4:6] != b'18' and msg[4:6] != b'98': 291 | self.error("Invalid message type %s" % (msg[4:6])) 292 | return 293 | 294 | # Parse fields 295 | account = msg[0:4].decode('ascii').replace('A','0') 296 | try: 297 | qualifier = int(msg[6:7]) 298 | event = int(msg[7:10]) 299 | group = int(msg[10:12]) 300 | value = int(msg[12:15]) 301 | except ValueError: 302 | self.error("Parse error") 303 | return 304 | 305 | try: 306 | qualstr = ' ' + self.QUALIFIERS[qualifier] 307 | except KeyError: 308 | qualstr = '' 309 | try: 310 | eventstr = self.EVENTS[event] 311 | except KeyError: 312 | eventstr = "Unknown Event %03u" % (event) 313 | 314 | # Populate class properties 315 | self.account_number = account 316 | self.area = group 317 | self.event = eventstr + qualstr 318 | self.description = eventstr + qualstr 319 | self.value = value 320 | self.value_name = 'Zone/User' 321 | 322 | class SIA(EventParser): 323 | EVENTS = { 324 | 'AA': ("Alarm - Panel Substitution", "An attempt to substitute an alternate alarm panel for a secure panel has been made", "Condition Number"), 325 | 'AB': ("Abort", "An event message was not sent due to User action", "Zone"), 326 | 'AN': ("Analog Restoral", "An analog fire sensor has been restored to normal operation", "Zone"), 327 | 'AR': ("AC Restoral", "AC power has been restored", ""), 328 | 'AS': ("Analog Service", "An analog fire sensor needs to be cleaned or calibrated", "Zone"), 329 | 'AT': ("AC Trouble", "AC power has been failed", ""), 330 | 'BA': ("Burglary Alarm", "Burglary zone has been violated while armed", "Zone"), 331 | 'BB': ("Burglary Bypass", "Burglary zone has been bypassed", "Zone"), 332 | 'BC': ("Burglary Cancel", "Alarm has been cancelled by authorized user", "User"), 333 | 'BD': ("Swinger Trouble", "A non-fire zone has been violated after a Swinger Shutdown on the zone", "Zone"), 334 | 'BE': ("Swinger Trouble Restore", "A non-fire zone restores to normal from a Swinger Trouble state", "Zone"), 335 | 'BG': ("Unverified Event - Burglary", "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal", "Zone"), 336 | 'BH': ("Burglary Alarm Restore", "Alarm condition eliminated", "Zone"), 337 | 'BJ': ("Burglary Trouble Restore", "Trouble condition eliminated", "Zone"), 338 | 'BM': ("Burglary Alarm - Cross Point", "Burglary alarm w/cross point also in alarm - alarm verified", "Zone"), 339 | 'BR': ("Burglary Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 340 | 'BS': ("Burglary Supervisory", "Unsafe intrusion detection system condition", "Zone"), 341 | 'BT': ("Burglary Trouble", "Burglary zone disabled by fault", "Zone"), 342 | 'BU': ("Burglary Unbypass", "Zone bypass has been removed", "Zone"), 343 | 'BV': ("Burglary Verified", "A burglary alarm has occurred and been verified within programmed conditions. (zone or point not sent)", "Area"), 344 | 'BX': ("Burglary Test", "Burglary zone activated during testing", "Zone"), 345 | 'BZ': ("Missing Supervision", "A non-fire Supervisory point has gone missing", "Zone"), 346 | 'CA': ("Automatic Closing", "System armed automatically", "Area"), 347 | 'CD': ("Closing Delinquent", "The system has not been armed for a programmed amount of time", "Area"), 348 | 'CE': ("Closing Extend", "Extend closing time", "User"), 349 | 'CF': ("Forced Closing", "System armed, some zones not ready", "User"), 350 | 'CG': ("Close Area", "System has been partially armed", "Area"), 351 | 'CI': ("Fail to Close", "An area has not been armed at the end of the closing window", "Area"), 352 | 'CJ': ("Late Close", "An area was armed after the closing window", "User"), 353 | 'CK': ("Early Close", "An area was armed before the closing window", "User"), 354 | 'CL': ("Closing Report", "System armed, normal", "User"), 355 | 'CM': ("Missing Alarm - Recent Closing", "A point has gone missing within 2 minutes of closing", "Zone"), 356 | 'CO': ("Command Sent", "A command has been sent to an expansion/peripheral device", "Condition Number"), 357 | 'CP': ("Automatic Closing", "System armed automatically", "User"), 358 | 'CQ': ("Remote Closing", "The system was armed from a remote location", "User"), 359 | 'CR': ("Recent Closing", "An alarm occurred within five minutes after the system was closed", "User"), 360 | 'CS': ("Closing Keyswitch", "Account has been armed by keyswitch", "Zone"), 361 | 'CT': ("Late to Open", "System was not disarmed on time", "Area"), 362 | 'CW': ("Was Force Armed", "Header for a force armed session, forced point msgs may follow", "Area"), 363 | 'CX': ("Custom Function Executed", "The panel has executed a preprogrammed set of instructions", "Custom Function"), 364 | 'CZ': ("Point Closing", "A point, as opposed to a whole area or account, has closed", "Zone"), 365 | 'DA': ("Card Assigned", "An access ID has been added to the controller", "User"), 366 | 'DB': ("Card Deleted", "An access ID has been deleted from the controller", "User"), 367 | 'DC': ("Access Closed", "Access to all users prohibited", "Door"), 368 | 'DD': ("Access Denied", "Access denied, unknown code", "Door"), 369 | 'DE': ("Request to Enter", "An access point was opened via a Request to Enter device", "Door"), 370 | 'DF': ("Door Forced", "Door opened without access request", "Door"), 371 | 'DG': ("Access Granted", "Door access granted", "Door"), 372 | 'DH': ("Door Left Open - Restoral", "An access point in a Door Left Open state has restored", "Door"), 373 | 'DI': ("Access Denied - Passback", "Access denied because credential has not exited area before attempting to re-enter same area", "Door"), 374 | 'DJ': ("Door Forced - Trouble", "An access point has been forced open in an unarmed area", "Door"), 375 | 'DK': ("Access Lockout", "Access denied, known code", "Door"), 376 | 'DL': ("Door Left Open - Alarm", "An open access point when open time expired in an armed area", "Door"), 377 | 'DM': ("Door Left Open - Trouble", "An open access point when open time expired in an unarmed area", "Door"), 378 | 'DN': ("Door Left Open (non-alarm, non-trouble)", "An access point was open when the door cycle time expired", "Door"), 379 | 'DO': ("Access Open", "Access to authorized users allowed", "Door"), 380 | 'DP': ("Access Denied - Unauthorized Time", "An access request was denied because the request is occurring outside the user's authorized time window(s)", "Door"), 381 | 'DQ': ("Access Denied Unauthorized Arming State", "An access request was denied because the user was not authorized in this area when the area was armed", "Door"), 382 | 'DR': ("Door Restoral", "Access alarm/trouble condition eliminated", "Door"), 383 | 'DS': ("Door Station", "Identifies door for next report", "Door"), 384 | 'DT': ("Access Trouble", "Access system trouble", ""), 385 | 'DU': ("Dealer ID", "Dealer ID number", "Dealer ID"), 386 | 'DV': ("Access Denied Unauthorized Entry Level", "An access request was denied because the user is not authorized in this area", "Door"), 387 | 'DW': ("Access Denied - Interlock", "An access request was denied because the doors associated Interlock point is open", "Door"), 388 | 'DX': ("Request to Exit", "An access point was opened via a Request to Exit device", "Door"), 389 | 'DY': ("Door Locked", "The door's lock has been engaged", "Door"), 390 | 'DZ': ("Access Denied - Door Secured", "An access request was denied because the door has been placed in an Access Closed state", "Door"), 391 | 'EA': ("Exit Alarm", "An exit zone remained violated at the end of the exit delay period", "Zone"), 392 | 'EE': ("Exit Error", "An exit zone remained violated at the end of the exit delay period", "User"), 393 | 'EJ': ("Expansion Tamper Restore", "Expansion device tamper restoral", "Device"), 394 | 'EM': ("Expansion Device Missing", "Expansion device missing", "Device"), 395 | 'EN': ("Expansion Missing Restore", "Expansion device communications re-established", "Device"), 396 | 'ER': ("Expansion Restoral", "Expansion device trouble eliminated", "Expander"), 397 | 'ES': ("Expansion Device Tamper", "Expansion device enclosure tamper", "Device"), 398 | 'ET': ("Expansion Trouble", "Expansion device trouble", "Expander"), 399 | 'EX': ("External Device Condition", "A specific reportable condition is detected on an external device", "Device"), 400 | 'EZ': ("Missing Alarm - Exit Error", "A point remained missing at the end of the exit delay period", "Zone"), 401 | 'FA': ("Fire Alarm", "Fire condition detected", "Zone"), 402 | 'FB': ("Fire Bypass", "Zone has been bypassed", "Zone"), 403 | 'FC': ("Fire Cancel", "A Fire Alarm has been cancelled by an authorized person", "Zone"), 404 | 'FG': ("Unverified Event - Fire", "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal", "Zone"), 405 | 'FH': ("Fire Alarm Restore", "Alarm condition eliminated", "Zone"), 406 | 'FI': ("Fire Test Begin", "The transmitter area's fire test has begun", "Area"), 407 | 'FJ': ("Fire Trouble Restore", "Trouble condition eliminated", "Zone"), 408 | 'FK': ("Fire Test End", "The transmitter area's fire test has ended", "Area"), 409 | 'FL': ("Fire Alarm Silenced", "The fire panel's sounder was silenced by command", "Zone"), 410 | 'FM': ("Fire Alarm - Cross Point", "Fire Alarm with Cross Point also in alarm verifying the Fire Alarm", "Zone"), 411 | 'FQ': ("Fire Supervisory Trouble Restore", "A fire supervisory zone that was in trouble condition has now restored to normal", "Zone"), 412 | 'FR': ("Fire Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 413 | 'FS': ("Fire Supervisory", "Unsafe fire detection system condition", "Zone"), 414 | 'FT': ("Fire Trouble", "Zone disabled by fault", "Zone"), 415 | 'FU': ("Fire Unbypass", "Bypass has been removed", "Zone"), 416 | 'FV': ("Fire Supervision Restore", "A fire supervision zone that was in alarm has restored to normal", "Zone"), 417 | 'FW': ("Fire Supervisory Trouble", "A fire supervisory zone is now in a trouble condition", "Zone"), 418 | 'FX': ("Fire Test", "Fire zone activated during test", "Zone"), 419 | 'FY': ("Missing Fire Trouble", "A fire point is now logically missing", "Zone"), 420 | 'FZ': ("Missing Fire Supervision", "A Fire Supervisory point has gone missing", "Zone"), 421 | 'GA': ("Gas Alarm", "Gas alarm condition detected", "Zone"), 422 | 'GB': ("Gas Bypass", "Zone has been bypassed", "Zone"), 423 | 'GH': ("Gas Alarm Restore", "Alarm condition eliminated", "Zone"), 424 | 'GJ': ("Gas Trouble Restore", "Trouble condition eliminated", "Zone"), 425 | 'GR': ("Gas Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 426 | 'GS': ("Gas Supervisory", "Unsafe gas detection system condition", "Zone"), 427 | 'GT': ("Gas Trouble", "Zone disabled by fault", "Zone"), 428 | 'GU': ("Gas Unbypass", "Bypass has been removed", "Zone"), 429 | 'GX': ("Gas Test", "Zone activated during test", "Zone"), 430 | 'HA': ("Holdup Alarm", "Silent alarm, user under duress", "Zone"), 431 | 'HB': ("Holdup Bypass", "Zone has been bypassed", "Zone"), 432 | 'HH': ("Holdup Alarm Restore", "Alarm condition eliminated", "Zone"), 433 | 'HJ': ("Holdup Trouble Restore", "Trouble condition eliminated", "Zone"), 434 | 'HR': ("Holdup Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 435 | 'HS': ("Holdup Supervisory", "Unsafe holdup system condition", "Zone"), 436 | 'HT': ("Holdup Trouble", "Zone disabled by fault", "Zone"), 437 | 'HU': ("Holdup Unbypass", "Bypass has been removed", "Zone"), 438 | 'IA': ("Equipment Failure Condition", "A specific, reportable condition is detected on a device", "Zone"), 439 | 'IR': ("Equipment Fail - Restoral", "The equipment condition has been restored to normal", "Zone"), 440 | 'JA': ("User Code Tamper", "Too many unsuccessful attempts have been made to enter a user ID", "Area"), 441 | 'JD': ("Date Changed", "The date was changed in the transmitter/receiver", "User"), 442 | 'JH': ("Holiday Changed", "The transmitter's holiday schedule has been changed", "User"), 443 | 'JK': ("Latchkey Alert", "A designated user passcode has not been entered during a scheduled time window", "User"), 444 | 'JL': ("Log Threshold", "The transmitter's log memory has reached its threshold level", ""), 445 | 'JO': ("Log Overflow", "The transmitter's log memory has overflowed", ""), 446 | 'JP': ("User On Premises", "A designated user passcode has been used to gain access to the premises.", "User"), 447 | 'JR': ("Schedule Executed", "An automatic scheduled event was executed", "Area"), 448 | 'JS': ("Schedule Changed", "An automatic schedule was changed", "User"), 449 | 'JT': ("Time Changed", "The time was changed in the transmitter/receiver", "User"), 450 | 'JV': ("User Code Changed", "A user's code has been changed", "User"), 451 | 'JX': ("User Code Deleted", "A user's code has been removed", "User"), 452 | 'JY': ("User Code Added", "A user's code has been added", "User"), 453 | 'JZ': ("User Level Set", "A user's authority level has been set", "User"), 454 | 'KA': ("Heat Alarm", "High temperature detected on premise", "Zone"), 455 | 'KB': ("Heat Bypass", "Zone has been bypassed", "Zone"), 456 | 'KH': ("Heat Alarm Restore", "Alarm condition eliminated", "Zone"), 457 | 'KJ': ("Heat Trouble Restore", "Trouble condition eliminated", "Zone"), 458 | 'KR': ("Heat Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 459 | 'KS': ("Heat Supervisory", "Unsafe heat detection system condition", "Zone"), 460 | 'KT': ("Heat Trouble", "Zone disabled by fault", "Zone"), 461 | 'KU': ("Heat Unbypass", "Bypass has been removed", "Zone"), 462 | 'LB': ("Local Program", "Begin local programming", ""), 463 | 'LD': ("Local Program Denied", "Access code incorrect", ""), 464 | 'LE': ("Listen-in Ended", "The listen-in session has been terminated", ""), 465 | 'LF': ("Listen-in Begin", "The listen-in session with the RECEIVER has begun", ""), 466 | 'LR': ("Phone Line Restoral", "Phone line restored to service", "Line"), 467 | 'LS': ("Local Program Success", "Local programming successful", ""), 468 | 'LT': ("Phone Line Trouble", "Phone line trouble report", "Line"), 469 | 'LU': ("Local Program Fail", "Local programming unsuccessful", ""), 470 | 'LX': ("Local Programming Ended", "A local programming session has been terminated", ""), 471 | 'MA': ("Medical Alarm", "Emergency assistance request", "Zone"), 472 | 'MB': ("Medical Bypass", "Zone has been bypassed", "Zone"), 473 | 'MH': ("Medical Alarm Restore", "Alarm condition eliminated", "Zone"), 474 | 'MI': ("Message", "A canned message is being sent", "Message"), 475 | 'MJ': ("Medical Trouble Restore", "Trouble condition eliminated", "Zone"), 476 | 'MR': ("Medical Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 477 | 'MS': ("Medical Supervisory", "Unsafe system condition exists", "Zone"), 478 | 'MT': ("Medical Trouble", "Zone disabled by fault", "Zone"), 479 | 'MU': ("Medical Unbypass", "Bypass has been removed", "Zone"), 480 | 'NA': ("No Activity", "There has been no zone activity for a programmed amount of time", "Zone"), 481 | 'NC': ("Network Condition", "A communications network has a specific reportable condition", "Network"), 482 | 'NF': ("Forced Perimeter Arm", "Some zones/points not ready", "Area"), 483 | 'NL': ("Perimeter Armed", "An area has been perimeter armed", "Area"), 484 | 'NM': ("Perimeter Armed, User Defined", "A user defined area has been perimeter armed", "Area"), 485 | 'NR': ("Network Restoral", "A communications network has returned to normal operation", "Network"), 486 | 'NS': ("Activity Resumed", "A zone has detected activity after an alert", "Zone"), 487 | 'NT': ("Network Failure", "A communications network has failed", "Network"), 488 | 'OA': ("Automatic Opening", "System has disarmed automatically", "Area"), 489 | 'OC': ("Cancel Report", "Untyped zone cancel", "User"), 490 | 'OG': ("Open Area", "System has been partially disarmed", "Area"), 491 | 'OH': ("Early to Open from Alarm", "An area in alarm was disarmed before the opening window", "User"), 492 | 'OI': ("Fail to Open", "An area has not been armed at the end of the opening window", "Area"), 493 | 'OJ': ("Late Open", "An area was disarmed after the opening window", "User"), 494 | 'OK': ("Early Open", "An area was disarmed before the opening window", "User"), 495 | 'OL': ("Late to Open from Alarm", "An area in alarm was disarmed after the opening window", "User"), 496 | 'OP': ("Opening Report", "Account was disarmed", "User"), 497 | 'OQ': ("Remote Opening", "The system was disarmed from a remote location", "User"), 498 | 'OR': ("Disarm From Alarm", "Account in alarm was reset/disarmed", "User"), 499 | 'OS': ("Opening Keyswitch", "Account has been disarmed by keyswitch", "Zone"), 500 | 'OT': ("Late To Close", "System was not armed on time", "User"), 501 | 'OU': ("Output State - Trouble", "An output on a peripheral device or NAC is not functioning", "Output"), 502 | 'OV': ("Output State - Restore", "An output on a peripheral device or NAC is back to normal operation", "Output"), 503 | 'OZ': ("Point Opening", "A point, rather than a full area or account, disarmed", "Zone"), 504 | 'PA': ("Panic Alarm", "Emergency assistance request, manually activated", "Zone"), 505 | 'PB': ("Panic Bypass", "Panic zone has been bypassed", "Zone"), 506 | 'PH': ("Panic Alarm Restore", "Alarm condition eliminated", "Zone"), 507 | 'PJ': ("Panic Trouble Restore", "Trouble condition eliminated", "Zone"), 508 | 'PR': ("Panic Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 509 | 'PS': ("Panic Supervisory", "Unsafe system condition exists", "Zone"), 510 | 'PT': ("Panic Trouble", "Zone disabled by fault", "Zone"), 511 | 'PU': ("Panic Unbypass", "Panic zone bypass has been removed", "Zone"), 512 | 'QA': ("Emergency Alarm", "Emergency assistance request", "Zone"), 513 | 'QB': ("Emergency Bypass", "Zone has been bypassed", "Zone"), 514 | 'QH': ("Emergency Alarm Restore", "Alarm condition has been eliminated", "Zone"), 515 | 'QJ': ("Emergency Trouble Restore", "Trouble condition has been eliminated", "Zone"), 516 | 'QR': ("Emergency Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 517 | 'QS': ("Emergency Supervisory", "Unsafe system condition exists", "Zone"), 518 | 'QT': ("Emergency Trouble", "Zone disabled by fault", "Zone"), 519 | 'QU': ("Emergency Unbypass", "Bypass has been removed", "Zone"), 520 | 'RA': ("Remote Programmer Call Failed", "Transmitter failed to communicate with the remote programmer", ""), 521 | 'RB': ("Remote Program Begin", "Remote programming session initiated", ""), 522 | 'RC': ("Relay Close", "A relay has energized", "Relay"), 523 | 'RD': ("Remote Program Denied", "Access passcode incorrect", ""), 524 | 'RN': ("Remote Reset", "A TRANSMITTER was reset via a remote programmer", ""), 525 | 'RO': ("Relay Open", "A relay has de-energized", "Relay"), 526 | 'RP': ("Automatic Test", "Automatic communication test report", ""), 527 | 'RR': ("Power Up", "System lost power, is now restored", ""), 528 | 'RS': ("Remote Program Success", "Remote programming successful", ""), 529 | 'RT': ("Data Lost", "Dialer data lost, transmission error", "Line"), 530 | 'RU': ("Remote Program Fail", "Remote programming unsuccessful", ""), 531 | 'RX': ("Manual Test", "Manual communication test report", "User"), 532 | 'RY': ("Test Off Normal", "Test signal(s) indicates abnormal condition(s) exist", "Zone"), 533 | 'SA': ("Sprinkler Alarm", "Sprinkler flow condition exists", "Zone"), 534 | 'SB': ("Sprinkler Bypass", "Sprinkler zone has been bypassed", "Zone"), 535 | 'SC': ("Change of State", "An expansion/peripheral device is reporting a new condition or state change", "Condition Number"), 536 | 'SH': ("Sprinkler Alarm Restore", "Alarm condition eliminated", "Zone"), 537 | 'SJ': ("Sprinkler Trouble Restore", "Trouble condition eliminated", "Zone"), 538 | 'SR': ("Sprinkler Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 539 | 'SS': ("Sprinkler Supervisory", "Unsafe sprinkler system condition", "Zone"), 540 | 'ST': ("Sprinkler Trouble", "Zone disabled by fault", "Zone"), 541 | 'SU': ("Sprinkler Unbypass", "Sprinkler zone bypass has been removed", "Zone"), 542 | 'TA': ("Tamper Alarm", "Alarm equipment enclosure opened", "Zone"), 543 | 'TB': ("Tamper Bypass", "Tamper detection has been bypassed", "Zone"), 544 | 'TC': ("All Points Tested", "All point tested", ""), 545 | 'TE': ("Test End", "Communicator restored to operation", ""), 546 | 'TH': ("Tamper Alarm Restore", "An Expansion Device's tamper switch restores to normal from an Alarm state", ""), 547 | 'TJ': ("Tamper Trouble Restore", "An Expansion Device's tamper switch restores to normal from a Trouble state", ""), 548 | 'TP': ("Walk Test Point", "This point was tested during a Walk Test", "Zone"), 549 | 'TR': ("Tamper Restoral", "Alarm equipment enclosure has been closed", "Zone"), 550 | 'TS': ("Test Start", "Communicator taken out of operation", ""), 551 | 'TT': ("Tamper Trouble", "Equipment enclosure opened in disarmed state", "Zone"), 552 | 'TU': ("Tamper Unbypass", "Tamper detection bypass has been removed", "Zone"), 553 | 'TW': ("Area Watch Start", "Area watch feature has been activated", ""), 554 | 'TX': ("Test Report", "An unspecified (manual or automatic) communicator test", ""), 555 | 'TZ': ("Area Watch End", "Area watch feature has been deactivated", ""), 556 | 'UA': ("Untyped Zone Alarm", "Alarm condition from zone of unknown type", "Zone"), 557 | 'UB': ("Untyped Zone Bypass", "Zone of unknown type has been bypassed", "Zone"), 558 | 'UG': ("Unverified Event - Untyped", "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal", "Zone"), 559 | 'UH': ("Untyped Alarm Restore", "Alarm condition eliminated", "Zone"), 560 | 'UJ': ("Untyped Trouble Restore", "Trouble condition eliminated", "Zone"), 561 | 'UR': ("Untyped Zone Restoral", "Alarm/trouble condition eliminated from zone of unknown type", "Zone"), 562 | 'US': ("Untyped Zone Supervisory", "Unsafe condition from zone of unknown type", "Zone"), 563 | 'UT': ("Untyped Zone Trouble", "Trouble condition from zone of unknown type", "Zone"), 564 | 'UU': ("Untyped Zone Unbypass", "Bypass on zone of unknown type has been removed", "Zone"), 565 | 'UX': ("Undefined", "An undefined alarm condition has occurred", ""), 566 | 'UY': ("Untyped Missing Trouble", "A point or device which was not armed is now logically missing", "Zone"), 567 | 'UZ': ("Untyped Missing Alarm", "A point or device which was armed is now logically missing", "Zone"), 568 | 'VI': ("Printer Paper In", "TRANSMITTER or RECEIVER paper in", "Printer"), 569 | 'VO': ("Printer Paper Out", "TRANSMITTER or RECEIVER paper out", "Printer"), 570 | 'VR': ("Printer Restore", "TRANSMITTER or RECEIVER trouble restored", "Printer"), 571 | 'VT': ("Printer Trouble", "TRANSMITTER or RECEIVER trouble", "Printer"), 572 | 'VX': ("Printer Test", "TRANSMITTER or RECEIVER test", "Printer"), 573 | 'VY': ("Printer Online", "RECEIVER'S printer is now online", ""), 574 | 'VZ': ("Printer Offline", "RECEIVER'S printer is now offline", ""), 575 | 'WA': ("Water Alarm", "Water detected at protected premises", "Zone"), 576 | 'WB': ("Water Bypass", "Water detection has been bypassed", "Zone"), 577 | 'WH': ("Water Alarm Restore", "Water alarm condition eliminated", "Zone"), 578 | 'WJ': ("Water Trouble Restore", "Water trouble condition eliminated", "Zone"), 579 | 'WR': ("Water Restoral", "Water alarm/trouble condition has been eliminated", "Zone"), 580 | 'WS': ("Water Supervisory", "Water unsafe water detection system condition", "Zone"), 581 | 'WT': ("Water Trouble", "Water zone disabled by fault", "Zone"), 582 | 'WU': ("Water Unbypass", "Water detection bypass has been removed", "Zone"), 583 | 'XA': ("Extra Account Report", "CS RECEIVER has received an event from a non-existent account", ""), 584 | 'XE': ("Extra Point", "Panel has sensed an extra point not specified for this site", "Zone"), 585 | 'XF': ("Extra RF Point", "Panel has sensed an extra RF point not specified for this site", "Zone"), 586 | 'XH': ("RF Interference Restoral", "A radio device is no longer detecting RF Interference", "Receiver"), 587 | 'XI': ("Sensor Reset", "A user has reset a sensor", "Zone"), 588 | 'XJ': ("RF Receiver Tamper Restoral", "A Tamper condition at a premises RF Receiver has been restored", "Receiver"), 589 | 'XL': ("Low Received Signal Strength", "The RF signal strength of a reported event is below minimum level", "Receiver"), 590 | 'XM': ("Missing Alarm - Cross Point", "Missing Alarm verified by Cross Point in Alarm (or missing)", "Zone"), 591 | 'XQ': ("RF Interference", "A radio device is detecting RF Interference", "Receiver"), 592 | 'XR': ("Transmitter Battery Restoral", "Low battery has been corrected", "Zone"), 593 | 'XS': ("RF Receiver Tamper", "A Tamper condition at a premises receiver is detected", "Receiver"), 594 | 'XT': ("Transmitter Battery Trouble", "Low battery in wireless transmitter", "Zone"), 595 | 'XW': ("Forced Point", "A point was forced out of the system at arm time", "Zone"), 596 | 'XX': ("Fail to Test", "A specific test from a panel was not received", ""), 597 | 'YA': ("Bell Fault", "A trouble condition has been detected on a Local Bell, Siren, or Annunciator", ""), 598 | 'YB': ("Busy Seconds", "Percent of time receiver's line card is on-line", "Line Card"), 599 | 'YC': ("Communications Fail", "RECEIVER and TRANSMITTER", ""), 600 | 'YD': ("Receiver Line Card Trouble", "A line card identified by the passed address is in trouble", "Line Card"), 601 | 'YE': ("Receiver Line Card Restored", "A line card identified by the passed address is restored", "Line Card"), 602 | 'YF': ("Parameter Checksum Fail", "System data corrupted", ""), 603 | 'YG': ("Parameter Changed", "A TRANSMITTER'S parameters have been changed", ""), 604 | 'YH': ("Bell Restored", "A trouble condition has been restored on a Local Bell, Siren, or Annunciator", ""), 605 | 'YI': ("Overcurrent Trouble", "An Expansion Device has detected an overcurrent condition", ""), 606 | 'YJ': ("Overcurrent Restore", "An Expansion Device has restored from an overcurrent condition", ""), 607 | 'YK': ("Communications Restoral", "TRANSMITTER has resumed communication with a RECEIVER", ""), 608 | 'YM': ("System Battery Missing", "TRANSMITTER/RECEIVER battery is missing", ""), 609 | 'YN': ("Invalid Report", "TRANSMITTER has sent a packet with invalid data", ""), 610 | 'YO': ("Unknown Message", "An unknown message was received from automation or the printer", ""), 611 | 'YP': ("Power Supply Trouble", "TRANSMITTER/RECEIVER has a problem with the power supply", ""), 612 | 'YQ': ("Power Supply Restored", "TRANSMITTER'S/RECEIVER'S power supply has been restored", ""), 613 | 'YR': ("System Battery Restoral", "Low battery has been corrected", ""), 614 | 'YS': ("Communications Trouble", "RECEIVER and TRANSMITTER", ""), 615 | 'YT': ("System Battery Trouble", "Low battery in control/communicator", ""), 616 | 'YU': ("Diagnostic Error", "An expansion/peripheral device is reporting a diagnostic error", "Condition Number"), 617 | 'YW': ("Watchdog Reset", "The TRANSMITTER created an internal reset", ""), 618 | 'YX': ("Service Required", "A TRANSMITTER/RECEIVER needs service", ""), 619 | 'YY': ("Status Report", "This is a header for an account status report transmission", ""), 620 | 'YZ': ("Service Completed", "Required TRANSMITTER / RECEIVER service completed", ""), 621 | 'ZA': ("Freeze Alarm", "Low temperature detected at premises", "Zone"), 622 | 'ZB': ("Freeze Bypass", "Low temperature detection has been bypassed", "Zone"), 623 | 'ZH': ("Freeze Alarm Restore", "Alarm condition eliminated", "Zone"), 624 | 'ZJ': ("Freeze Trouble Restore", "Trouble condition eliminated", "Zone"), 625 | 'ZR': ("Freeze Restoral", "Alarm/trouble condition has been eliminated", "Zone"), 626 | 'ZS': ("Freeze Supervisory", "Unsafe freeze detection system condition", "Zone"), 627 | 'ZT': ("Freeze Trouble", "Zone disabled by fault", "Zone"), 628 | 'ZU': ("Freeze Unbypass", "Low temperature detection bypass removed", "Zone"), 629 | } 630 | 631 | def parse_record(self, data): 632 | try: 633 | rectype = data[0] & 0xc0 634 | payloadlength = data[0] & 0x3f 635 | payloadtype = data[1:2] 636 | payload = data[2:2 + payloadlength] 637 | nextrec = data[3 + payloadlength:] 638 | except IndexError: 639 | self.error('Record parse error') 640 | return (b'', b'', b'') 641 | 642 | # Verify check byte 643 | checksum = 0xff ^ reduce(lambda x,y: x^y, data[:3 + payloadlength]) 644 | if checksum != 0: 645 | self.error('Check byte error') 646 | return (b'', b'', b'') 647 | 648 | if rectype == 0xc0: 649 | return (payloadtype, payload, nextrec) 650 | else: 651 | return (b'', b'', nextrec) 652 | 653 | def __init__(self, client_ip, msg): 654 | self.client_ip = client_ip 655 | 656 | while msg: 657 | (t, payload, msg) = self.parse_record(msg) 658 | 659 | self.debug("%s" % (t + b' ' + payload)) 660 | 661 | if t == b'#': 662 | self.account_number = payload.decode('ascii') 663 | elif t == b'A': 664 | self.extra_text = payload.decode('ascii') 665 | elif t == b'N': 666 | try: 667 | area = int(payload[2:3]) 668 | event_code = payload[3:5].decode('ascii') 669 | value = int(payload[5:8]) 670 | except: 671 | self.error("Payload parse error") 672 | continue 673 | 674 | # Populate class properties 675 | self.area = area 676 | self.value = value 677 | try: 678 | (self.event, self.description, self.value_name) = self.EVENTS[event_code] 679 | except KeyError: 680 | self.error("Unknown event code " + event_code) 681 | 682 | class TexecomService(socketserver.BaseRequestHandler): 683 | error = lambda self, msg: logger.error(msg, extra={'tag': self.client_address[0]}) 684 | info = lambda self, msg: logger.info(msg, extra={'tag': self.client_address[0]}) 685 | debug = lambda self, msg: logger.debug(msg, extra={'tag': self.client_address[0]}) 686 | 687 | # POLL flags (not all of these are verified - see docs) 688 | FLAG_LINE_FAILURE = 1 689 | FLAG_AC_FAILURE = 2 690 | FLAG_BATTERY_FAILURE = 4 691 | FLAG_ARMED = 8 692 | FLAG_ENGINEER = 16 693 | 694 | def handle_poll(self, data): 695 | self.debug("%s" % (data)) 696 | 697 | try: 698 | parts = (data[4:].decode('ascii')).strip().split('#') 699 | account = parts[0] 700 | flags = ord(parts[1][0]) 701 | except: 702 | self.error("POLL parse error") 703 | return 704 | 705 | self.info("Poll for a/c %s flags 0x%02x" % (account, flags)) 706 | 707 | # Check we recognise the account number and that 708 | # the client IP matches 709 | if self.server.account_manager.get_ip(account) != self.client_address[0]: 710 | self.error("Invalid IP address for account %s" % (account)) 711 | return 712 | 713 | # Send ack/polling delay in minutes 714 | interval = self.server.account_manager.get_polling_interval(account) 715 | self.request.send(b'[P]\x00' + bytes([interval]) + b'\x06\r\n') 716 | 717 | # Record poll event 718 | self.server.account_manager.polled(account) 719 | 720 | # Post retained status update to broker 721 | info = { 722 | 'client_ip': self.client_address[0], 723 | 'client_port': self.client_address[1], 724 | 'account': int(account), 725 | 'timestamp': datetime.now().isoformat(), 726 | 'interval': interval * 60, 727 | 'flags_raw': flags, 728 | 'line_failure': (flags & self.FLAG_LINE_FAILURE) > 0, 729 | 'ac_failure': (flags & self.FLAG_AC_FAILURE) > 0, 730 | 'battery_failure': (flags & self.FLAG_BATTERY_FAILURE) > 0, 731 | 'armed': (flags & self.FLAG_ARMED) > 0, 732 | 'engineer': (flags & self.FLAG_ENGINEER) > 0, 733 | } 734 | self.server.mqtt_client.publish("alarms/%s/status" % (account), json.dumps(info), qos=1, retain=True) 735 | 736 | def handle_message(self, data, parser): 737 | # Parse message 738 | message = parser(self.client_address[0], data[1:]) 739 | 740 | self.info("%s: a/c %s area %d %s %s %d %s" % (type(message).__name__, message.account_number, message.area, message.event, message.value_name, message.value, message.extra_text)) 741 | if message.description: 742 | self.debug(message.description) 743 | 744 | # Check we recognise the account number and that 745 | # the client IP matches 746 | if self.server.account_manager.get_ip(message.account_number) != self.client_address[0]: 747 | self.error("Invalid IP address for account %s" % (message.account_number)) 748 | return 749 | 750 | # Send ACK 751 | self.request.send(data[0:1] + b'\x06\r\n') 752 | 753 | # Post event message to broker (not retained) 754 | info = { 755 | 'client_ip': self.client_address[0], 756 | 'client_port': self.client_address[1], 757 | 'account': int(message.account_number), 758 | 'timestamp': datetime.now().isoformat(), 759 | 'area': message.area, 760 | 'event': message.event, 761 | 'value': message.value, 762 | 'value_type': message.value_name, 763 | 'extra_text': message.extra_text, 764 | } 765 | self.server.mqtt_client.publish("alarms/%s/event" % (message.account_number), json.dumps(info), qos=1, retain=False) 766 | msg = "Area %d %s %s %d" % (message.area, message.event, message.value_name, message.value) 767 | if message.extra_text: 768 | msg = msg + " (" + message.extra_text + ")" 769 | self.server.mqtt_client.publish("alarms/%s/message" % (message.account_number), msg, qos=1, retain=False) 770 | 771 | def handle(self): 772 | self.debug("Client connected from %s:%s" % (self.client_address[0], self.client_address[1])) 773 | 774 | while True: 775 | data = self.request.recv(1024) 776 | if not data: 777 | break 778 | 779 | # Dump raw packet 780 | self.debug('RAW: %s' % (hexlify(data))) 781 | 782 | if data[0:3] == b'+++': 783 | # End of transmission - we'll got a TCP disconnection after this 784 | # so just ignore this silently 785 | continue 786 | 787 | # All other messages should have terminator which we 788 | # can remove 789 | if data[-2:] != b'\r\n': 790 | self.error("Ignoring line with missing terminator") 791 | continue 792 | data = data[:-2] 793 | 794 | # Determine packet type and pass to handler 795 | if data[0:4] == b'POLL': 796 | # Polling packet 797 | self.handle_poll(data) 798 | elif data[0:1] == b'2': 799 | self.handle_message(data, ContactId) 800 | elif data[0:1] == b'3': 801 | self.handle_message(data, SIA) 802 | else: 803 | self.error("Unhandled message: %s" % (hexlify(data))) 804 | 805 | self.debug("Client disconnected") 806 | self.request.close() 807 | 808 | class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): 809 | pass 810 | 811 | def on_mqtt_connect(client, userdata, rc): 812 | logger.info("MQTT connection established: %d" % (rc), extra={'tag': ''}) 813 | 814 | def on_mqtt_disconnect(client, userdata, rc): 815 | logger.info("MQTT connection lost: %d" % (rc), extra={'tag': ''}) 816 | 817 | def on_mqtt_publish(client, userdata, mid): 818 | logger.debug("MQTT publish complete", extra={'tag': ''}) 819 | 820 | def main(): 821 | # Start account manager 822 | manager = AccountManager() 823 | 824 | # Start threaded MQTT client 825 | MQTT_HOST = get_config('mqtt', 'host') 826 | MQTT_PORT = int(get_config('mqtt', 'port')) 827 | MQTT_USERNAME = get_config('mqtt', 'username') 828 | MQTT_PASSWORD = get_config('mqtt', 'password') 829 | MQTT_CAFILE = get_config('mqtt', 'cafile') 830 | logger.info("Starting MQTT client for %s:%u" % (MQTT_HOST, MQTT_PORT), extra={'tag': ''}) 831 | 832 | client = mqtt.Client() 833 | client.on_connect = on_mqtt_connect 834 | client.on_disconnect = on_mqtt_disconnect 835 | client.on_publish = on_mqtt_publish 836 | if MQTT_CAFILE: 837 | client.tls_set(MQTT_CAFILE) 838 | if MQTT_USERNAME and MQTT_PASSWORD: 839 | client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) 840 | client.connect_async(MQTT_HOST, MQTT_PORT, 60) 841 | client.loop_start() 842 | 843 | # Start ARC server 844 | SERVER_HOST = get_config('server', 'host') 845 | SERVER_PORT = int(get_config('server', 'port')) 846 | logger.info("Starting alarm server on %s:%u" % (SERVER_HOST, SERVER_PORT), extra={'tag': ''}) 847 | 848 | # Start monitoring missed polls 849 | manager.start_polling_watchdog() 850 | manager.mqtt_client = client 851 | 852 | t = ThreadedTCPServer((SERVER_HOST, SERVER_PORT), TexecomService) 853 | t.mqtt_client = client 854 | t.account_manager = manager 855 | t.serve_forever() 856 | 857 | if __name__ == '__main__': 858 | if len(sys.argv)>1 and sys.argv[1] == '-d': 859 | # Daemonize while retaining logger file handles 860 | daemon = daemon.DaemonContext( 861 | pidfile = lockfile.FileLock(PID_FILE), 862 | files_preserve = [ fh.stream ], 863 | ) 864 | logger.info("Daemonizing...", extra={'tag': ''}) 865 | daemon.open() 866 | 867 | # Log exceptions 868 | try: 869 | main() 870 | except Exception: 871 | logger.exception("Terminated due to exception", extra={'tag': ''}) 872 | 873 | --------------------------------------------------------------------------------