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.

574 lines
17 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 .event import event
  9. from . import devices
  10. from . import util
  11. class Overseer(object):
  12. """
  13. Factory for creation of AD2USB devices as well as provide4s attach/detach events."
  14. """
  15. # Factory events
  16. on_attached = event.Event('Called when an AD2USB device has been detected.')
  17. on_detached = event.Event('Called when an AD2USB device has been removed.')
  18. __devices = []
  19. @classmethod
  20. def find_all(cls):
  21. """
  22. Returns all AD2USB devices located on the system.
  23. """
  24. cls.__devices = devices.USBDevice.find_all()
  25. return cls.__devices
  26. @classmethod
  27. def devices(cls):
  28. """
  29. Returns a cached list of AD2USB devices located on the system.
  30. """
  31. return cls.__devices
  32. @classmethod
  33. def create(cls, device=None):
  34. """
  35. Factory method that returns the requested AD2USB device, or the first device.
  36. """
  37. cls.find_all()
  38. if len(cls.__devices) == 0:
  39. raise util.NoDeviceError('No AD2USB devices present.')
  40. if device is None:
  41. device = cls.__devices[0]
  42. vendor, product, sernum, ifcount, description = device
  43. device = devices.USBDevice(serial=sernum, description=description)
  44. return AD2USB(device)
  45. def __init__(self, attached_event=None, detached_event=None):
  46. """
  47. Constructor
  48. """
  49. self._detect_thread = Overseer.DetectThread(self)
  50. if attached_event:
  51. self.on_attached += attached_event
  52. if detached_event:
  53. self.on_detached += detached_event
  54. Overseer.find_all()
  55. self.start()
  56. def close(self):
  57. """
  58. Clean up and shut down.
  59. """
  60. self.stop()
  61. def start(self):
  62. """
  63. Starts the detection thread, if not already running.
  64. """
  65. if not self._detect_thread.is_alive():
  66. self._detect_thread.start()
  67. def stop(self):
  68. """
  69. Stops the detection thread.
  70. """
  71. self._detect_thread.stop()
  72. def get_device(self, device=None):
  73. """
  74. Factory method that returns the requested AD2USB device, or the first device.
  75. """
  76. return Overseer.create(device)
  77. class DetectThread(threading.Thread):
  78. """
  79. Thread that handles detection of added/removed devices.
  80. """
  81. def __init__(self, overseer):
  82. """
  83. Constructor
  84. """
  85. threading.Thread.__init__(self)
  86. self._overseer = overseer
  87. self._running = False
  88. def stop(self):
  89. """
  90. Stops the thread.
  91. """
  92. self._running = False
  93. def run(self):
  94. """
  95. The actual detection process.
  96. """
  97. self._running = True
  98. last_devices = set()
  99. while self._running:
  100. try:
  101. Overseer.find_all()
  102. current_devices = set(Overseer.devices())
  103. new_devices = [d for d in current_devices if d not in last_devices]
  104. removed_devices = [d for d in last_devices if d not in current_devices]
  105. last_devices = current_devices
  106. for d in new_devices:
  107. self._overseer.on_attached(d)
  108. for d in removed_devices:
  109. self._overseer.on_detached(d)
  110. except util.CommError, err:
  111. pass
  112. time.sleep(0.25)
  113. class AD2USB(object):
  114. """
  115. High-level wrapper around AD2USB/AD2SERIAL devices.
  116. """
  117. # High-level Events
  118. on_arm = event.Event('Called when the panel is armed.')
  119. on_disarm = event.Event('Called when the panel is disarmed.')
  120. on_power_changed = event.Event('Called when panel power switches between AC and DC.')
  121. on_alarm = event.Event('Called when the alarm is triggered.')
  122. on_fire = event.Event('Called when a fire is detected.')
  123. on_bypass = event.Event('Called when a zone is bypassed.')
  124. on_boot = event.Event('Called when the device finishes bootings.')
  125. on_config_received = event.Event('Called when the device receives its configuration.')
  126. # Mid-level Events
  127. on_message = event.Event('Called when a message has been received from the device.')
  128. # Low-level Events
  129. on_open = event.Event('Called when the device has been opened.')
  130. on_close = event.Event('Called when the device has been closed.')
  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. # Constants
  134. F1 = unichr(1) + unichr(1) + unichr(1)
  135. F2 = unichr(2) + unichr(2) + unichr(2)
  136. F3 = unichr(3) + unichr(3) + unichr(3)
  137. F4 = unichr(4) + unichr(4) + unichr(4)
  138. def __init__(self, device):
  139. """
  140. Constructor
  141. """
  142. self._device = device
  143. self._power_status = None
  144. self._alarm_status = None
  145. self._bypass_status = None
  146. self._armed_status = None
  147. self._fire_status = None
  148. self.address = 18
  149. self.configbits = 0xFF00
  150. self.address_mask = 0x00000000
  151. self.emulate_zone = [False for x in range(5)]
  152. self.emulate_relay = [False for x in range(4)]
  153. self.emulate_lrr = False
  154. self.deduplicate = False
  155. @property
  156. def id(self):
  157. return self._device.id
  158. def open(self, baudrate=None, interface=None, index=None, no_reader_thread=False):
  159. """
  160. Opens the device.
  161. """
  162. self._wire_events()
  163. self._device.open(baudrate=baudrate, interface=interface, index=index, no_reader_thread=no_reader_thread)
  164. def close(self):
  165. """
  166. Closes the device.
  167. """
  168. self._device.close()
  169. self._device = None
  170. def get_config(self):
  171. """
  172. Retrieves the configuration from the device.
  173. """
  174. self._device.write("C\r")
  175. def save_config(self):
  176. """
  177. Sets configuration entries on the device.
  178. """
  179. config_string = ''
  180. # HACK: Both of these methods are ugly.. but I can't think of an elegant way of doing it.
  181. #config_string += 'ADDRESS={0}&'.format(self.address)
  182. #config_string += 'CONFIGBITS={0:x}&'.format(self.configbits)
  183. #config_string += 'MASK={0:x}&'.format(self.address_mask)
  184. #config_string += 'EXP={0}&'.format(''.join(['Y' if z else 'N' for z in self.emulate_zone]))
  185. #config_string += 'REL={0}&'.format(''.join(['Y' if r else 'N' for r in self.emulate_relay]))
  186. #config_string += 'LRR={0}&'.format('Y' if self.emulate_lrr else 'N')
  187. #config_string += 'DEDUPLICATE={0}'.format('Y' if self.deduplicate else 'N')
  188. config_entries = []
  189. config_entries.append(('ADDRESS', '{0}'.format(self.address)))
  190. config_entries.append(('CONFIGBITS', '{0:x}'.format(self.configbits)))
  191. config_entries.append(('MASK', '{0:x}'.format(self.address_mask)))
  192. config_entries.append(('EXP', ''.join(['Y' if z else 'N' for z in self.emulate_zone])))
  193. config_entries.append(('REL', ''.join(['Y' if r else 'N' for r in self.emulate_relay])))
  194. config_entries.append(('LRR', 'Y' if self.emulate_lrr else 'N'))
  195. config_entries.append(('DEDUPLICATE', 'Y' if self.deduplicate else 'N'))
  196. config_string = '&'.join(['='.join(t) for t in config_entries])
  197. self._device.write("C{0}\r".format(config_string))
  198. def reboot(self):
  199. """
  200. Reboots the device.
  201. """
  202. self._device.write('=')
  203. def fault_zone(self, zone, simulate_wire_problem=False):
  204. """
  205. Faults a zone if we are emulating a zone expander.
  206. """
  207. status = 2 if simulate_wire_problem else 1
  208. self._device.write("L{0:02}{1}".format(zone, status))
  209. def clear_zone(self, zone):
  210. """
  211. Clears a zone if we are emulating a zone expander.
  212. """
  213. self._device.write("L{0:02}0".format(zone))
  214. def _wire_events(self):
  215. """
  216. Wires up the internal device events.
  217. """
  218. self._device.on_open += self._on_open
  219. self._device.on_close += self._on_close
  220. self._device.on_read += self._on_read
  221. self._device.on_write += self._on_write
  222. def _handle_message(self, data):
  223. """
  224. Parses messages from the panel.
  225. """
  226. if data is None:
  227. return None
  228. msg = None
  229. if data[0] != '!':
  230. msg = Message(data)
  231. if self.address_mask & msg.mask > 0:
  232. self._update_internal_states(msg)
  233. else: # specialty messages
  234. header = data[0:4]
  235. if header == '!EXP' or header == '!REL':
  236. msg = ExpanderMessage(data)
  237. elif header == '!RFX':
  238. msg = RFMessage(data)
  239. elif header == '!LRR':
  240. msg = LRRMessage(data)
  241. elif data.startswith('!Ready'):
  242. self.on_boot()
  243. elif data.startswith('!CONFIG'):
  244. self._handle_config(data)
  245. return msg
  246. def _handle_config(self, data):
  247. _, config_string = data.split('>')
  248. for setting in config_string.split('&'):
  249. k, v = setting.split('=')
  250. if k == 'ADDRESS':
  251. self.address = int(v)
  252. elif k == 'CONFIGBITS':
  253. self.configbits = int(v, 16)
  254. elif k == 'MASK':
  255. self.address_mask = int(v, 16)
  256. elif k == 'EXP':
  257. for z in range(5):
  258. self.emulate_zone[z] = True if v[z] == 'Y' else False
  259. elif k == 'REL':
  260. for r in range(4):
  261. self.emulate_relay[r] = True if v[r] == 'Y' else False
  262. elif k == 'LRR':
  263. self.emulate_lrr = True if v == 'Y' else False
  264. elif k == 'DEDUPLICATE':
  265. self.deduplicate = True if v == 'Y' else False
  266. self.on_config_received()
  267. def _update_internal_states(self, message):
  268. if message.ac_power != self._power_status:
  269. self._power_status, old_status = message.ac_power, self._power_status
  270. if old_status is not None:
  271. self.on_power_changed(self._power_status)
  272. if message.alarm_sounding != self._alarm_status:
  273. self._alarm_status, old_status = message.alarm_sounding, self._alarm_status
  274. if old_status is not None:
  275. self.on_alarm(self._alarm_status)
  276. if message.zone_bypassed != self._bypass_status:
  277. self._bypass_status, old_status = message.zone_bypassed, self._bypass_status
  278. if old_status is not None:
  279. self.on_bypass(self._bypass_status)
  280. if (message.armed_away | message.armed_home) != self._armed_status:
  281. self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status
  282. if old_status is not None:
  283. if self._armed_status:
  284. self.on_arm()
  285. else:
  286. self.on_disarm()
  287. if message.fire_alarm != self._fire_status:
  288. self._fire_status, old_status = message.fire_alarm, self._fire_status
  289. if old_status is not None:
  290. self.on_fire(self._fire_status)
  291. def _on_open(self, sender, args):
  292. """
  293. Internal handler for opening the device.
  294. """
  295. self.on_open(args)
  296. def _on_close(self, sender, args):
  297. """
  298. Internal handler for closing the device.
  299. """
  300. self.on_close(args)
  301. def _on_read(self, sender, args):
  302. """
  303. Internal handler for reading from the device.
  304. """
  305. self.on_read(args)
  306. msg = self._handle_message(args)
  307. if msg:
  308. self.on_message(msg)
  309. def _on_write(self, sender, args):
  310. """
  311. Internal handler for writing to the device.
  312. """
  313. self.on_write(args)
  314. class Message(object):
  315. """
  316. Represents a message from the alarm panel.
  317. """
  318. def __init__(self, data=None):
  319. """
  320. Constructor
  321. """
  322. self.ready = False
  323. self.armed_away = False
  324. self.armed_home = False
  325. self.backlight_on = False
  326. self.programming_mode = False
  327. self.beeps = -1
  328. self.zone_bypassed = False
  329. self.ac_power = False
  330. self.chime_on = False
  331. self.alarm_event_occurred = False
  332. self.alarm_sounding = False
  333. self.fire_alarm = False
  334. self.numeric_code = ""
  335. self.text = ""
  336. self.cursor_location = -1
  337. self.data = ""
  338. self.mask = ""
  339. self.bitfield = ""
  340. self.panel_data = ""
  341. self._regex = re.compile('("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*)')
  342. if data is not None:
  343. self._parse_message(data)
  344. def _parse_message(self, data):
  345. """
  346. Parse the message from the device.
  347. """
  348. m = self._regex.match(data)
  349. if m is None:
  350. raise util.InvalidMessageError('Received invalid message: {0}'.format(data))
  351. self.bitfield, self.numeric_code, self.panel_data, alpha = m.group(1, 2, 3, 4)
  352. self.mask = int(self.panel_data[3:3+8], 16)
  353. self.data = data
  354. self.ready = not self.bitfield[1:2] == "0"
  355. self.armed_away = not self.bitfield[2:3] == "0"
  356. self.armed_home = not self.bitfield[3:4] == "0"
  357. self.backlight_on = not self.bitfield[4:5] == "0"
  358. self.programming_mode = not self.bitfield[5:6] == "0"
  359. self.beeps = int(self.bitfield[6:7], 16)
  360. self.zone_bypassed = not self.bitfield[7:8] == "0"
  361. self.ac_power = not self.bitfield[8:9] == "0"
  362. self.chime_on = not self.bitfield[9:10] == "0"
  363. self.alarm_event_occurred = not self.bitfield[10:11] == "0"
  364. self.alarm_sounding = not self.bitfield[11:12] == "0"
  365. self.fire_alarm = not self.bitfield[13:14] == "0"
  366. self.text = alpha.strip('"')
  367. if int(self.panel_data[19:21], 16) & 0x01 > 0:
  368. self.cursor_location = int(self.bitfield[21:23], 16) # Alpha character index that the cursor is on.
  369. def __str__(self):
  370. """
  371. String conversion operator.
  372. """
  373. 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)
  374. class ExpanderMessage(object):
  375. """
  376. Represents a message from a zone or relay expansion module.
  377. """
  378. ZONE = 0
  379. RELAY = 1
  380. def __init__(self, data=None):
  381. """
  382. Constructor
  383. """
  384. self.type = None
  385. self.address = None
  386. self.channel = None
  387. self.value = None
  388. self.raw = None
  389. if data is not None:
  390. self._parse_message(data)
  391. def __str__(self):
  392. """
  393. String conversion operator.
  394. """
  395. expander_type = 'UNKWN'
  396. if self.type == ExpanderMessage.ZONE:
  397. expander_type = 'ZONE'
  398. elif self.type == ExpanderMessage.RELAY:
  399. expander_type = 'RELAY'
  400. return 'exp > [{0: <5}] {1}/{2} -- {3}'.format(expander_type, self.address, self.channel, self.value)
  401. def _parse_message(self, data):
  402. """
  403. Parse the raw message from the device.
  404. """
  405. header, values = data.split(':')
  406. address, channel, value = values.split(',')
  407. self.raw = data
  408. self.address = address
  409. self.channel = channel
  410. self.value = value
  411. if header == '!EXP':
  412. self.type = ExpanderMessage.ZONE
  413. elif header == '!REL':
  414. self.type = ExpanderMessage.RELAY
  415. class RFMessage(object):
  416. """
  417. Represents a message from an RF receiver.
  418. """
  419. def __init__(self, data=None):
  420. """
  421. Constructor
  422. """
  423. self.raw = None
  424. self.serial_number = None
  425. self.value = None
  426. if data is not None:
  427. self._parse_message(data)
  428. def __str__(self):
  429. """
  430. String conversion operator.
  431. """
  432. return 'rf > {0}: {1}'.format(self.serial_number, self.value)
  433. def _parse_message(self, data):
  434. """
  435. Parses the raw message from the device.
  436. """
  437. self.raw = data
  438. _, values = data.split(':')
  439. self.serial_number, self.value = values.split(',')
  440. class LRRMessage(object):
  441. """
  442. Represent a message from a Long Range Radio.
  443. """
  444. def __init__(self, data=None):
  445. """
  446. Constructor
  447. """
  448. self.raw = None
  449. self._event_data = None
  450. self._partition = None
  451. self._event_type = None
  452. if data is not None:
  453. self._parse_message(data)
  454. def __str__(self):
  455. """
  456. String conversion operator.
  457. """
  458. return 'lrr > {0} @ {1} -- {2}'.format()
  459. def _parse_message(self, data):
  460. """
  461. Parses the raw message from the device.
  462. """
  463. self.raw = data
  464. _, values = data.split(':')
  465. self._event_data, self._partition, self._event_type = values.split(',')