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.

579 lines
18 KiB

  1. """
  2. Provides the full AD2 class and factory.
  3. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  4. """
  5. import time
  6. import threading
  7. from .event import event
  8. from .devices import USBDevice
  9. from .util import CommError, NoDeviceError
  10. from .messages import Message, ExpanderMessage, RFMessage, LRRMessage
  11. from .zonetracking import Zonetracker
  12. class AD2Factory(object):
  13. """
  14. Factory for creation of AD2USB devices as well as provides attach/detach events."
  15. """
  16. # Factory events
  17. on_attached = event.Event('Called when an AD2USB device has been detected.')
  18. on_detached = event.Event('Called when an AD2USB device has been removed.')
  19. __devices = []
  20. @classmethod
  21. def find_all(cls):
  22. """
  23. Returns all AD2USB devices located on the system.
  24. :returns: list of devices found
  25. :raises: CommError
  26. """
  27. cls.__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. :returns: cached list of devices found.
  34. """
  35. return cls.__devices
  36. @classmethod
  37. def create(cls, device=None):
  38. """
  39. Factory method that returns the requested AD2USB device, or the first device.
  40. :param device: Tuple describing the USB device to open, as returned by find_all().
  41. :type device: tuple
  42. :returns: AD2USB object utilizing the specified device.
  43. :raises: NoDeviceError
  44. """
  45. cls.find_all()
  46. if len(cls.__devices) == 0:
  47. raise NoDeviceError('No AD2USB devices present.')
  48. if device is None:
  49. device = cls.__devices[0]
  50. vendor, product, sernum, ifcount, description = device
  51. device = USBDevice((sernum, ifcount - 1))
  52. return AD2(device)
  53. def __init__(self, attached_event=None, detached_event=None):
  54. """
  55. Constructor
  56. :param attached_event: Event to trigger when a device is attached.
  57. :type attached_event: function
  58. :param detached_event: Event to trigger when a device is detached.
  59. :type detached_event: function
  60. """
  61. self._detect_thread = AD2Factory.DetectThread(self)
  62. if attached_event:
  63. self.on_attached += attached_event
  64. if detached_event:
  65. self.on_detached += detached_event
  66. AD2Factory.find_all()
  67. self.start()
  68. def close(self):
  69. """
  70. Clean up and shut down.
  71. """
  72. self.stop()
  73. def start(self):
  74. """
  75. Starts the detection thread, if not already running.
  76. """
  77. if not self._detect_thread.is_alive():
  78. self._detect_thread.start()
  79. def stop(self):
  80. """
  81. Stops the detection thread.
  82. """
  83. self._detect_thread.stop()
  84. def get_device(self, device=None):
  85. """
  86. Factory method that returns the requested AD2USB device, or the first device.
  87. :param device: Tuple describing the USB device to open, as returned by find_all().
  88. :type device: tuple
  89. """
  90. return AD2Factory.create(device)
  91. class DetectThread(threading.Thread):
  92. """
  93. Thread that handles detection of added/removed devices.
  94. """
  95. def __init__(self, factory):
  96. """
  97. Constructor
  98. :param factory: AD2Factory object to use with the thread.
  99. :type factory: AD2Factory
  100. """
  101. threading.Thread.__init__(self)
  102. self._factory = factory
  103. self._running = False
  104. def stop(self):
  105. """
  106. Stops the thread.
  107. """
  108. self._running = False
  109. def run(self):
  110. """
  111. The actual detection process.
  112. """
  113. self._running = True
  114. last_devices = set()
  115. while self._running:
  116. try:
  117. AD2Factory.find_all()
  118. current_devices = set(AD2Factory.devices())
  119. new_devices = [d for d in current_devices if d not in last_devices]
  120. removed_devices = [d for d in last_devices if d not in current_devices]
  121. last_devices = current_devices
  122. for d in new_devices:
  123. self._factory.on_attached(d)
  124. for d in removed_devices:
  125. self._factory.on_detached(d)
  126. except CommError, err:
  127. pass
  128. time.sleep(0.25)
  129. class AD2(object):
  130. """
  131. High-level wrapper around AD2 devices.
  132. """
  133. # High-level Events
  134. on_arm = event.Event('Called when the panel is armed.')
  135. on_disarm = event.Event('Called when the panel is disarmed.')
  136. on_power_changed = event.Event('Called when panel power switches between AC and DC.')
  137. on_alarm = event.Event('Called when the alarm is triggered.')
  138. on_fire = event.Event('Called when a fire is detected.')
  139. on_bypass = event.Event('Called when a zone is bypassed.')
  140. on_boot = event.Event('Called when the device finishes bootings.')
  141. on_config_received = event.Event('Called when the device receives its configuration.')
  142. on_zone_fault = event.Event('Called when the device detects a zone fault.')
  143. on_zone_restore = event.Event('Called when the device detects that a fault is restored.')
  144. on_low_battery = event.Event('Called when the device detects a low battery.')
  145. on_panic = event.Event('Called when the device detects a panic.')
  146. on_relay_changed = event.Event('Called when a relay is opened or closed on an expander board.')
  147. # Mid-level Events
  148. on_message = event.Event('Called when a message has been received from the device.')
  149. on_lrr_message = event.Event('Called when an LRR message is received.')
  150. on_rfx_message = event.Event('Called when an RFX message is received.')
  151. # Low-level Events
  152. on_open = event.Event('Called when the device has been opened.')
  153. on_close = event.Event('Called when the device has been closed.')
  154. on_read = event.Event('Called when a line has been read from the device.')
  155. on_write = event.Event('Called when data has been written to the device.')
  156. # Constants
  157. F1 = unichr(1) + unichr(1) + unichr(1)
  158. """Represents panel function key #1"""
  159. F2 = unichr(2) + unichr(2) + unichr(2)
  160. """Represents panel function key #2"""
  161. F3 = unichr(3) + unichr(3) + unichr(3)
  162. """Represents panel function key #3"""
  163. F4 = unichr(4) + unichr(4) + unichr(4)
  164. """Represents panel function key #4"""
  165. BATTERY_TIMEOUT = 30
  166. """Timeout before the battery status reverts."""
  167. FIRE_TIMEOUT = 30
  168. """Timeout before the fire status reverts."""
  169. def __init__(self, device):
  170. """
  171. Constructor
  172. :param device: The low-level device used for this AD2 interface.
  173. :type device: Device
  174. """
  175. self._device = device
  176. self._zonetracker = Zonetracker()
  177. self._power_status = None
  178. self._alarm_status = None
  179. self._bypass_status = None
  180. self._armed_status = None
  181. self._fire_status = (False, 0)
  182. self._battery_status = (False, 0)
  183. self._panic_status = None
  184. self._relay_status = {}
  185. self.address = 18
  186. self.configbits = 0xFF00
  187. self.address_mask = 0x00000000
  188. self.emulate_zone = [False for x in range(5)]
  189. self.emulate_relay = [False for x in range(4)]
  190. self.emulate_lrr = False
  191. self.deduplicate = False
  192. @property
  193. def id(self):
  194. """
  195. The ID of the AD2 device.
  196. :returns: The identification string for the device.
  197. """
  198. return self._device.id
  199. def open(self, baudrate=None, no_reader_thread=False):
  200. """
  201. Opens the device.
  202. :param baudrate: The baudrate used for the device.
  203. :type baudrate: int
  204. :param interface: The interface used for the device.
  205. :type interface: varies depends on device type.. FIXME
  206. :param index: Interface index.. can probably remove. FIXME
  207. :type index: int
  208. :param no_reader_thread: Specifies whether or not the automatic reader thread should be started or not
  209. :type no_reader_thread: bool
  210. """
  211. self._wire_events()
  212. self._device.open(baudrate=baudrate, no_reader_thread=no_reader_thread)
  213. def close(self):
  214. """
  215. Closes the device.
  216. """
  217. if self._device:
  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.send("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.send("C{0}\r".format(config_string))
  252. def reboot(self):
  253. """
  254. Reboots the device.
  255. """
  256. self.send('=')
  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.send("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.send("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. raise InvalidMessageError()
  299. msg = None
  300. header = data[0:4]
  301. if header[0] != '!' or header == '!KPE':
  302. msg = 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 = 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 = 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 = LRRMessage(data)
  329. if msg.event_type == 'ALARM_PANIC':
  330. self._panic_status = True
  331. self.on_panic(True)
  332. elif msg.event_type == 'CANCEL':
  333. if self._panic_status == True:
  334. self._panic_status = False
  335. self.on_panic(False)
  336. self.on_lrr_message(msg)
  337. return msg
  338. def _handle_config(self, data):
  339. """
  340. Handles received configuration data.
  341. :param data: Configuration string to parse.
  342. :type data: str
  343. """
  344. _, config_string = data.split('>')
  345. for setting in config_string.split('&'):
  346. k, v = setting.split('=')
  347. if k == 'ADDRESS':
  348. self.address = int(v)
  349. elif k == 'CONFIGBITS':
  350. self.configbits = int(v, 16)
  351. elif k == 'MASK':
  352. self.address_mask = int(v, 16)
  353. elif k == 'EXP':
  354. for z in range(5):
  355. self.emulate_zone[z] = (v[z] == 'Y')
  356. elif k == 'REL':
  357. for r in range(4):
  358. self.emulate_relay[r] = (v[r] == 'Y')
  359. elif k == 'LRR':
  360. self.emulate_lrr = (v == 'Y')
  361. elif k == 'DEDUPLICATE':
  362. self.deduplicate = (v == 'Y')
  363. self.on_config_received()
  364. def _update_internal_states(self, message):
  365. """
  366. Updates internal device states.
  367. :param message: Message to update internal states with.
  368. :type message: Message, ExpanderMessage, LRRMessage, or RFMessage
  369. """
  370. if isinstance(message, Message):
  371. if message.ac_power != self._power_status:
  372. self._power_status, old_status = message.ac_power, self._power_status
  373. if old_status is not None:
  374. self.on_power_changed(self._power_status)
  375. if message.alarm_sounding != self._alarm_status:
  376. self._alarm_status, old_status = message.alarm_sounding, self._alarm_status
  377. if old_status is not None:
  378. self.on_alarm(self._alarm_status)
  379. if message.zone_bypassed != self._bypass_status:
  380. self._bypass_status, old_status = message.zone_bypassed, self._bypass_status
  381. if old_status is not None:
  382. self.on_bypass(self._bypass_status)
  383. if (message.armed_away | message.armed_home) != self._armed_status:
  384. self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status
  385. if old_status is not None:
  386. if self._armed_status:
  387. self.on_arm()
  388. else:
  389. self.on_disarm()
  390. if message.battery_low == self._battery_status[0]:
  391. self._battery_status = (self._battery_status[0], time.time())
  392. else:
  393. if message.battery_low == True or time.time() > self._battery_status[1] + AD2.BATTERY_TIMEOUT:
  394. self._battery_status = (message.battery_low, time.time())
  395. self.on_low_battery(self._battery_status)
  396. if message.fire_alarm == self._fire_status[0]:
  397. self._fire_status = (self._fire_status[0], time.time())
  398. else:
  399. if message.fire_alarm == True or time.time() > self._fire_status[1] + AD2.FIRE_TIMEOUT:
  400. self._fire_status = (message.fire_alarm, time.time())
  401. self.on_fire(self._fire_status)
  402. elif isinstance(message, ExpanderMessage):
  403. if message.type == ExpanderMessage.RELAY:
  404. self._relay_status[(message.address, message.channel)] = message.value
  405. self.on_relay_changed(message)
  406. self._update_zone_tracker(message)
  407. def _update_zone_tracker(self, message):
  408. """
  409. Trigger an update of the zonetracker.
  410. :param message: The message to update the zonetracker with.
  411. :type message: Message, ExpanderMessage, LRRMessage, or RFMessage
  412. """
  413. # Retrieve a list of faults.
  414. # NOTE: This only happens on first boot or after exiting programming mode.
  415. if isinstance(message, Message):
  416. if not message.ready and "Hit * for faults" in message.text:
  417. self.send('*')
  418. return
  419. self._zonetracker.update(message)
  420. def _on_open(self, sender, args):
  421. """
  422. Internal handler for opening the device.
  423. """
  424. self.on_open(args)
  425. def _on_close(self, sender, args):
  426. """
  427. Internal handler for closing the device.
  428. """
  429. self.on_close(args)
  430. def _on_read(self, sender, args):
  431. """
  432. Internal handler for reading from the device.
  433. """
  434. self.on_read(args)
  435. msg = self._handle_message(args)
  436. if msg:
  437. self.on_message(msg)
  438. def _on_write(self, sender, args):
  439. """
  440. Internal handler for writing to the device.
  441. """
  442. self.on_write(args)
  443. def _on_zone_fault(self, sender, args):
  444. """
  445. Internal handler for zone faults.
  446. """
  447. self.on_zone_fault(args)
  448. def _on_zone_restore(self, sender, args):
  449. """
  450. Internal handler for zone restoration.
  451. """
  452. self.on_zone_restore(args)