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.

431 lines
12 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 close(self):
  56. """
  57. Clean up and shut down.
  58. """
  59. self.stop()
  60. def start(self):
  61. """
  62. Starts the detection thread, if not already running.
  63. """
  64. if not self._detect_thread.is_alive():
  65. self._detect_thread.start()
  66. def stop(self):
  67. """
  68. Stops the detection thread.
  69. """
  70. self._detect_thread.stop()
  71. def get_device(self, device=None):
  72. """
  73. Factory method that returns the requested AD2USB device, or the first device.
  74. """
  75. return Overseer.create(device)
  76. class DetectThread(threading.Thread):
  77. """
  78. Thread that handles detection of added/removed devices.
  79. """
  80. def __init__(self, overseer):
  81. """
  82. Constructor
  83. """
  84. threading.Thread.__init__(self)
  85. self._overseer = overseer
  86. self._running = False
  87. def stop(self):
  88. """
  89. Stops the thread.
  90. """
  91. self._running = False
  92. def run(self):
  93. """
  94. The actual detection process.
  95. """
  96. self._running = True
  97. last_devices = set()
  98. while self._running:
  99. try:
  100. Overseer.find_all()
  101. current_devices = set(Overseer.devices())
  102. new_devices = [d for d in current_devices if d not in last_devices]
  103. removed_devices = [d for d in last_devices if d not in current_devices]
  104. last_devices = current_devices
  105. for d in new_devices:
  106. self._overseer.on_attached(d)
  107. for d in removed_devices:
  108. self._overseer.on_detached(d)
  109. except util.CommError, err:
  110. pass
  111. time.sleep(0.25)
  112. class AD2USB(object):
  113. """
  114. High-level wrapper around AD2USB/AD2SERIAL devices.
  115. """
  116. # High-level Events
  117. on_status_changed = event.Event('Called when the panel status changes.')
  118. on_power_changed = event.Event('Called when panel power switches between AC and DC.')
  119. on_alarm = event.Event('Called when the alarm is triggered.')
  120. on_bypass = event.Event('Called when a zone is bypassed.')
  121. on_boot = event.Event('Called when the device finishes bootings.')
  122. # Mid-level Events
  123. on_message = event.Event('Called when a message has been received from the device.')
  124. # Low-level Events
  125. on_open = event.Event('Called when the device has been opened.')
  126. on_close = event.Event('Called when the device has been closed.')
  127. on_read = event.Event('Called when a line has been read from the device.')
  128. on_write = event.Event('Called when data has been written to the device.')
  129. def __init__(self, device):
  130. """
  131. Constructor
  132. """
  133. self._device = device
  134. self._power_status = None
  135. self._alarm_status = None
  136. self._bypass_status = None
  137. self._address_mask = 0xFF80 # TEMP
  138. def open(self, baudrate=None, interface=None, index=None, no_reader_thread=False):
  139. """
  140. Opens the device.
  141. """
  142. self._wire_events()
  143. self._device.open(baudrate=baudrate, interface=interface, index=index, no_reader_thread=no_reader_thread)
  144. def close(self):
  145. """
  146. Closes the device.
  147. """
  148. self._device.close()
  149. self._device = None
  150. def reboot(self):
  151. """
  152. Reboots the device.
  153. """
  154. self._device.write('=')
  155. @property
  156. def id(self):
  157. return self._device.id
  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 is None:
  171. return None
  172. msg = None
  173. if data[0] != '!':
  174. msg = Message(data)
  175. if self._address_mask & msg.mask > 0:
  176. self._update_internal_states(msg)
  177. else: # specialty messages
  178. header = data[0:4]
  179. if header == '!EXP' or header == '!REL':
  180. msg = ExpanderMessage(data)
  181. elif header == '!RFX':
  182. msg = RFMessage(data)
  183. elif data.startswith('!Ready'):
  184. self.on_boot()
  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. class ExpanderMessage(object):
  281. """
  282. Represents a message from a zone or relay expansion module.
  283. """
  284. ZONE = 0
  285. RELAY = 1
  286. def __init__(self, data=None):
  287. """
  288. Constructor
  289. """
  290. self.type = None
  291. self.address = None
  292. self.channel = None
  293. self.value = None
  294. self.raw = None
  295. if data is not None:
  296. self._parse_message(data)
  297. def __str__(self):
  298. """
  299. String conversion operator.
  300. """
  301. expander_type = 'UNKWN'
  302. if self.type == ExpanderMessage.ZONE:
  303. expander_type = 'ZONE'
  304. elif self.type == ExpanderMessage.RELAY:
  305. expander_type = 'RELAY'
  306. return 'exp > [{0: <5}] {1}/{2} -- {3}'.format(expander_type, self.address, self.channel, self.value)
  307. def _parse_message(self, data):
  308. """
  309. Parse the raw message from the device.
  310. """
  311. header, values = data.split(':')
  312. address, channel, value = values.split(',')
  313. self.raw = data
  314. self.address = address
  315. self.channel = channel
  316. self.value = value
  317. if header == '!EXP':
  318. self.type = ExpanderMessage.ZONE
  319. elif header == '!REL':
  320. self.type = ExpanderMessage.RELAY
  321. class RFMessage(object):
  322. """
  323. Represents a message from an RF receiver.
  324. """
  325. def __init__(self, data=None):
  326. """
  327. Constructor
  328. """
  329. self.raw = None
  330. self.serial_number = None
  331. self.value = None
  332. if data is not None:
  333. self._parse_message(data)
  334. def __str__(self):
  335. """
  336. String conversion operator.
  337. """
  338. return 'rf > {0}: {1}'.format(self.serial_number, self.value)
  339. def _parse_message(self, data):
  340. """
  341. Parses the raw message from the device.
  342. """
  343. self.raw = data
  344. _, values = data.split(':')
  345. self.serial_number, self.value = values.split(',')