A clone of: https://github.com/nutechsoftware/alarmdecoder This is requires as they dropped support for older firmware releases w/o building in backward compatibility code, and they had previously hardcoded pyserial to a python2 only version.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

800 lines
24 KiB

  1. """
  2. Provides the full AD2USB class and factory.
  3. """
  4. import time
  5. import threading
  6. import re
  7. import logging
  8. from collections import OrderedDict
  9. from .event import event
  10. from . import devices
  11. from . import util
  12. class Overseer(object):
  13. """
  14. Factory for creation of AD2USB devices as well as provide4s attach/detach events."
  15. """
  16. # Factory events
  17. on_attached = event.Event('Called when an AD2USB device has been detected.')
  18. on_detached = event.Event('Called when an AD2USB device has been removed.')
  19. __devices = []
  20. @classmethod
  21. def find_all(cls):
  22. """
  23. Returns all AD2USB devices located on the system.
  24. """
  25. cls.__devices = devices.USBDevice.find_all()
  26. return cls.__devices
  27. @classmethod
  28. def devices(cls):
  29. """
  30. Returns a cached list of AD2USB devices located on the system.
  31. """
  32. return cls.__devices
  33. @classmethod
  34. def create(cls, device=None):
  35. """
  36. Factory method that returns the requested AD2USB device, or the first device.
  37. """
  38. cls.find_all()
  39. if len(cls.__devices) == 0:
  40. raise util.NoDeviceError('No AD2USB devices present.')
  41. if device is None:
  42. device = cls.__devices[0]
  43. vendor, product, sernum, ifcount, description = device
  44. device = devices.USBDevice(serial=sernum, description=description)
  45. return AD2USB(device)
  46. def __init__(self, attached_event=None, detached_event=None):
  47. """
  48. Constructor
  49. """
  50. self._detect_thread = Overseer.DetectThread(self)
  51. if attached_event:
  52. self.on_attached += attached_event
  53. if detached_event:
  54. self.on_detached += detached_event
  55. Overseer.find_all()
  56. self.start()
  57. def close(self):
  58. """
  59. Clean up and shut down.
  60. """
  61. self.stop()
  62. def start(self):
  63. """
  64. Starts the detection thread, if not already running.
  65. """
  66. if not self._detect_thread.is_alive():
  67. self._detect_thread.start()
  68. def stop(self):
  69. """
  70. Stops the detection thread.
  71. """
  72. self._detect_thread.stop()
  73. def get_device(self, device=None):
  74. """
  75. Factory method that returns the requested AD2USB device, or the first device.
  76. """
  77. return Overseer.create(device)
  78. class DetectThread(threading.Thread):
  79. """
  80. Thread that handles detection of added/removed devices.
  81. """
  82. def __init__(self, overseer):
  83. """
  84. Constructor
  85. """
  86. threading.Thread.__init__(self)
  87. self._overseer = overseer
  88. self._running = False
  89. def stop(self):
  90. """
  91. Stops the thread.
  92. """
  93. self._running = False
  94. def run(self):
  95. """
  96. The actual detection process.
  97. """
  98. self._running = True
  99. last_devices = set()
  100. while self._running:
  101. try:
  102. Overseer.find_all()
  103. current_devices = set(Overseer.devices())
  104. new_devices = [d for d in current_devices if d not in last_devices]
  105. removed_devices = [d for d in last_devices if d not in current_devices]
  106. last_devices = current_devices
  107. for d in new_devices:
  108. self._overseer.on_attached(d)
  109. for d in removed_devices:
  110. self._overseer.on_detached(d)
  111. except util.CommError, err:
  112. pass
  113. time.sleep(0.25)
  114. class AD2USB(object):
  115. """
  116. High-level wrapper around AD2USB/AD2SERIAL devices.
  117. """
  118. # High-level Events
  119. on_arm = event.Event('Called when the panel is armed.')
  120. on_disarm = event.Event('Called when the panel is disarmed.')
  121. on_power_changed = event.Event('Called when panel power switches between AC and DC.')
  122. on_alarm = event.Event('Called when the alarm is triggered.')
  123. on_fire = event.Event('Called when a fire is detected.')
  124. on_bypass = event.Event('Called when a zone is bypassed.')
  125. on_boot = event.Event('Called when the device finishes bootings.')
  126. on_config_received = event.Event('Called when the device receives its configuration.')
  127. on_fault = event.Event('Called when the device detects a zone fault.')
  128. on_restore = event.Event('Called when the device detects that a fault is restored.')
  129. # Mid-level Events
  130. on_message = event.Event('Called when a message has been received from the device.')
  131. # Low-level Events
  132. on_open = event.Event('Called when the device has been opened.')
  133. on_close = event.Event('Called when the device has been closed.')
  134. on_read = event.Event('Called when a line has been read from the device.')
  135. on_write = event.Event('Called when data has been written to the device.')
  136. # Constants
  137. F1 = unichr(1) + unichr(1) + unichr(1)
  138. F2 = unichr(2) + unichr(2) + unichr(2)
  139. F3 = unichr(3) + unichr(3) + unichr(3)
  140. F4 = unichr(4) + unichr(4) + unichr(4)
  141. ZONE_EXPIRE = 30
  142. def __init__(self, device):
  143. """
  144. Constructor
  145. """
  146. self._device = device
  147. self._power_status = None
  148. self._alarm_status = None
  149. self._bypass_status = None
  150. self._armed_status = None
  151. self._fire_status = None
  152. self._zones_faulted = []
  153. self._last_zone_fault = 0
  154. self._last_wait = False
  155. self.address = 18
  156. self.configbits = 0xFF00
  157. self.address_mask = 0x00000000
  158. self.emulate_zone = [False for x in range(5)]
  159. self.emulate_relay = [False for x in range(4)]
  160. self.emulate_lrr = False
  161. self.deduplicate = False
  162. @property
  163. def id(self):
  164. """
  165. The ID of the AD2USB device.
  166. """
  167. return self._device.id
  168. def open(self, baudrate=None, interface=None, index=None, no_reader_thread=False):
  169. """
  170. Opens the device.
  171. """
  172. self._wire_events()
  173. self._device.open(baudrate=baudrate, interface=interface, index=index, no_reader_thread=no_reader_thread)
  174. def close(self):
  175. """
  176. Closes the device.
  177. """
  178. self._device.close()
  179. del self._device
  180. self._device = None
  181. def get_config(self):
  182. """
  183. Retrieves the configuration from the device.
  184. """
  185. self._device.write("C\r")
  186. def save_config(self):
  187. """
  188. Sets configuration entries on the device.
  189. """
  190. config_string = ''
  191. # HACK: Both of these methods are ugly.. but I can't think of an elegant way of doing it.
  192. #config_string += 'ADDRESS={0}&'.format(self.address)
  193. #config_string += 'CONFIGBITS={0:x}&'.format(self.configbits)
  194. #config_string += 'MASK={0:x}&'.format(self.address_mask)
  195. #config_string += 'EXP={0}&'.format(''.join(['Y' if z else 'N' for z in self.emulate_zone]))
  196. #config_string += 'REL={0}&'.format(''.join(['Y' if r else 'N' for r in self.emulate_relay]))
  197. #config_string += 'LRR={0}&'.format('Y' if self.emulate_lrr else 'N')
  198. #config_string += 'DEDUPLICATE={0}'.format('Y' if self.deduplicate else 'N')
  199. config_entries = []
  200. config_entries.append(('ADDRESS', '{0}'.format(self.address)))
  201. config_entries.append(('CONFIGBITS', '{0:x}'.format(self.configbits)))
  202. config_entries.append(('MASK', '{0:x}'.format(self.address_mask)))
  203. config_entries.append(('EXP', ''.join(['Y' if z else 'N' for z in self.emulate_zone])))
  204. config_entries.append(('REL', ''.join(['Y' if r else 'N' for r in self.emulate_relay])))
  205. config_entries.append(('LRR', 'Y' if self.emulate_lrr else 'N'))
  206. config_entries.append(('DEDUPLICATE', 'Y' if self.deduplicate else 'N'))
  207. config_string = '&'.join(['='.join(t) for t in config_entries])
  208. self._device.write("C{0}\r".format(config_string))
  209. def reboot(self):
  210. """
  211. Reboots the device.
  212. """
  213. self._device.write('=')
  214. def fault_zone(self, zone, simulate_wire_problem=False):
  215. """
  216. Faults a zone if we are emulating a zone expander.
  217. """
  218. status = 2 if simulate_wire_problem else 1
  219. self._device.write("L{0:02}{1}\r".format(zone, status))
  220. def clear_zone(self, zone):
  221. """
  222. Clears a zone if we are emulating a zone expander.
  223. """
  224. self._device.write("L{0:02}0\r".format(zone))
  225. def _wire_events(self):
  226. """
  227. Wires up the internal device events.
  228. """
  229. self._device.on_open += self._on_open
  230. self._device.on_close += self._on_close
  231. self._device.on_read += self._on_read
  232. self._device.on_write += self._on_write
  233. def _handle_message(self, data):
  234. """
  235. Parses messages from the panel.
  236. """
  237. if data is None:
  238. return None
  239. msg = None
  240. if data[0] != '!':
  241. msg = Message(data)
  242. if self.address_mask & msg.mask > 0:
  243. self._update_internal_states(msg)
  244. else: # specialty messages
  245. header = data[0:4]
  246. if header == '!EXP' or header == '!REL':
  247. msg = ExpanderMessage(data)
  248. elif header == '!RFX':
  249. msg = RFMessage(data)
  250. elif header == '!LRR':
  251. msg = LRRMessage(data)
  252. elif data.startswith('!Ready'):
  253. self.on_boot()
  254. elif data.startswith('!CONFIG'):
  255. self._handle_config(data)
  256. return msg
  257. def _handle_config(self, data):
  258. """
  259. Handles received configuration data.
  260. """
  261. _, config_string = data.split('>')
  262. for setting in config_string.split('&'):
  263. k, v = setting.split('=')
  264. if k == 'ADDRESS':
  265. self.address = int(v)
  266. elif k == 'CONFIGBITS':
  267. self.configbits = int(v, 16)
  268. elif k == 'MASK':
  269. self.address_mask = int(v, 16)
  270. elif k == 'EXP':
  271. for z in range(5):
  272. self.emulate_zone[z] = True if v[z] == 'Y' else False
  273. elif k == 'REL':
  274. for r in range(4):
  275. self.emulate_relay[r] = True if v[r] == 'Y' else False
  276. elif k == 'LRR':
  277. self.emulate_lrr = True if v == 'Y' else False
  278. elif k == 'DEDUPLICATE':
  279. self.deduplicate = True if v == 'Y' else False
  280. self.on_config_received()
  281. def _update_internal_states(self, message):
  282. """
  283. Updates internal device states.
  284. """
  285. if message.ac_power != self._power_status:
  286. self._power_status, old_status = message.ac_power, self._power_status
  287. if old_status is not None:
  288. self.on_power_changed(self._power_status)
  289. if message.alarm_sounding != self._alarm_status:
  290. self._alarm_status, old_status = message.alarm_sounding, self._alarm_status
  291. if old_status is not None:
  292. self.on_alarm(self._alarm_status)
  293. if message.zone_bypassed != self._bypass_status:
  294. self._bypass_status, old_status = message.zone_bypassed, self._bypass_status
  295. if old_status is not None:
  296. self.on_bypass(self._bypass_status)
  297. if (message.armed_away | message.armed_home) != self._armed_status:
  298. self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status
  299. if old_status is not None:
  300. if self._armed_status:
  301. self.on_arm()
  302. else:
  303. self.on_disarm()
  304. if message.fire_alarm != self._fire_status:
  305. self._fire_status, old_status = message.fire_alarm, self._fire_status
  306. if old_status is not None:
  307. self.on_fire(self._fire_status)
  308. self._update_zone_status(message)
  309. #self._clear_expired_zones(message)
  310. def _update_zone_status(self, message):
  311. #if message.check_zone or (not message.ready and "FAULT" in message.text):
  312. if "Hit * for faults" in message.text:
  313. self._device.send('*')
  314. return
  315. if message.ready:
  316. cleared_zones = []
  317. for z in self._zones_faulted:
  318. cleared_zones.append(z)
  319. for idx, status in enumerate(cleared_zones):
  320. del self._zones_faulted[idx]
  321. self.on_restore(z)
  322. elif "FAULT" in message.text:
  323. zone = -1
  324. try:
  325. zone = int(message.numeric_code)
  326. except ValueError:
  327. zone = int(message.numeric_code, 16)
  328. if zone not in self._zones_faulted:
  329. # if self._last_zone_fault == 0:
  330. # idx = 0
  331. # else:
  332. # idx = self._zones_faulted.index(self._last_zone_fault) + 1
  333. self._last_zone_fault = zone
  334. self._last_wait = True
  335. self._zones_faulted.append(zone)
  336. self._zones_faulted.sort()
  337. self.on_fault(zone)
  338. self._clear_expired_zones(zone)
  339. self._last_zone_fault = zone
  340. def _clear_expired_zones(self, zone):
  341. cleared_zones = []
  342. found_last = False
  343. found_end = False
  344. print '_clear_expired_zones: ', repr(self._zones_faulted)
  345. # ----------
  346. #for idx in range(len(self._zones_faulted)):
  347. # idx = 0
  348. # while idx < len(self._zones_faulted):
  349. # z = self._zones_faulted[idx]
  350. # if not found_last:
  351. # if z == self._last_zone_fault:
  352. # print ' found start point', z
  353. # found_last = True
  354. # if found_last:
  355. # if z == zone and self._last_zone_fault != zone and not break_loop:
  356. # print ' found end point', z
  357. # found_end = True
  358. # break
  359. # elif z != self._last_zone_fault and len(self._zones_faulted) > 1:
  360. # print ' clearing', z
  361. # cleared_zones.append(z)
  362. # if idx == len(self._zones_faulted) - 1 and not found_end:
  363. # print ' rolling back to front of the list.'
  364. # idx = 0
  365. # break_loop = True
  366. # else:
  367. # idx += 1
  368. # ----------
  369. # idx = 0
  370. # while not found_end and idx < len(self._zones_faulted):
  371. # z = self._zones_faulted[idx]
  372. # if z == zone and found_last:
  373. # print ' found end point, exiting', z
  374. # found_end = True
  375. # break
  376. # if not found_last and z == self._last_zone_fault:
  377. # print ' found start point', z
  378. # found_last = True
  379. # if found_last:
  380. # print 'removing', z
  381. # self._zones_faulted.remove(z)
  382. # #print ' idx', idx
  383. # #print ' end', found_end
  384. # #print ' start', found_last
  385. # if idx >= len(self._zones_faulted) - 1 and not found_end and found_last:
  386. # print ' roll'
  387. # idx = 0
  388. # else:
  389. # idx += 1
  390. # -----
  391. # idx = 0
  392. # start_pos = -1
  393. # end_pos = -1
  394. # while idx < len(self._zones_faulted):
  395. # z = self._zones_faulted[idx]
  396. # if z == self._last_zone_fault or self._last_zone_fault == 0:
  397. # print 'start', idx
  398. # start_pos = idx
  399. # if z == zone:
  400. # print 'end', idx
  401. # end_pos = idx
  402. # if idx >= len(self._zones_faulted) - 1 and end_pos == -1 and start_pos != -1:
  403. # print 'roll'
  404. # idx = 0
  405. # else:
  406. # idx += 1
  407. # if start_pos < end_pos:
  408. # diff = end_pos - start_pos
  409. # if diff > 1 and not self._last_wait:
  410. # print 'deleting', start_pos + 1, end_pos
  411. # del self._zones_faulted[start_pos + 1:end_pos]
  412. # elif end_pos < start_pos:
  413. # diff = len(self._zones_faulted) - start_pos + end_pos
  414. # if diff > 1 and not self._last_wait:
  415. # print 'deleting', start_pos + 1, ' -> end'
  416. # del self._zones_faulted[start_pos + 1:]
  417. # print 'deleting', 'start -> ', end_pos
  418. # del self._zones_faulted[:end_pos]
  419. # if self._last_wait == True:
  420. # self._last_wait = False
  421. # for idx, z in enumerate(cleared_zones):
  422. # print ' !remove it', z
  423. # #del self._zones_faulted[idx]
  424. # self._zones_faulted.remove(z)
  425. # self.on_restore(z)
  426. # -----
  427. idx = 0
  428. start_pos = -1
  429. end_pos = -1
  430. while idx < len(self._zones_faulted):
  431. z = self._zones_faulted[idx]
  432. if z == self._last_zone_fault or self._last_zone_fault == 0:
  433. print 'start', idx
  434. start_pos = idx
  435. if z == zone:
  436. print 'end', idx
  437. end_pos = idx
  438. if idx >= len(self._zones_faulted) - 1 and end_pos == -1 and start_pos != -1:
  439. print 'roll'
  440. idx = 0
  441. else:
  442. idx += 1
  443. if start_pos < end_pos:
  444. diff = end_pos - start_pos
  445. if diff > 1:
  446. print 'deleting', start_pos + 1, end_pos
  447. del self._zones_faulted[start_pos + 1:end_pos]
  448. elif end_pos <= start_pos:
  449. diff = len(self._zones_faulted) - start_pos + end_pos
  450. if diff > 1:
  451. print 'deleting', start_pos + 1, ' -> end'
  452. del self._zones_faulted[start_pos + 1:]
  453. print 'deleting', 'start -> ', end_pos
  454. del self._zones_faulted[:end_pos]
  455. for idx, z in enumerate(cleared_zones):
  456. print ' !remove it', z
  457. #del self._zones_faulted[idx]
  458. self._zones_faulted.remove(z)
  459. self.on_restore(z)
  460. def _on_open(self, sender, args):
  461. """
  462. Internal handler for opening the device.
  463. """
  464. self.on_open(args)
  465. def _on_close(self, sender, args):
  466. """
  467. Internal handler for closing the device.
  468. """
  469. self.on_close(args)
  470. def _on_read(self, sender, args):
  471. """
  472. Internal handler for reading from the device.
  473. """
  474. self.on_read(args)
  475. msg = self._handle_message(args)
  476. if msg:
  477. self.on_message(msg)
  478. def _on_write(self, sender, args):
  479. """
  480. Internal handler for writing to the device.
  481. """
  482. self.on_write(args)
  483. class Message(object):
  484. """
  485. Represents a message from the alarm panel.
  486. """
  487. def __init__(self, data=None):
  488. """
  489. Constructor
  490. """
  491. self.ready = False
  492. self.armed_away = False
  493. self.armed_home = False
  494. self.backlight_on = False
  495. self.programming_mode = False
  496. self.beeps = -1
  497. self.zone_bypassed = False
  498. self.ac_power = False
  499. self.chime_on = False
  500. self.alarm_event_occurred = False
  501. self.alarm_sounding = False
  502. self.battery_low = False
  503. self.entry_delay_off = False
  504. self.fire_alarm = False
  505. self.check_zone = False
  506. self.perimeter_only = False
  507. self.numeric_code = ""
  508. self.text = ""
  509. self.cursor_location = -1
  510. self.data = ""
  511. self.mask = ""
  512. self.bitfield = ""
  513. self.panel_data = ""
  514. self._regex = re.compile('("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*)')
  515. if data is not None:
  516. self._parse_message(data)
  517. def _parse_message(self, data):
  518. """
  519. Parse the message from the device.
  520. """
  521. m = self._regex.match(data)
  522. if m is None:
  523. raise util.InvalidMessageError('Received invalid message: {0}'.format(data))
  524. self.bitfield, self.numeric_code, self.panel_data, alpha = m.group(1, 2, 3, 4)
  525. self.mask = int(self.panel_data[3:3+8], 16)
  526. self.data = data
  527. self.ready = not self.bitfield[1:2] == "0"
  528. self.armed_away = not self.bitfield[2:3] == "0"
  529. self.armed_home = not self.bitfield[3:4] == "0"
  530. self.backlight_on = not self.bitfield[4:5] == "0"
  531. self.programming_mode = not self.bitfield[5:6] == "0"
  532. self.beeps = int(self.bitfield[6:7], 16)
  533. self.zone_bypassed = not self.bitfield[7:8] == "0"
  534. self.ac_power = not self.bitfield[8:9] == "0"
  535. self.chime_on = not self.bitfield[9:10] == "0"
  536. self.alarm_event_occurred = not self.bitfield[10:11] == "0"
  537. self.alarm_sounding = not self.bitfield[11:12] == "0"
  538. self.battery_low = not self.bitfield[12:13] == "0"
  539. self.entry_delay_off = not self.bitfield[13:14] == "0"
  540. self.fire_alarm = not self.bitfield[14:15] == "0"
  541. self.check_zone = not self.bitfield[15:16] == "0"
  542. self.perimeter_only = not self.bitfield[16:17] == "0"
  543. # bits 17-20 unused.
  544. self.text = alpha.strip('"')
  545. if int(self.panel_data[19:21], 16) & 0x01 > 0:
  546. self.cursor_location = int(self.bitfield[21:23], 16) # Alpha character index that the cursor is on.
  547. def __str__(self):
  548. """
  549. String conversion operator.
  550. """
  551. return 'msg > {0:0<9} [{1}{2}{3}] -- ({4}) {5}'.format(hex(self.mask), 1 if self.ready else 0, 1 if self.armed_away else 0, 1 if self.armed_home else 0, self.numeric_code, self.text)
  552. class ExpanderMessage(object):
  553. """
  554. Represents a message from a zone or relay expansion module.
  555. """
  556. ZONE = 0
  557. RELAY = 1
  558. def __init__(self, data=None):
  559. """
  560. Constructor
  561. """
  562. self.type = None
  563. self.address = None
  564. self.channel = None
  565. self.value = None
  566. self.raw = None
  567. if data is not None:
  568. self._parse_message(data)
  569. def __str__(self):
  570. """
  571. String conversion operator.
  572. """
  573. expander_type = 'UNKWN'
  574. if self.type == ExpanderMessage.ZONE:
  575. expander_type = 'ZONE'
  576. elif self.type == ExpanderMessage.RELAY:
  577. expander_type = 'RELAY'
  578. return 'exp > [{0: <5}] {1}/{2} -- {3}'.format(expander_type, self.address, self.channel, self.value)
  579. def _parse_message(self, data):
  580. """
  581. Parse the raw message from the device.
  582. """
  583. header, values = data.split(':')
  584. address, channel, value = values.split(',')
  585. self.raw = data
  586. self.address = address
  587. self.channel = channel
  588. self.value = value
  589. if header == '!EXP':
  590. self.type = ExpanderMessage.ZONE
  591. elif header == '!REL':
  592. self.type = ExpanderMessage.RELAY
  593. class RFMessage(object):
  594. """
  595. Represents a message from an RF receiver.
  596. """
  597. def __init__(self, data=None):
  598. """
  599. Constructor
  600. """
  601. self.raw = None
  602. self.serial_number = None
  603. self.value = None
  604. if data is not None:
  605. self._parse_message(data)
  606. def __str__(self):
  607. """
  608. String conversion operator.
  609. """
  610. return 'rf > {0}: {1}'.format(self.serial_number, self.value)
  611. def _parse_message(self, data):
  612. """
  613. Parses the raw message from the device.
  614. """
  615. self.raw = data
  616. _, values = data.split(':')
  617. self.serial_number, self.value = values.split(',')
  618. class LRRMessage(object):
  619. """
  620. Represent a message from a Long Range Radio.
  621. """
  622. def __init__(self, data=None):
  623. """
  624. Constructor
  625. """
  626. self.raw = None
  627. self._event_data = None
  628. self._partition = None
  629. self._event_type = None
  630. if data is not None:
  631. self._parse_message(data)
  632. def __str__(self):
  633. """
  634. String conversion operator.
  635. """
  636. return 'lrr > {0} @ {1} -- {2}'.format()
  637. def _parse_message(self, data):
  638. """
  639. Parses the raw message from the device.
  640. """
  641. self.raw = data
  642. _, values = data.split(':')
  643. self._event_data, self._partition, self._event_type = values.split(',')