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.

ad2usb.py 18 KiB

11 years ago
11 years ago
11 years ago
12 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. """
  2. Provides the full AD2USB class and factory.
  3. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  4. """
  5. import time
  6. import threading
  7. import re
  8. from .event import event
  9. from . import devices
  10. from . import util
  11. from . import messages
  12. from . import zonetracking
  13. class Overseer(object):
  14. """
  15. Factory for creation of AD2USB devices as well as provides attach/detach events."
  16. """
  17. # Factory events
  18. on_attached = event.Event('Called when an AD2USB device has been detected.')
  19. on_detached = event.Event('Called when an AD2USB device has been removed.')
  20. __devices = []
  21. @classmethod
  22. def find_all(cls):
  23. """
  24. Returns all AD2USB devices located on the system.
  25. :returns: list of devices found
  26. :raises: util.CommError
  27. """
  28. cls.__devices = devices.USBDevice.find_all()
  29. return cls.__devices
  30. @classmethod
  31. def devices(cls):
  32. """
  33. Returns a cached list of AD2USB devices located on the system.
  34. :returns: cached list of devices found.
  35. """
  36. return cls.__devices
  37. @classmethod
  38. def create(cls, device=None):
  39. """
  40. Factory method that returns the requested AD2USB device, or the first device.
  41. :param device: Tuple describing the USB device to open, as returned by find_all().
  42. :type device: tuple
  43. :returns: AD2USB object utilizing the specified device.
  44. :raises: util.NoDeviceError
  45. """
  46. cls.find_all()
  47. if len(cls.__devices) == 0:
  48. raise util.NoDeviceError('No AD2USB devices present.')
  49. if device is None:
  50. device = cls.__devices[0]
  51. vendor, product, sernum, ifcount, description = device
  52. device = devices.USBDevice(serial=sernum, description=description)
  53. return AD2USB(device)
  54. def __init__(self, attached_event=None, detached_event=None):
  55. """
  56. Constructor
  57. :param attached_event: Event to trigger when a device is attached.
  58. :type attached_event: function
  59. :param detached_event: Event to trigger when a device is detached.
  60. :type detached_event: function
  61. """
  62. self._detect_thread = Overseer.DetectThread(self)
  63. if attached_event:
  64. self.on_attached += attached_event
  65. if detached_event:
  66. self.on_detached += detached_event
  67. Overseer.find_all()
  68. self.start()
  69. def close(self):
  70. """
  71. Clean up and shut down.
  72. """
  73. self.stop()
  74. def start(self):
  75. """
  76. Starts the detection thread, if not already running.
  77. """
  78. if not self._detect_thread.is_alive():
  79. self._detect_thread.start()
  80. def stop(self):
  81. """
  82. Stops the detection thread.
  83. """
  84. self._detect_thread.stop()
  85. def get_device(self, device=None):
  86. """
  87. Factory method that returns the requested AD2USB device, or the first device.
  88. :param device: Tuple describing the USB device to open, as returned by find_all().
  89. :type device: tuple
  90. """
  91. return Overseer.create(device)
  92. class DetectThread(threading.Thread):
  93. """
  94. Thread that handles detection of added/removed devices.
  95. """
  96. def __init__(self, overseer):
  97. """
  98. Constructor
  99. :param overseer: Overseer object to use with the thread.
  100. :type overseer: Overseer
  101. """
  102. threading.Thread.__init__(self)
  103. self._overseer = overseer
  104. self._running = False
  105. def stop(self):
  106. """
  107. Stops the thread.
  108. """
  109. self._running = False
  110. def run(self):
  111. """
  112. The actual detection process.
  113. """
  114. self._running = True
  115. last_devices = set()
  116. while self._running:
  117. try:
  118. Overseer.find_all()
  119. current_devices = set(Overseer.devices())
  120. new_devices = [d for d in current_devices if d not in last_devices]
  121. removed_devices = [d for d in last_devices if d not in current_devices]
  122. last_devices = current_devices
  123. for d in new_devices:
  124. self._overseer.on_attached(d)
  125. for d in removed_devices:
  126. self._overseer.on_detached(d)
  127. except util.CommError, err:
  128. pass
  129. time.sleep(0.25)
  130. class AD2USB(object):
  131. """
  132. High-level wrapper around AD2USB/AD2SERIAL devices.
  133. """
  134. # High-level Events
  135. on_arm = event.Event('Called when the panel is armed.')
  136. on_disarm = event.Event('Called when the panel is disarmed.')
  137. on_power_changed = event.Event('Called when panel power switches between AC and DC.')
  138. on_alarm = event.Event('Called when the alarm is triggered.')
  139. on_fire = event.Event('Called when a fire is detected.')
  140. on_bypass = event.Event('Called when a zone is bypassed.')
  141. on_boot = event.Event('Called when the device finishes bootings.')
  142. on_config_received = event.Event('Called when the device receives its configuration.')
  143. on_zone_fault = event.Event('Called when the device detects a zone fault.')
  144. on_zone_restore = event.Event('Called when the device detects that a fault is restored.')
  145. on_low_battery = event.Event('Called when the device detects a low battery.')
  146. on_panic = event.Event('Called when the device detects a panic.')
  147. on_relay_changed = event.Event('Called when a relay is opened or closed on an expander board.')
  148. # Mid-level Events
  149. on_message = event.Event('Called when a message has been received from the device.')
  150. on_lrr_message = event.Event('Called when an LRR message is received.')
  151. on_rfx_message = event.Event('Called when an RFX message is received.')
  152. # Low-level Events
  153. on_open = event.Event('Called when the device has been opened.')
  154. on_close = event.Event('Called when the device has been closed.')
  155. on_read = event.Event('Called when a line has been read from the device.')
  156. on_write = event.Event('Called when data has been written to the device.')
  157. # Constants
  158. F1 = unichr(1) + unichr(1) + unichr(1)
  159. """Represents panel function key #1"""
  160. F2 = unichr(2) + unichr(2) + unichr(2)
  161. """Represents panel function key #2"""
  162. F3 = unichr(3) + unichr(3) + unichr(3)
  163. """Represents panel function key #3"""
  164. F4 = unichr(4) + unichr(4) + unichr(4)
  165. """Represents panel function key #4"""
  166. BATTERY_TIMEOUT = 30
  167. """Timeout before the battery status reverts."""
  168. FIRE_TIMEOUT = 30
  169. """Timeout before the fire status reverts."""
  170. def __init__(self, device):
  171. """
  172. Constructor
  173. :param device: The low-level device used for this AD2USB interface.
  174. :type device: devices.Device
  175. """
  176. self._device = device
  177. self._zonetracker = zonetracking.Zonetracker()
  178. self._power_status = None
  179. self._alarm_status = None
  180. self._bypass_status = None
  181. self._armed_status = None
  182. self._fire_status = (False, 0)
  183. self._battery_status = (False, 0)
  184. self._panic_status = None
  185. self._relay_status = {}
  186. self.address = 18
  187. self.configbits = 0xFF00
  188. self.address_mask = 0x00000000
  189. self.emulate_zone = [False for x in range(5)]
  190. self.emulate_relay = [False for x in range(4)]
  191. self.emulate_lrr = False
  192. self.deduplicate = False
  193. @property
  194. def id(self):
  195. """
  196. The ID of the AD2USB device.
  197. :returns: The identification string for the device.
  198. """
  199. return self._device.id
  200. def open(self, baudrate=None, no_reader_thread=False):
  201. """
  202. Opens the device.
  203. :param baudrate: The baudrate used for the device.
  204. :type baudrate: int
  205. :param interface: The interface used for the device.
  206. :type interface: varies depends on device type.. FIXME
  207. :param index: Interface index.. can probably remove. FIXME
  208. :type index: int
  209. :param no_reader_thread: Specifies whether or not the automatic reader thread should be started or not
  210. :type no_reader_thread: bool
  211. """
  212. self._wire_events()
  213. self._device.open(baudrate=baudrate, no_reader_thread=no_reader_thread)
  214. def close(self):
  215. """
  216. Closes the device.
  217. """
  218. self._device.close()
  219. del self._device
  220. self._device = None
  221. def send(self, data):
  222. if self._device:
  223. self._device.write(data)
  224. def get_config(self):
  225. """
  226. Retrieves the configuration from the device.
  227. """
  228. self._device.write("C\r")
  229. def save_config(self):
  230. """
  231. Sets configuration entries on the device.
  232. """
  233. config_string = ''
  234. # HACK: Both of these methods are ugly.. but I can't think of an elegant way of doing it.
  235. #config_string += 'ADDRESS={0}&'.format(self.address)
  236. #config_string += 'CONFIGBITS={0:x}&'.format(self.configbits)
  237. #config_string += 'MASK={0:x}&'.format(self.address_mask)
  238. #config_string += 'EXP={0}&'.format(''.join(['Y' if z else 'N' for z in self.emulate_zone]))
  239. #config_string += 'REL={0}&'.format(''.join(['Y' if r else 'N' for r in self.emulate_relay]))
  240. #config_string += 'LRR={0}&'.format('Y' if self.emulate_lrr else 'N')
  241. #config_string += 'DEDUPLICATE={0}'.format('Y' if self.deduplicate else 'N')
  242. config_entries = []
  243. config_entries.append(('ADDRESS', '{0}'.format(self.address)))
  244. config_entries.append(('CONFIGBITS', '{0:x}'.format(self.configbits)))
  245. config_entries.append(('MASK', '{0:x}'.format(self.address_mask)))
  246. config_entries.append(('EXP', ''.join(['Y' if z else 'N' for z in self.emulate_zone])))
  247. config_entries.append(('REL', ''.join(['Y' if r else 'N' for r in self.emulate_relay])))
  248. config_entries.append(('LRR', 'Y' if self.emulate_lrr else 'N'))
  249. config_entries.append(('DEDUPLICATE', 'Y' if self.deduplicate else 'N'))
  250. config_string = '&'.join(['='.join(t) for t in config_entries])
  251. self._device.write("C{0}\r".format(config_string))
  252. def reboot(self):
  253. """
  254. Reboots the device.
  255. """
  256. self._device.write('=')
  257. def fault_zone(self, zone, simulate_wire_problem=False):
  258. """
  259. Faults a zone if we are emulating a zone expander.
  260. :param zone: The zone to fault.
  261. :type zone: int
  262. :param simulate_wire_problem: Whether or not to simulate a wire fault.
  263. :type simulate_wire_problem: bool
  264. """
  265. # Allow ourselves to also be passed an address/channel combination
  266. # for zone expanders.
  267. #
  268. # Format (expander index, channel)
  269. if isinstance(zone, tuple):
  270. zone = self._zonetracker._expander_to_zone(*zone)
  271. status = 2 if simulate_wire_problem else 1
  272. self._device.write("L{0:02}{1}\r".format(zone, status))
  273. def clear_zone(self, zone):
  274. """
  275. Clears a zone if we are emulating a zone expander.
  276. :param zone: The zone to clear.
  277. :type zone: int
  278. """
  279. self._device.write("L{0:02}0\r".format(zone))
  280. def _wire_events(self):
  281. """
  282. Wires up the internal device events.
  283. """
  284. self._device.on_open += self._on_open
  285. self._device.on_close += self._on_close
  286. self._device.on_read += self._on_read
  287. self._device.on_write += self._on_write
  288. self._zonetracker.on_fault += self._on_zone_fault
  289. self._zonetracker.on_restore += self._on_zone_restore
  290. def _handle_message(self, data):
  291. """
  292. Parses messages from the panel.
  293. :param data: Panel data to parse.
  294. :type data: str
  295. :returns: An object representing the message.
  296. """
  297. if data is None:
  298. return None
  299. msg = None
  300. header = data[0:4]
  301. if header[0] != '!' or header == '!KPE':
  302. msg = messages.Message(data)
  303. if self.address_mask & msg.mask > 0:
  304. self._update_internal_states(msg)
  305. elif header == '!EXP' or header == '!REL':
  306. msg = messages.ExpanderMessage(data)
  307. self._update_internal_states(msg)
  308. elif header == '!RFX':
  309. msg = self._handle_rfx(data)
  310. elif header == '!LRR':
  311. msg = self._handle_lrr(data)
  312. elif data.startswith('!Ready'):
  313. self.on_boot()
  314. elif data.startswith('!CONFIG'):
  315. self._handle_config(data)
  316. return msg
  317. def _handle_rfx(self, data):
  318. msg = messages.RFMessage(data)
  319. self.on_rfx_message(msg)
  320. return msg
  321. def _handle_lrr(self, data):
  322. """
  323. Handle Long Range Radio messages.
  324. :param data: LRR message to parse.
  325. :type data: str
  326. :returns: An object representing the LRR message.
  327. """
  328. msg = messages.LRRMessage(data)
  329. args = (msg.partition, msg.event_type, msg.event_data)
  330. if msg._event_type == 'ALARM_PANIC':
  331. self._panic_status = True
  332. self.on_panic(args + (True,))
  333. elif msg._event_type == 'CANCEL':
  334. if self._panic_status == True:
  335. self._panic_status = False
  336. self.on_panic(args + (False,))
  337. self.on_lrr_message(args)
  338. return msg
  339. def _handle_config(self, data):
  340. """
  341. Handles received configuration data.
  342. :param data: Configuration string to parse.
  343. :type data: str
  344. """
  345. _, config_string = data.split('>')
  346. for setting in config_string.split('&'):
  347. k, v = setting.split('=')
  348. if k == 'ADDRESS':
  349. self.address = int(v)
  350. elif k == 'CONFIGBITS':
  351. self.configbits = int(v, 16)
  352. elif k == 'MASK':
  353. self.address_mask = int(v, 16)
  354. elif k == 'EXP':
  355. for z in range(5):
  356. self.emulate_zone[z] = (v[z] == 'Y')
  357. elif k == 'REL':
  358. for r in range(4):
  359. self.emulate_relay[r] = (v[r] == 'Y')
  360. elif k == 'LRR':
  361. self.emulate_lrr = (v == 'Y')
  362. elif k == 'DEDUPLICATE':
  363. self.deduplicate = (v == 'Y')
  364. self.on_config_received()
  365. def _update_internal_states(self, message):
  366. """
  367. Updates internal device states.
  368. :param message: Message to update internal states with.
  369. :type message: Message, ExpanderMessage, LRRMessage, or RFMessage
  370. """
  371. if isinstance(message, messages.Message):
  372. if message.ac_power != self._power_status:
  373. self._power_status, old_status = message.ac_power, self._power_status
  374. if old_status is not None:
  375. self.on_power_changed(self._power_status)
  376. if message.alarm_sounding != self._alarm_status:
  377. self._alarm_status, old_status = message.alarm_sounding, self._alarm_status
  378. if old_status is not None:
  379. self.on_alarm(self._alarm_status)
  380. if message.zone_bypassed != self._bypass_status:
  381. self._bypass_status, old_status = message.zone_bypassed, self._bypass_status
  382. if old_status is not None:
  383. self.on_bypass(self._bypass_status)
  384. if (message.armed_away | message.armed_home) != self._armed_status:
  385. self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status
  386. if old_status is not None:
  387. if self._armed_status:
  388. self.on_arm()
  389. else:
  390. self.on_disarm()
  391. if message.battery_low == self._battery_status[0]:
  392. self._battery_status = (self._battery_status[0], time.time())
  393. else:
  394. if message.battery_low == True or time.time() > self._battery_status[1] + AD2USB.BATTERY_TIMEOUT:
  395. self._battery_status = (message.battery_low, time.time())
  396. self.on_low_battery(self._battery_status)
  397. if message.fire_alarm == self._fire_status[0]:
  398. self._fire_status = (self._fire_status[0], time.time())
  399. else:
  400. if message.fire_alarm == True or time.time() > self._fire_status[1] + AD2USB.FIRE_TIMEOUT:
  401. self._fire_status = (message.fire_alarm, time.time())
  402. self.on_fire(self._fire_status)
  403. elif isinstance(message, messages.ExpanderMessage):
  404. if message.type == messages.ExpanderMessage.RELAY:
  405. self._relay_status[(message.address, message.channel)] = message.value
  406. self.on_relay_changed(message)
  407. self._update_zone_tracker(message)
  408. def _update_zone_tracker(self, message):
  409. """
  410. Trigger an update of the zonetracker.
  411. :param message: The message to update the zonetracker with.
  412. :type message: Message, ExpanderMessage, LRRMessage, or RFMessage
  413. """
  414. # Retrieve a list of faults.
  415. # NOTE: This only happens on first boot or after exiting programming mode.
  416. if isinstance(message, messages.Message):
  417. if not message.ready and "Hit * for faults" in message.text:
  418. self._device.write('*')
  419. return
  420. self._zonetracker.update(message)
  421. def _on_open(self, sender, args):
  422. """
  423. Internal handler for opening the device.
  424. """
  425. self.on_open(args)
  426. def _on_close(self, sender, args):
  427. """
  428. Internal handler for closing the device.
  429. """
  430. self.on_close(args)
  431. def _on_read(self, sender, args):
  432. """
  433. Internal handler for reading from the device.
  434. """
  435. self.on_read(args)
  436. msg = self._handle_message(args)
  437. if msg:
  438. self.on_message(msg)
  439. def _on_write(self, sender, args):
  440. """
  441. Internal handler for writing to the device.
  442. """
  443. self.on_write(args)
  444. def _on_zone_fault(self, sender, args):
  445. """
  446. Internal handler for zone faults.
  447. """
  448. self.on_zone_fault(args)
  449. def _on_zone_restore(self, sender, args):
  450. """
  451. Internal handler for zone restoration.
  452. """
  453. self.on_zone_restore(args)