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.

794 lines
20 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. self._address_mask = 0xFF80 # TEMP
  142. def __del__(self):
  143. """
  144. Destructor
  145. """
  146. pass
  147. def open(self, baudrate=None, interface=None, index=None, no_reader_thread=False):
  148. """
  149. Opens the device.
  150. """
  151. self._wire_events()
  152. self._device.open(baudrate=baudrate, interface=interface, index=index, no_reader_thread=no_reader_thread)
  153. def close(self):
  154. """
  155. Closes the device.
  156. """
  157. self._device.close()
  158. self._device = None
  159. @property
  160. def id(self):
  161. return self._device.id
  162. def _wire_events(self):
  163. """
  164. Wires up the internal device events.
  165. """
  166. self._device.on_open += self._on_open
  167. self._device.on_close += self._on_close
  168. self._device.on_read += self._on_read
  169. self._device.on_write += self._on_write
  170. def _handle_message(self, data):
  171. """
  172. Parses messages from the panel.
  173. """
  174. msg = None
  175. if data[0] != '!':
  176. msg = Message(data)
  177. if self._address_mask & msg.mask > 0:
  178. self._update_internal_states(msg)
  179. else: # specialty messages
  180. header = data[0:4]
  181. if header == '!EXP' or header == '!REL':
  182. msg = ExpanderMessage(data)
  183. elif header == '!RFX':
  184. msg = RFMessage(data)
  185. return msg
  186. def _update_internal_states(self, message):
  187. if message.ac_power != self._power_status:
  188. self._power_status, old_status = message.ac_power, self._power_status
  189. if old_status is not None:
  190. self.on_power_changed(self._power_status)
  191. if message.alarm_sounding != self._alarm_status:
  192. self._alarm_status, old_status = message.alarm_sounding, self._alarm_status
  193. if old_status is not None:
  194. self.on_alarm(self._alarm_status)
  195. if message.zone_bypassed != self._bypass_status:
  196. self._bypass_status, old_status = message.zone_bypassed, self._bypass_status
  197. if old_status is not None:
  198. self.on_bypass(self._bypass_status)
  199. def _on_open(self, sender, args):
  200. """
  201. Internal handler for opening the device.
  202. """
  203. self.on_open(args)
  204. def _on_close(self, sender, args):
  205. """
  206. Internal handler for closing the device.
  207. """
  208. self.on_close(args)
  209. def _on_read(self, sender, args):
  210. """
  211. Internal handler for reading from the device.
  212. """
  213. self.on_read(args)
  214. msg = self._handle_message(args)
  215. if msg:
  216. self.on_message(msg)
  217. def _on_write(self, sender, args):
  218. """
  219. Internal handler for writing to the device.
  220. """
  221. self.on_write(args)
  222. class Message(object):
  223. """
  224. Represents a message from the alarm panel.
  225. """
  226. def __init__(self, data=None):
  227. """
  228. Constructor
  229. """
  230. self._ready = False
  231. self._armed_away = False
  232. self._armed_home = False
  233. self._backlight_on = False
  234. self._programming_mode = False
  235. self._beeps = -1
  236. self._zone_bypassed = False
  237. self._ac_power = False
  238. self._chime_on = False
  239. self._alarm_event_occurred = False
  240. self._alarm_sounding = False
  241. self._numeric_code = ""
  242. self._text = ""
  243. self._cursor_location = -1
  244. self._data = ""
  245. self._mask = ""
  246. self._bitfield = ""
  247. self._panel_data = ""
  248. self._regex = re.compile('("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*)')
  249. if data is not None:
  250. self._parse_message(data)
  251. def _parse_message(self, data):
  252. """
  253. Parse the message from the device.
  254. """
  255. m = self._regex.match(data)
  256. if m is None:
  257. raise util.InvalidMessageError('Received invalid message: {0}'.format(data))
  258. self._bitfield, self._numeric_code, self._panel_data, alpha = m.group(1, 2, 3, 4)
  259. self._mask = int(self._panel_data[3:3+8], 16)
  260. self._data = data
  261. self._ready = not self._bitfield[1:2] == "0"
  262. self._armed_away = not self._bitfield[2:3] == "0"
  263. self._armed_home = not self._bitfield[3:4] == "0"
  264. self._backlight_on = not self._bitfield[4:5] == "0"
  265. self._programming_mode = not self._bitfield[5:6] == "0"
  266. self._beeps = int(self._bitfield[6:7], 16)
  267. self._zone_bypassed = not self._bitfield[7:8] == "0"
  268. self._ac_power = not self._bitfield[8:9] == "0"
  269. self._chime_on = not self._bitfield[9:10] == "0"
  270. self._alarm_event_occurred = not self._bitfield[10:11] == "0"
  271. self._alarm_sounding = not self._bitfield[11:12] == "0"
  272. self._text = alpha.strip('"')
  273. if int(self._panel_data[19:21], 16) & 0x01 > 0:
  274. self._cursor_location = int(self._bitfield[21:23], 16) # Alpha character index that the cursor is on.
  275. def __str__(self):
  276. """
  277. String conversion operator.
  278. """
  279. 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)
  280. @property
  281. def ready(self):
  282. """
  283. Indicates whether or not the panel is ready.
  284. """
  285. return self._ready
  286. @ready.setter
  287. def ready(self, value):
  288. """
  289. Sets the value indicating whether or not the panel is ready.
  290. """
  291. self._ready = value
  292. @property
  293. def armed_away(self):
  294. """
  295. Indicates whether or not the panel is armed in away mode.
  296. """
  297. return self._armed_away
  298. @armed_away.setter
  299. def armed_away(self, value):
  300. """
  301. Sets the value indicating whether or not the panel is armed in away mode.
  302. """
  303. self._armed_away = value
  304. @property
  305. def armed_home(self):
  306. """
  307. Indicates whether or not the panel is armed in home/stay mode.
  308. """
  309. return self._armed_home
  310. @armed_home.setter
  311. def armed_home(self, value):
  312. """
  313. Sets the value indicating whether or not the panel is armed in home/stay mode.
  314. """
  315. self._armed_home = value
  316. @property
  317. def backlight_on(self):
  318. """
  319. Indicates whether or not the panel backlight is on.
  320. """
  321. return self._backlight_on
  322. @backlight_on.setter
  323. def backlight_on(self, value):
  324. """
  325. Sets the value indicating whether or not the panel backlight is on.
  326. """
  327. self._backlight_on = value
  328. @property
  329. def programming_mode(self):
  330. """
  331. Indicates whether or not the panel is in programming mode.
  332. """
  333. return self._programming_mode
  334. @programming_mode.setter
  335. def programming_mode(self, value):
  336. """
  337. Sets the value indicating whether or not the panel is in programming mode.
  338. """
  339. self._programming_mode = value
  340. @property
  341. def beeps(self):
  342. """
  343. Returns the number of beeps associated with this message.
  344. """
  345. return self._beeps
  346. @beeps.setter
  347. def beeps(self, value):
  348. """
  349. Sets the number of beeps associated with this message.
  350. """
  351. self._beeps = value
  352. @property
  353. def zone_bypassed(self):
  354. """
  355. Indicates whether or not zones have been bypassed.
  356. """
  357. return self._zone_bypassed
  358. @zone_bypassed.setter
  359. def zone_bypassed(self, value):
  360. """
  361. Sets the value indicating whether or not zones have been bypassed.
  362. """
  363. self._zone_bypassed = value
  364. @property
  365. def ac_power(self):
  366. """
  367. Indicates whether or not the system is on AC power.
  368. """
  369. return self._ac_power
  370. @ac_power.setter
  371. def ac_power(self, value):
  372. """
  373. Sets the value indicating whether or not the system is on AC power.
  374. """
  375. self._ac_power = value
  376. @property
  377. def chime_on(self):
  378. """
  379. Indicates whether or not panel chimes are enabled.
  380. """
  381. return self._chime_on
  382. @chime_on.setter
  383. def chime_on(self, value):
  384. """
  385. Sets the value indicating whether or not the panel chimes are enabled.
  386. """
  387. self._chime_on = value
  388. @property
  389. def alarm_event_occurred(self):
  390. """
  391. Indicates whether or not an alarm event has occurred.
  392. """
  393. return self._alarm_event_occurred
  394. @alarm_event_occurred.setter
  395. def alarm_event_occurred(self, value):
  396. """
  397. Sets the value indicating whether or not an alarm event has occurred.
  398. """
  399. self._alarm_event_occurred = value
  400. @property
  401. def alarm_sounding(self):
  402. """
  403. Indicates whether or not an alarm is currently sounding.
  404. """
  405. return self._alarm_sounding
  406. @alarm_sounding.setter
  407. def alarm_sounding(self, value):
  408. """
  409. Sets the value indicating whether or not an alarm is currently sounding.
  410. """
  411. self._alarm_sounding = value
  412. @property
  413. def numeric_code(self):
  414. """
  415. Numeric indicator of associated with message. For example: If zone #3 is faulted, this value is 003.
  416. """
  417. return self._numeric_code
  418. @numeric_code.setter
  419. def numeric_code(self, value):
  420. """
  421. Sets the numeric indicator associated with this message.
  422. """
  423. self._numeric_code = value
  424. @property
  425. def text(self):
  426. """
  427. Alphanumeric text associated with this message.
  428. """
  429. return self._text
  430. @text.setter
  431. def text(self, value):
  432. """
  433. Sets the alphanumeric text associated with this message.
  434. """
  435. self._text = value
  436. @property
  437. def cursor_location(self):
  438. """
  439. Indicates which text position has the cursor underneath it.
  440. """
  441. return self._cursor_location
  442. @cursor_location.setter
  443. def cursor_location(self, value):
  444. """
  445. Sets the value indicating which text position has the cursor underneath it.
  446. """
  447. self._cursor_location = value
  448. @property
  449. def data(self):
  450. """
  451. Raw representation of the message from the panel.
  452. """
  453. return self._data
  454. @data.setter
  455. def data(self, value):
  456. """
  457. Sets the raw representation of the message from the panel.
  458. """
  459. self._data = value
  460. @property
  461. def mask(self):
  462. """
  463. The panel mask for which this message is intended.
  464. """
  465. return self._mask
  466. @mask.setter
  467. def mask(self, value):
  468. """
  469. Sets the panel mask for which this message is intended.
  470. """
  471. self._mask = value
  472. @property
  473. def bitfield(self):
  474. """
  475. The bit field associated with this message.
  476. """
  477. return self._bitfield
  478. @bitfield.setter
  479. def bitfield(self, value):
  480. """
  481. Sets the bit field associated with this message.
  482. """
  483. self._bitfield = value
  484. @property
  485. def panel_data(self):
  486. """
  487. The binary field associated with this message.
  488. """
  489. return self._panel_data
  490. @panel_data.setter
  491. def panel_data(self, value):
  492. """
  493. Sets the binary field associated with this message.
  494. """
  495. self._panel_data = value
  496. class ExpanderMessage(object):
  497. """
  498. Represents a message from a zone or relay expansion module.
  499. """
  500. ZONE = 0
  501. RELAY = 1
  502. def __init__(self, data=None):
  503. """
  504. Constructor
  505. """
  506. self._type = None
  507. self._address = None
  508. self._channel = None
  509. self._value = None
  510. self._raw = None
  511. if data is not None:
  512. self._parse_message(data)
  513. def __str__(self):
  514. """
  515. String conversion operator.
  516. """
  517. expander_type = 'UNKWN'
  518. if self.type == ExpanderMessage.ZONE:
  519. expander_type = 'ZONE'
  520. elif self.type == ExpanderMessage.RELAY:
  521. expander_type = 'RELAY'
  522. return 'exp > [{0: <5}] {1}/{2} -- {3}'.format(expander_type, self.address, self.channel, self.value)
  523. def _parse_message(self, data):
  524. """
  525. Parse the raw message from the device.
  526. """
  527. header, values = data.split(':')
  528. address, channel, value = values.split(',')
  529. self.raw = data
  530. self.address = address
  531. self.channel = channel
  532. self.value = value
  533. if header == '!EXP':
  534. self.type = ExpanderMessage.ZONE
  535. elif header == '!REL':
  536. self.type = ExpanderMessage.RELAY
  537. @property
  538. def address(self):
  539. """
  540. The relay address from which the message originated.
  541. """
  542. return self._address
  543. @address.setter
  544. def address(self, value):
  545. """
  546. Sets the relay address from which the message originated.
  547. """
  548. self._address = value
  549. @property
  550. def channel(self):
  551. """
  552. The zone expander channel from which the message originated.
  553. """
  554. return self._channel
  555. @channel.setter
  556. def channel(self, value):
  557. """
  558. Sets the zone expander channel from which the message originated.
  559. """
  560. self._channel = value
  561. @property
  562. def value(self):
  563. """
  564. The value associated with the message.
  565. """
  566. return self._value
  567. @value.setter
  568. def value(self, value):
  569. """
  570. Sets the value associated with the message.
  571. """
  572. self._value = value
  573. @property
  574. def raw(self):
  575. """
  576. The raw message from the expander device.
  577. """
  578. return self._raw
  579. @raw.setter
  580. def raw(self, value):
  581. """
  582. Sets the raw message from the expander device.
  583. """
  584. self._value = value
  585. @property
  586. def type(self):
  587. """
  588. The type of expander associated with this message.
  589. """
  590. return self._type
  591. @type.setter
  592. def type(self, value):
  593. """
  594. Sets the type of expander associated with this message.
  595. """
  596. self._type = value
  597. class RFMessage(object):
  598. """
  599. Represents a message from an RF receiver.
  600. """
  601. def __init__(self, data=None):
  602. """
  603. Constructor
  604. """
  605. self._raw = None
  606. self._serial_number = None
  607. self._value = None
  608. if data is not None:
  609. self._parse_message(data)
  610. def __str__(self):
  611. """
  612. String conversion operator.
  613. """
  614. return 'rf > {0}: {1}'.format(self.serial_number, self.value)
  615. def _parse_message(self, data):
  616. """
  617. Parses the raw message from the device.
  618. """
  619. self.raw = data
  620. _, values = data.split(':')
  621. self.serial_number, self.value = values.split(',')
  622. @property
  623. def serial_number(self):
  624. """
  625. The serial number for the RF receiver.
  626. """
  627. return self._serial_number
  628. @serial_number.setter
  629. def serial_number(self, value):
  630. self._serial_number = value
  631. @property
  632. def value(self):
  633. """
  634. The value of the RF message.
  635. """
  636. return self._value
  637. @value.setter
  638. def value(self, value):
  639. """
  640. Sets the value of the RF message.
  641. """
  642. self._value = value
  643. @property
  644. def raw(self):
  645. """
  646. The raw message from the RF receiver.
  647. """
  648. return self._raw
  649. @raw.setter
  650. def raw(self, value):
  651. """
  652. Sets the raw message from the RF receiver.
  653. """
  654. self._raw = value