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.

607 lines
16 KiB

  1. """
  2. Provides the full AD2USB class and factory.
  3. """
  4. import time
  5. import threading
  6. import re
  7. from .event import event
  8. from . import devices
  9. from . import util
  10. class Overseer(object):
  11. """
  12. Factory for creation of AD2USB devices as well as provide4s attach/detach events."
  13. """
  14. # Factory events
  15. on_attached = event.Event('Called when an AD2USB device has been detected.')
  16. on_detached = event.Event('Called when an AD2USB device has been removed.')
  17. __devices = []
  18. @classmethod
  19. def find_all(cls):
  20. """
  21. Returns all AD2USB devices located on the system.
  22. """
  23. cls.__devices = devices.USBDevice.find_all()
  24. return cls.__devices
  25. @classmethod
  26. def devices(cls):
  27. """
  28. Returns a cached list of AD2USB devices located on the system.
  29. """
  30. return cls.__devices
  31. @classmethod
  32. def create(cls, device=None):
  33. """
  34. Factory method that returns the requested AD2USB device, or the first device.
  35. """
  36. cls.find_all()
  37. if len(cls.__devices) == 0:
  38. raise util.NoDeviceError('No AD2USB devices present.')
  39. if device is None:
  40. device = cls.__devices[0]
  41. vendor, product, sernum, ifcount, description = device
  42. device = devices.USBDevice(serial=sernum, description=description)
  43. return AD2USB(device)
  44. def __init__(self, attached_event=None, detached_event=None):
  45. """
  46. Constructor
  47. """
  48. self._detect_thread = Overseer.DetectThread(self)
  49. if attached_event:
  50. self.on_attached += attached_event
  51. if detached_event:
  52. self.on_detached += detached_event
  53. Overseer.find_all()
  54. self.start()
  55. def __del__(self):
  56. """
  57. Destructor
  58. """
  59. pass
  60. def close(self):
  61. """
  62. Clean up and shut down.
  63. """
  64. self.stop()
  65. def start(self):
  66. """
  67. Starts the detection thread, if not already running.
  68. """
  69. if not self._detect_thread.is_alive():
  70. self._detect_thread.start()
  71. def stop(self):
  72. """
  73. Stops the detection thread.
  74. """
  75. self._detect_thread.stop()
  76. def get_device(self, device=None):
  77. """
  78. Factory method that returns the requested AD2USB device, or the first device.
  79. """
  80. return Overseer.create(device)
  81. class DetectThread(threading.Thread):
  82. """
  83. Thread that handles detection of added/removed devices.
  84. """
  85. def __init__(self, overseer):
  86. """
  87. Constructor
  88. """
  89. threading.Thread.__init__(self)
  90. self._overseer = overseer
  91. self._running = False
  92. def stop(self):
  93. """
  94. Stops the thread.
  95. """
  96. self._running = False
  97. def run(self):
  98. """
  99. The actual detection process.
  100. """
  101. self._running = True
  102. last_devices = set()
  103. while self._running:
  104. try:
  105. Overseer.find_all()
  106. current_devices = set(Overseer.devices())
  107. new_devices = [d for d in current_devices if d not in last_devices]
  108. removed_devices = [d for d in last_devices if d not in current_devices]
  109. last_devices = current_devices
  110. for d in new_devices:
  111. self._overseer.on_attached(d)
  112. for d in removed_devices:
  113. self._overseer.on_detached(d)
  114. except util.CommError, err:
  115. pass
  116. time.sleep(0.25)
  117. class AD2USB(object):
  118. """
  119. High-level wrapper around AD2USB/AD2SERIAL devices.
  120. """
  121. # High-level Events
  122. on_open = event.Event('Called when the device has been opened.')
  123. on_close = event.Event('Called when the device has been closed.')
  124. on_status_changed = event.Event('Called when the panel status changes.')
  125. on_power_changed = event.Event('Called when panel power switches between AC and DC.')
  126. on_alarm = event.Event('Called when the alarm is triggered.')
  127. on_bypass = event.Event('Called when a zone is bypassed.')
  128. # Mid-level Events
  129. on_message = event.Event('Called when a message has been received from the device.')
  130. # Low-level Events
  131. on_read = event.Event('Called when a line has been read from the device.')
  132. on_write = event.Event('Called when data has been written to the device.')
  133. def __init__(self, device):
  134. """
  135. Constructor
  136. """
  137. self._power_status = None
  138. self._alarm_status = None
  139. self._bypass_status = None
  140. self._device = device
  141. def __del__(self):
  142. """
  143. Destructor
  144. """
  145. pass
  146. def open(self, baudrate=None, interface=None, index=None):
  147. """
  148. Opens the device.
  149. """
  150. self._wire_events()
  151. self._device.open(baudrate=baudrate, interface=interface, index=index)
  152. def close(self):
  153. """
  154. Closes the device.
  155. """
  156. self._device.close()
  157. self._device = None
  158. def _wire_events(self):
  159. """
  160. Wires up the internal device events.
  161. """
  162. self._device.on_open += self._on_open
  163. self._device.on_close += self._on_close
  164. self._device.on_read += self._on_read
  165. self._device.on_write += self._on_write
  166. def _handle_message(self, data):
  167. """
  168. Parses messages from the panel.
  169. """
  170. if data[0] == '!': # TEMP: Remove this.
  171. return None
  172. msg = Message(data)
  173. # parse and build stuff
  174. # TEMP
  175. address_mask = 0xFF80
  176. if address_mask & msg.mask > 0:
  177. #print 'ac={0}, alarm={1}, bypass={2}'.format(msg.ac, msg.alarm_bell, msg.bypass)
  178. if msg.ac != self._power_status:
  179. self._power_status, old_status = msg.ac, self._power_status
  180. #print '\tpower: new={0}, old={1}'.format(self._power_status, old_status)
  181. if old_status is not None:
  182. self.on_power_changed(self._power_status)
  183. if msg.alarm_bell != self._alarm_status:
  184. self._alarm_status, old_status = msg.alarm_bell, self._alarm_status
  185. #print '\talarm: new={0}, old={1}'.format(self._alarm_status, old_status)
  186. if old_status is not None:
  187. self.on_alarm(self._alarm_status)
  188. if msg.bypass != self._bypass_status:
  189. self._bypass_status, old_status = msg.bypass, self._bypass_status
  190. #print '\tbypass: new={0}, old={1}'.format(self._bypass_status, old_status)
  191. if old_status is not None:
  192. self.on_bypass(self._bypass_status)
  193. def _on_open(self, sender, args):
  194. """
  195. Internal handler for opening the device.
  196. """
  197. self.on_open(args)
  198. def _on_close(self, sender, args):
  199. """
  200. Internal handler for closing the device.
  201. """
  202. self.on_close(args)
  203. def _on_read(self, sender, args):
  204. """
  205. Internal handler for reading from the device.
  206. """
  207. msg = self._handle_message(args)
  208. if msg:
  209. self.on_message(msg)
  210. self.on_read(args)
  211. def _on_write(self, sender, args):
  212. """
  213. Internal handler for writing to the device.
  214. """
  215. self.on_write(args)
  216. class Message(object):
  217. """
  218. Represents a message from the alarm panel.
  219. """
  220. def __init__(self, data=None):
  221. """
  222. Constructor
  223. """
  224. self._ignore_packet = False
  225. self._ready = False
  226. self._armed_away = False
  227. self._armed_home = False
  228. self._backlight = False
  229. self._programming_mode = False
  230. self._beeps = -1
  231. self._bypass = False
  232. self._ac = False
  233. self._chime_mode = False
  234. self._alarm_event_occurred = False
  235. self._alarm_bell = False
  236. self._numeric = ""
  237. self._text = ""
  238. self._cursor = -1
  239. self._raw = ""
  240. self._mask = ""
  241. self._msg_bitfields = ""
  242. self._msg_zone = ""
  243. self._msg_binary = ""
  244. self._msg_alpha = ""
  245. self._regex = re.compile('("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*)')
  246. if data is not None:
  247. self._parse_message(data)
  248. def _parse_message(self, data):
  249. m = self._regex.match(data)
  250. if m is None:
  251. raise util.InvalidMessageError('Received invalid message: {0}'.format(data))
  252. self._msg_bitfields, self._msg_zone, self._msg_binary, self._msg_alpha = m.group(1, 2, 3, 4)
  253. self.mask = int(self._msg_binary[3:3+8], 16)
  254. self.raw = data
  255. self.ready = not self._msg_bitfields[1:2] == "0"
  256. self.armed_away = not self._msg_bitfields[2:3] == "0"
  257. self.armed_home = not self._msg_bitfields[3:4] == "0"
  258. self.backlight = not self._msg_bitfields[4:5] == "0"
  259. self.programming_mode = not self._msg_bitfields[5:6] == "0"
  260. self.beeps = int(self._msg_bitfields[6:7], 16)
  261. self.bypass = not self._msg_bitfields[7:8] == "0"
  262. self.ac = not self._msg_bitfields[8:9] == "0"
  263. self.chime_mode = not self._msg_bitfields[9:10] == "0"
  264. self.alarm_event_occurred = not self._msg_bitfields[10:11] == "0"
  265. self.alarm_bell = not self._msg_bitfields[11:12] == "0"
  266. self.numeric = self._msg_zone
  267. self.text = self._msg_alpha.strip('"')
  268. if int(self._msg_binary[19:21], 16) & 0x01 > 0:
  269. self.cursor = int(self._msg_bitfields[21:23], 16)
  270. #print "Message:\r\n" \
  271. # "\tmask: {0}\r\n" \
  272. # "\tready: {1}\r\n" \
  273. # "\tarmed_away: {2}\r\n" \
  274. # "\tarmed_home: {3}\r\n" \
  275. # "\tbacklight: {4}\r\n" \
  276. # "\tprogramming_mode: {5}\r\n" \
  277. # "\tbeeps: {6}\r\n" \
  278. # "\tbypass: {7}\r\n" \
  279. # "\tac: {8}\r\n" \
  280. # "\tchime_mode: {9}\r\n" \
  281. # "\talarm_event_occurred: {10}\r\n" \
  282. # "\talarm_bell: {11}\r\n" \
  283. # "\tcursor: {12}\r\n" \
  284. # "\tnumeric: {13}\r\n" \
  285. # "\ttext: {14}\r\n".format(
  286. # self.mask,
  287. # self.ready,
  288. # self.armed_away,
  289. # self.armed_home,
  290. # self.backlight,
  291. # self.programming_mode,
  292. # self.beeps,
  293. # self.bypass,
  294. # self.ac,
  295. # self.chime_mode,
  296. # self.alarm_event_occurred,
  297. # self.alarm_bell,
  298. # self.cursor,
  299. # self.numeric,
  300. # self.text
  301. # )
  302. @property
  303. def ignore_packet(self):
  304. """
  305. Indicates whether or not this message should be ignored.
  306. """
  307. return self._ignore_packet
  308. @ignore_packet.setter
  309. def ignore_packet(self, value):
  310. """
  311. Sets the value indicating whether or not this packet should be ignored.
  312. """
  313. self._ignore_packet = value
  314. @property
  315. def ready(self):
  316. """
  317. Indicates whether or not the panel is ready.
  318. """
  319. return self._ready
  320. @ready.setter
  321. def ready(self, value):
  322. """
  323. Sets the value indicating whether or not the panel is ready.
  324. """
  325. self._ready = value
  326. @property
  327. def armed_away(self):
  328. """
  329. Indicates whether or not the panel is armed in away mode.
  330. """
  331. return self._armed_away
  332. @armed_away.setter
  333. def armed_away(self, value):
  334. """
  335. Sets the value indicating whether or not the panel is armed in away mode.
  336. """
  337. self._armed_away = value
  338. @property
  339. def armed_home(self):
  340. """
  341. Indicates whether or not the panel is armed in home/stay mode.
  342. """
  343. return self._armed_home
  344. @armed_home.setter
  345. def armed_home(self, value):
  346. """
  347. Sets the value indicating whether or not the panel is armed in home/stay mode.
  348. """
  349. self._armed_home = value
  350. @property
  351. def backlight(self):
  352. """
  353. Indicates whether or not the panel backlight is on.
  354. """
  355. return self._backlight
  356. @backlight.setter
  357. def backlight(self, value):
  358. """
  359. Sets the value indicating whether or not the panel backlight is on.
  360. """
  361. self._backlight = value
  362. @property
  363. def programming_mode(self):
  364. """
  365. Indicates whether or not the panel is in programming mode.
  366. """
  367. return self._programming_mode
  368. @programming_mode.setter
  369. def programming_mode(self, value):
  370. """
  371. Sets the value indicating whether or not the panel is in programming mode.
  372. """
  373. self._programming_mode = value
  374. @property
  375. def beeps(self):
  376. """
  377. Returns the number of beeps associated with this message.
  378. """
  379. return self._beeps
  380. @beeps.setter
  381. def beeps(self, value):
  382. """
  383. Sets the number of beeps associated with this message.
  384. """
  385. self._beeps = value
  386. @property
  387. def bypass(self):
  388. """
  389. Indicates whether or not zones have been bypassed.
  390. """
  391. return self._bypass
  392. @bypass.setter
  393. def bypass(self, value):
  394. """
  395. Sets the value indicating whether or not zones have been bypassed.
  396. """
  397. self._bypass = value
  398. @property
  399. def ac(self):
  400. """
  401. Indicates whether or not the system is on AC power.
  402. """
  403. return self._ac
  404. @ac.setter
  405. def ac(self, value):
  406. """
  407. Sets the value indicating whether or not the system is on AC power.
  408. """
  409. self._ac = value
  410. @property
  411. def chime_mode(self):
  412. """
  413. Indicates whether or not panel chimes are enabled.
  414. """
  415. return self._chime_mode
  416. @chime_mode.setter
  417. def chime_mode(self, value):
  418. """
  419. Sets the value indicating whether or not the panel chimes are enabled.
  420. """
  421. self._chime_mode = value
  422. @property
  423. def alarm_event_occurred(self):
  424. """
  425. Indicates whether or not an alarm event has occurred.
  426. """
  427. return self._alarm_event_occurred
  428. @alarm_event_occurred.setter
  429. def alarm_event_occurred(self, value):
  430. """
  431. Sets the value indicating whether or not an alarm event has occurred.
  432. """
  433. self._alarm_event_occurred = value
  434. @property
  435. def alarm_bell(self):
  436. """
  437. Indicates whether or not an alarm is currently sounding.
  438. """
  439. return self._alarm_bell
  440. @alarm_bell.setter
  441. def alarm_bell(self, value):
  442. """
  443. Sets the value indicating whether or not an alarm is currently sounding.
  444. """
  445. self._alarm_bell = value
  446. @property
  447. def numeric(self):
  448. """
  449. Numeric indicator of associated with message. For example: If zone #3 is faulted, this value is 003.
  450. """
  451. return self._numeric
  452. @numeric.setter
  453. def numeric(self, value):
  454. """
  455. Sets the numeric indicator associated with this message.
  456. """
  457. self._numeric = value
  458. @property
  459. def text(self):
  460. """
  461. Alphanumeric text associated with this message.
  462. """
  463. return self._text
  464. @text.setter
  465. def text(self, value):
  466. """
  467. Sets the alphanumeric text associated with this message.
  468. """
  469. self._text = value
  470. @property
  471. def cursor(self):
  472. """
  473. Indicates which text position has the cursor underneath it.
  474. """
  475. return self._cursor
  476. @cursor.setter
  477. def cursor(self, value):
  478. """
  479. Sets the value indicating which text position has the cursor underneath it.
  480. """
  481. self._cursor = value
  482. @property
  483. def raw(self):
  484. """
  485. Raw representation of the message data from the panel.
  486. """
  487. return self._raw
  488. @raw.setter
  489. def raw(self, value):
  490. """
  491. Sets the raw representation of the message data from the panel.
  492. """
  493. self._raw = value
  494. @property
  495. def mask(self):
  496. """
  497. The panel mask for which this message is intended.
  498. """
  499. return self._mask
  500. @mask.setter
  501. def mask(self, value):
  502. """
  503. Sets the panel mask for which this message is intended.
  504. """
  505. self._mask = value