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.

458 lines
14 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. from . import messages
  13. from . import zonetracking
  14. class Overseer(object):
  15. """
  16. Factory for creation of AD2USB devices as well as provide4s attach/detach events."
  17. """
  18. # Factory events
  19. on_attached = event.Event('Called when an AD2USB device has been detected.')
  20. on_detached = event.Event('Called when an AD2USB device has been removed.')
  21. __devices = []
  22. @classmethod
  23. def find_all(cls):
  24. """
  25. Returns all AD2USB devices located on the system.
  26. """
  27. cls.__devices = devices.USBDevice.find_all()
  28. return cls.__devices
  29. @classmethod
  30. def devices(cls):
  31. """
  32. Returns a cached list of AD2USB devices located on the system.
  33. """
  34. return cls.__devices
  35. @classmethod
  36. def create(cls, device=None):
  37. """
  38. Factory method that returns the requested AD2USB device, or the first device.
  39. """
  40. cls.find_all()
  41. if len(cls.__devices) == 0:
  42. raise util.NoDeviceError('No AD2USB devices present.')
  43. if device is None:
  44. device = cls.__devices[0]
  45. vendor, product, sernum, ifcount, description = device
  46. device = devices.USBDevice(serial=sernum, description=description)
  47. return AD2USB(device)
  48. def __init__(self, attached_event=None, detached_event=None):
  49. """
  50. Constructor
  51. """
  52. self._detect_thread = Overseer.DetectThread(self)
  53. if attached_event:
  54. self.on_attached += attached_event
  55. if detached_event:
  56. self.on_detached += detached_event
  57. Overseer.find_all()
  58. self.start()
  59. def close(self):
  60. """
  61. Clean up and shut down.
  62. """
  63. self.stop()
  64. def start(self):
  65. """
  66. Starts the detection thread, if not already running.
  67. """
  68. if not self._detect_thread.is_alive():
  69. self._detect_thread.start()
  70. def stop(self):
  71. """
  72. Stops the detection thread.
  73. """
  74. self._detect_thread.stop()
  75. def get_device(self, device=None):
  76. """
  77. Factory method that returns the requested AD2USB device, or the first device.
  78. """
  79. return Overseer.create(device)
  80. class DetectThread(threading.Thread):
  81. """
  82. Thread that handles detection of added/removed devices.
  83. """
  84. def __init__(self, overseer):
  85. """
  86. Constructor
  87. """
  88. threading.Thread.__init__(self)
  89. self._overseer = overseer
  90. self._running = False
  91. def stop(self):
  92. """
  93. Stops the thread.
  94. """
  95. self._running = False
  96. def run(self):
  97. """
  98. The actual detection process.
  99. """
  100. self._running = True
  101. last_devices = set()
  102. while self._running:
  103. try:
  104. Overseer.find_all()
  105. current_devices = set(Overseer.devices())
  106. new_devices = [d for d in current_devices if d not in last_devices]
  107. removed_devices = [d for d in last_devices if d not in current_devices]
  108. last_devices = current_devices
  109. for d in new_devices:
  110. self._overseer.on_attached(d)
  111. for d in removed_devices:
  112. self._overseer.on_detached(d)
  113. except util.CommError, err:
  114. pass
  115. time.sleep(0.25)
  116. class AD2USB(object):
  117. """
  118. High-level wrapper around AD2USB/AD2SERIAL devices.
  119. """
  120. # High-level Events
  121. on_arm = event.Event('Called when the panel is armed.')
  122. on_disarm = event.Event('Called when the panel is disarmed.')
  123. on_power_changed = event.Event('Called when panel power switches between AC and DC.')
  124. on_alarm = event.Event('Called when the alarm is triggered.')
  125. on_fire = event.Event('Called when a fire is detected.')
  126. on_bypass = event.Event('Called when a zone is bypassed.')
  127. on_boot = event.Event('Called when the device finishes bootings.')
  128. on_config_received = event.Event('Called when the device receives its configuration.')
  129. on_zone_fault = event.Event('Called when the device detects a zone fault.')
  130. on_zone_restore = event.Event('Called when the device detects that a fault is restored.')
  131. on_low_battery = event.Event('Called when the device detects a low battery.')
  132. # Mid-level Events
  133. on_message = event.Event('Called when a message has been received from the device.')
  134. # Low-level Events
  135. on_open = event.Event('Called when the device has been opened.')
  136. on_close = event.Event('Called when the device has been closed.')
  137. on_read = event.Event('Called when a line has been read from the device.')
  138. on_write = event.Event('Called when data has been written to the device.')
  139. # Constants
  140. F1 = unichr(1) + unichr(1) + unichr(1)
  141. F2 = unichr(2) + unichr(2) + unichr(2)
  142. F3 = unichr(3) + unichr(3) + unichr(3)
  143. F4 = unichr(4) + unichr(4) + unichr(4)
  144. def __init__(self, device):
  145. """
  146. Constructor
  147. """
  148. self._device = device
  149. self._zonetracker = zonetracking.Zonetracker()
  150. self._power_status = None
  151. self._alarm_status = None
  152. self._bypass_status = None
  153. self._armed_status = None
  154. self._fire_status = None
  155. self._battery_status = None
  156. self.address = 18
  157. self.configbits = 0xFF00
  158. self.address_mask = 0x00000000
  159. self.emulate_zone = [False for x in range(5)]
  160. self.emulate_relay = [False for x in range(4)]
  161. self.emulate_lrr = False
  162. self.deduplicate = False
  163. @property
  164. def id(self):
  165. """
  166. The ID of the AD2USB device.
  167. """
  168. return self._device.id
  169. def open(self, baudrate=None, interface=None, index=None, no_reader_thread=False):
  170. """
  171. Opens the device.
  172. """
  173. self._wire_events()
  174. self._device.open(baudrate=baudrate, interface=interface, index=index, no_reader_thread=no_reader_thread)
  175. def close(self):
  176. """
  177. Closes the device.
  178. """
  179. self._device.close()
  180. del self._device
  181. self._device = None
  182. def get_config(self):
  183. """
  184. Retrieves the configuration from the device.
  185. """
  186. self._device.write("C\r")
  187. def save_config(self):
  188. """
  189. Sets configuration entries on the device.
  190. """
  191. config_string = ''
  192. # HACK: Both of these methods are ugly.. but I can't think of an elegant way of doing it.
  193. #config_string += 'ADDRESS={0}&'.format(self.address)
  194. #config_string += 'CONFIGBITS={0:x}&'.format(self.configbits)
  195. #config_string += 'MASK={0:x}&'.format(self.address_mask)
  196. #config_string += 'EXP={0}&'.format(''.join(['Y' if z else 'N' for z in self.emulate_zone]))
  197. #config_string += 'REL={0}&'.format(''.join(['Y' if r else 'N' for r in self.emulate_relay]))
  198. #config_string += 'LRR={0}&'.format('Y' if self.emulate_lrr else 'N')
  199. #config_string += 'DEDUPLICATE={0}'.format('Y' if self.deduplicate else 'N')
  200. config_entries = []
  201. config_entries.append(('ADDRESS', '{0}'.format(self.address)))
  202. config_entries.append(('CONFIGBITS', '{0:x}'.format(self.configbits)))
  203. config_entries.append(('MASK', '{0:x}'.format(self.address_mask)))
  204. config_entries.append(('EXP', ''.join(['Y' if z else 'N' for z in self.emulate_zone])))
  205. config_entries.append(('REL', ''.join(['Y' if r else 'N' for r in self.emulate_relay])))
  206. config_entries.append(('LRR', 'Y' if self.emulate_lrr else 'N'))
  207. config_entries.append(('DEDUPLICATE', 'Y' if self.deduplicate else 'N'))
  208. config_string = '&'.join(['='.join(t) for t in config_entries])
  209. self._device.write("C{0}\r".format(config_string))
  210. def reboot(self):
  211. """
  212. Reboots the device.
  213. """
  214. self._device.write('=')
  215. def fault_zone(self, zone, simulate_wire_problem=False):
  216. """
  217. Faults a zone if we are emulating a zone expander.
  218. """
  219. # Allow ourselves to also be passed an address/channel combination
  220. # for zone expanders.
  221. #
  222. # Format (expander index, channel)
  223. if isinstance(zone, tuple):
  224. zone = self._zonetracker._expander_to_zone(*zone)
  225. status = 2 if simulate_wire_problem else 1
  226. self._device.write("L{0:02}{1}\r".format(zone, status))
  227. def clear_zone(self, zone):
  228. """
  229. Clears a zone if we are emulating a zone expander.
  230. """
  231. self._device.write("L{0:02}0\r".format(zone))
  232. def _wire_events(self):
  233. """
  234. Wires up the internal device events.
  235. """
  236. self._device.on_open += self._on_open
  237. self._device.on_close += self._on_close
  238. self._device.on_read += self._on_read
  239. self._device.on_write += self._on_write
  240. self._zonetracker.on_fault += self._on_zone_fault
  241. self._zonetracker.on_restore += self._on_zone_restore
  242. def _handle_message(self, data):
  243. """
  244. Parses messages from the panel.
  245. """
  246. if data is None:
  247. return None
  248. msg = None
  249. if data[0] != '!':
  250. msg = messages.Message(data)
  251. if self.address_mask & msg.mask > 0:
  252. self._update_internal_states(msg)
  253. else: # specialty messages
  254. header = data[0:4]
  255. if header == '!EXP' or header == '!REL':
  256. msg = messages.ExpanderMessage(data)
  257. self._update_internal_states(msg)
  258. elif header == '!RFX':
  259. msg = messages.RFMessage(data)
  260. elif header == '!LRR':
  261. msg = messages.LRRMessage(data)
  262. elif data.startswith('!Ready'):
  263. self.on_boot()
  264. elif data.startswith('!CONFIG'):
  265. self._handle_config(data)
  266. return msg
  267. def _handle_config(self, data):
  268. """
  269. Handles received configuration data.
  270. """
  271. _, config_string = data.split('>')
  272. for setting in config_string.split('&'):
  273. k, v = setting.split('=')
  274. if k == 'ADDRESS':
  275. self.address = int(v)
  276. elif k == 'CONFIGBITS':
  277. self.configbits = int(v, 16)
  278. elif k == 'MASK':
  279. self.address_mask = int(v, 16)
  280. elif k == 'EXP':
  281. for z in range(5):
  282. self.emulate_zone[z] = True if v[z] == 'Y' else False
  283. elif k == 'REL':
  284. for r in range(4):
  285. self.emulate_relay[r] = True if v[r] == 'Y' else False
  286. elif k == 'LRR':
  287. self.emulate_lrr = True if v == 'Y' else False
  288. elif k == 'DEDUPLICATE':
  289. self.deduplicate = True if v == 'Y' else False
  290. self.on_config_received()
  291. def _update_internal_states(self, message):
  292. """
  293. Updates internal device states.
  294. """
  295. if isinstance(message, messages.Message):
  296. if message.ac_power != self._power_status:
  297. self._power_status, old_status = message.ac_power, self._power_status
  298. if old_status is not None:
  299. self.on_power_changed(self._power_status)
  300. if message.alarm_sounding != self._alarm_status:
  301. self._alarm_status, old_status = message.alarm_sounding, self._alarm_status
  302. if old_status is not None:
  303. self.on_alarm(self._alarm_status)
  304. if message.zone_bypassed != self._bypass_status:
  305. self._bypass_status, old_status = message.zone_bypassed, self._bypass_status
  306. if old_status is not None:
  307. self.on_bypass(self._bypass_status)
  308. if (message.armed_away | message.armed_home) != self._armed_status:
  309. self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status
  310. if old_status is not None:
  311. if self._armed_status:
  312. self.on_arm()
  313. else:
  314. self.on_disarm()
  315. # TODO: This needs a timeout or something.. only set on the LO BAT messages
  316. # instead of all messages after.
  317. if message.battery_low != self._battery_status:
  318. self._battery_status, old_status = message.battery_low, self._battery_status
  319. if old_status is not None and self._battery_status == True:
  320. self.on_low_battery(self._battery_status)
  321. # TODO: Also needs a timeout.
  322. if message.fire_alarm != self._fire_status:
  323. self._fire_status, old_status = message.fire_alarm, self._fire_status
  324. if old_status is not None:
  325. self.on_fire(self._fire_status)
  326. self._update_zone_tracker(message)
  327. def _update_zone_tracker(self, message):
  328. # Retrieve a list of faults.
  329. # NOTE: This only happens on first boot or after exiting programming mode.
  330. if isinstance(message, messages.Message):
  331. if not message.ready and "Hit * for faults" in message.text:
  332. self._device.write('*')
  333. return
  334. self._zonetracker.update(message)
  335. def _on_open(self, sender, args):
  336. """
  337. Internal handler for opening the device.
  338. """
  339. self.on_open(args)
  340. def _on_close(self, sender, args):
  341. """
  342. Internal handler for closing the device.
  343. """
  344. self.on_close(args)
  345. def _on_read(self, sender, args):
  346. """
  347. Internal handler for reading from the device.
  348. """
  349. self.on_read(args)
  350. msg = self._handle_message(args)
  351. if msg:
  352. self.on_message(msg)
  353. def _on_write(self, sender, args):
  354. """
  355. Internal handler for writing to the device.
  356. """
  357. self.on_write(args)
  358. def _on_zone_fault(self, sender, args):
  359. """
  360. Internal handler for zone faults.
  361. """
  362. self.on_zone_fault(args)
  363. def _on_zone_restore(self, sender, args):
  364. """
  365. Internal handler for zone restoration.
  366. """
  367. self.on_zone_restore(args)