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.

584 lines
18 KiB

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