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.

461 lines
15 KiB

  1. """
  2. Provides the full AlarmDecoder class.
  3. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  4. """
  5. import time
  6. from .event import event
  7. from .util import InvalidMessageError
  8. from .messages import Message, ExpanderMessage, RFMessage, LRRMessage
  9. from .zonetracking import Zonetracker
  10. class AlarmDecoder(object):
  11. """
  12. High-level wrapper around Alarm Decoder (AD2) devices.
  13. """
  14. # High-level Events
  15. on_arm = event.Event('Called when the panel is armed.')
  16. on_disarm = event.Event('Called when the panel is disarmed.')
  17. on_power_changed = event.Event('Called when panel power switches between AC and DC.')
  18. on_alarm = event.Event('Called when the alarm is triggered.')
  19. on_fire = event.Event('Called when a fire is detected.')
  20. on_bypass = event.Event('Called when a zone is bypassed.')
  21. on_boot = event.Event('Called when the device finishes bootings.')
  22. on_config_received = event.Event('Called when the device receives its configuration.')
  23. on_zone_fault = event.Event('Called when the device detects a zone fault.')
  24. on_zone_restore = event.Event('Called when the device detects that a fault is restored.')
  25. on_low_battery = event.Event('Called when the device detects a low battery.')
  26. on_panic = event.Event('Called when the device detects a panic.')
  27. on_relay_changed = event.Event('Called when a relay is opened or closed on an expander board.')
  28. # Mid-level Events
  29. on_message = event.Event('Called when a message has been received from the device.')
  30. on_lrr_message = event.Event('Called when an LRR message is received.')
  31. on_rfx_message = event.Event('Called when an RFX message is received.')
  32. # Low-level Events
  33. on_open = event.Event('Called when the device has been opened.')
  34. on_close = event.Event('Called when the device has been closed.')
  35. on_read = event.Event('Called when a line has been read from the device.')
  36. on_write = event.Event('Called when data has been written to the device.')
  37. # Constants
  38. KEY_F1 = unichr(1) + unichr(1) + unichr(1)
  39. """Represents panel function key #1"""
  40. KEY_F2 = unichr(2) + unichr(2) + unichr(2)
  41. """Represents panel function key #2"""
  42. KEY_F3 = unichr(3) + unichr(3) + unichr(3)
  43. """Represents panel function key #3"""
  44. KEY_F4 = unichr(4) + unichr(4) + unichr(4)
  45. """Represents panel function key #4"""
  46. BATTERY_TIMEOUT = 30
  47. """Timeout before the battery status reverts."""
  48. FIRE_TIMEOUT = 30
  49. """Timeout before the fire status reverts."""
  50. def __init__(self, device):
  51. """
  52. Constructor
  53. :param device: The low-level device used for this Alarm Decoder
  54. interface.
  55. :type device: Device
  56. """
  57. self._device = device
  58. self._zonetracker = Zonetracker()
  59. self._power_status = None
  60. self._alarm_status = None
  61. self._bypass_status = None
  62. self._armed_status = None
  63. self._fire_status = (False, 0)
  64. self._battery_status = (False, 0)
  65. self._panic_status = None
  66. self._relay_status = {}
  67. self.address = 18
  68. self.configbits = 0xFF00
  69. self.address_mask = 0x00000000
  70. self.emulate_zone = [False for x in range(5)]
  71. self.emulate_relay = [False for x in range(4)]
  72. self.emulate_lrr = False
  73. self.deduplicate = False
  74. def __enter__(self):
  75. """
  76. Support for context manager __enter__.
  77. """
  78. return self
  79. def __exit__(self, exc_type, exc_value, traceback):
  80. """
  81. Support for context manager __exit__.
  82. """
  83. self.close()
  84. return False
  85. @property
  86. def id(self):
  87. """
  88. The ID of the Alarm Decoder device.
  89. :returns: The identification string for the device.
  90. """
  91. return self._device.id
  92. def open(self, baudrate=None, no_reader_thread=False):
  93. """
  94. Opens the device.
  95. :param baudrate: The baudrate used for the device.
  96. :type baudrate: int
  97. :param no_reader_thread: Specifies whether or not the automatic reader
  98. thread should be started or not
  99. :type no_reader_thread: bool
  100. """
  101. self._wire_events()
  102. self._device.open(baudrate=baudrate, no_reader_thread=no_reader_thread)
  103. return self
  104. def close(self):
  105. """
  106. Closes the device.
  107. """
  108. if self._device:
  109. self._device.close()
  110. del self._device
  111. self._device = None
  112. def send(self, data):
  113. """
  114. Sends data to the Alarm Decoder device.
  115. :param data: The data to send.
  116. :type data: str
  117. """
  118. if self._device:
  119. self._device.write(data)
  120. def get_config(self):
  121. """
  122. Retrieves the configuration from the device.
  123. """
  124. self.send("C\r")
  125. def save_config(self):
  126. """
  127. Sets configuration entries on the device.
  128. """
  129. config_string = ''
  130. # HACK: Both of these methods are ugly.. but I can't think of an
  131. # elegant way of doing it.
  132. #config_string += 'ADDRESS={0}&'.format(self.address)
  133. #config_string += 'CONFIGBITS={0:x}&'.format(self.configbits)
  134. #config_string += 'MASK={0:x}&'.format(self.address_mask)
  135. #config_string += 'EXP={0}&'.format(''.join(['Y' if z else 'N' for z in self.emulate_zone]))
  136. #config_string += 'REL={0}&'.format(''.join(['Y' if r else 'N' for r in self.emulate_relay]))
  137. #config_string += 'LRR={0}&'.format('Y' if self.emulate_lrr else 'N')
  138. #config_string += 'DEDUPLICATE={0}'.format('Y' if self.deduplicate else 'N')
  139. config_entries = []
  140. config_entries.append(('ADDRESS',
  141. '{0}'.format(self.address)))
  142. config_entries.append(('CONFIGBITS',
  143. '{0:x}'.format(self.configbits)))
  144. config_entries.append(('MASK',
  145. '{0:x}'.format(self.address_mask)))
  146. config_entries.append(('EXP',
  147. ''.join(['Y' if z else 'N' for z in self.emulate_zone])))
  148. config_entries.append(('REL',
  149. ''.join(['Y' if r else 'N' for r in self.emulate_relay])))
  150. config_entries.append(('LRR',
  151. 'Y' if self.emulate_lrr else 'N'))
  152. config_entries.append(('DEDUPLICATE',
  153. 'Y' if self.deduplicate else 'N'))
  154. config_string = '&'.join(['='.join(t) for t in config_entries])
  155. self.send("C{0}\r".format(config_string))
  156. def reboot(self):
  157. """
  158. Reboots the device.
  159. """
  160. self.send('=')
  161. def fault_zone(self, zone, simulate_wire_problem=False):
  162. """
  163. Faults a zone if we are emulating a zone expander.
  164. :param zone: The zone to fault.
  165. :type zone: int
  166. :param simulate_wire_problem: Whether or not to simulate a wire fault.
  167. :type simulate_wire_problem: bool
  168. """
  169. # Allow ourselves to also be passed an address/channel combination
  170. # for zone expanders.
  171. #
  172. # Format (expander index, channel)
  173. if isinstance(zone, tuple):
  174. expander_idx, channel = zone
  175. zone = self._zonetracker.expander_to_zone(expander_idx, channel)
  176. status = 2 if simulate_wire_problem else 1
  177. self.send("L{0:02}{1}\r".format(zone, status))
  178. def clear_zone(self, zone):
  179. """
  180. Clears a zone if we are emulating a zone expander.
  181. :param zone: The zone to clear.
  182. :type zone: int
  183. """
  184. self.send("L{0:02}0\r".format(zone))
  185. def _wire_events(self):
  186. """
  187. Wires up the internal device events.
  188. """
  189. self._device.on_open += self._on_open
  190. self._device.on_close += self._on_close
  191. self._device.on_read += self._on_read
  192. self._device.on_write += self._on_write
  193. self._zonetracker.on_fault += self._on_zone_fault
  194. self._zonetracker.on_restore += self._on_zone_restore
  195. def _handle_message(self, data):
  196. """
  197. Parses messages from the panel.
  198. :param data: Panel data to parse.
  199. :type data: str
  200. :returns: An object representing the message.
  201. """
  202. if data is None:
  203. raise InvalidMessageError()
  204. msg = None
  205. header = data[0:4]
  206. if header[0] != '!' or header == '!KPE':
  207. msg = Message(data)
  208. if self.address_mask & msg.mask > 0:
  209. self._update_internal_states(msg)
  210. elif header == '!EXP' or header == '!REL':
  211. msg = ExpanderMessage(data)
  212. self._update_internal_states(msg)
  213. elif header == '!RFX':
  214. msg = self._handle_rfx(data)
  215. elif header == '!LRR':
  216. msg = self._handle_lrr(data)
  217. elif data.startswith('!Ready'):
  218. self.on_boot()
  219. elif data.startswith('!CONFIG'):
  220. self._handle_config(data)
  221. return msg
  222. def _handle_rfx(self, data):
  223. """
  224. Handle RF messages.
  225. :param data: RF message to parse.
  226. :type data: str
  227. :returns: An object representing the RF message.
  228. """
  229. msg = RFMessage(data)
  230. self.on_rfx_message(message=msg)
  231. return msg
  232. def _handle_lrr(self, data):
  233. """
  234. Handle Long Range Radio messages.
  235. :param data: LRR message to parse.
  236. :type data: str
  237. :returns: An object representing the LRR message.
  238. """
  239. msg = LRRMessage(data)
  240. if msg.event_type == 'ALARM_PANIC':
  241. self._panic_status = True
  242. self.on_panic(status=True)
  243. elif msg.event_type == 'CANCEL':
  244. if self._panic_status is True:
  245. self._panic_status = False
  246. self.on_panic(status=False)
  247. self.on_lrr_message(message=msg)
  248. return msg
  249. def _handle_config(self, data):
  250. """
  251. Handles received configuration data.
  252. :param data: Configuration string to parse.
  253. :type data: str
  254. """
  255. _, config_string = data.split('>')
  256. for setting in config_string.split('&'):
  257. key, val = setting.split('=')
  258. if key == 'ADDRESS':
  259. self.address = int(val)
  260. elif key == 'CONFIGBITS':
  261. self.configbits = int(val, 16)
  262. elif key == 'MASK':
  263. self.address_mask = int(val, 16)
  264. elif key == 'EXP':
  265. self.emulate_zone = [val[z] == 'Y' for z in range(5)]
  266. elif key == 'REL':
  267. self.emulate_relay = [val[r] == 'Y' for r in range(4)]
  268. elif key == 'LRR':
  269. self.emulate_lrr = (val == 'Y')
  270. elif key == 'DEDUPLICATE':
  271. self.deduplicate = (val == 'Y')
  272. self.on_config_received()
  273. def _update_internal_states(self, message):
  274. """
  275. Updates internal device states.
  276. :param message: Message to update internal states with.
  277. :type message: Message, ExpanderMessage, LRRMessage, or RFMessage
  278. """
  279. if isinstance(message, Message):
  280. if message.ac_power != self._power_status:
  281. self._power_status, old_status = message.ac_power, self._power_status
  282. if old_status is not None:
  283. self.on_power_changed(status=self._power_status)
  284. if message.alarm_sounding != self._alarm_status:
  285. self._alarm_status, old_status = message.alarm_sounding, self._alarm_status
  286. if old_status is not None:
  287. self.on_alarm(status=self._alarm_status)
  288. if message.zone_bypassed != self._bypass_status:
  289. self._bypass_status, old_status = message.zone_bypassed, self._bypass_status
  290. if old_status is not None:
  291. self.on_bypass(status=self._bypass_status)
  292. if (message.armed_away | message.armed_home) != self._armed_status:
  293. self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status
  294. if old_status is not None:
  295. if self._armed_status:
  296. self.on_arm()
  297. else:
  298. self.on_disarm()
  299. if message.battery_low == self._battery_status[0]:
  300. self._battery_status = (self._battery_status[0], time.time())
  301. else:
  302. if message.battery_low is True or time.time() > self._battery_status[1] + AlarmDecoder.BATTERY_TIMEOUT:
  303. self._battery_status = (message.battery_low, time.time())
  304. self.on_low_battery(status=self._battery_status)
  305. if message.fire_alarm == self._fire_status[0]:
  306. self._fire_status = (self._fire_status[0], time.time())
  307. else:
  308. if message.fire_alarm is True or time.time() > self._fire_status[1] + AlarmDecoder.FIRE_TIMEOUT:
  309. self._fire_status = (message.fire_alarm, time.time())
  310. self.on_fire(status=self._fire_status)
  311. elif isinstance(message, ExpanderMessage):
  312. if message.type == ExpanderMessage.RELAY:
  313. self._relay_status[(message.address, message.channel)] = message.value
  314. self.on_relay_changed(message=message)
  315. self._update_zone_tracker(message)
  316. def _update_zone_tracker(self, message):
  317. """
  318. Trigger an update of the zonetracker.
  319. :param message: The message to update the zonetracker with.
  320. :type message: Message, ExpanderMessage, LRRMessage, or RFMessage
  321. """
  322. # Retrieve a list of faults.
  323. # NOTE: This only happens on first boot or after exiting programming mode.
  324. if isinstance(message, Message):
  325. if not message.ready and "Hit * for faults" in message.text:
  326. self.send('*')
  327. return
  328. self._zonetracker.update(message)
  329. def _on_open(self, sender, *args, **kwargs):
  330. """
  331. Internal handler for opening the device.
  332. """
  333. self.get_config()
  334. self.on_open(args, kwargs)
  335. def _on_close(self, sender, *args, **kwargs):
  336. """
  337. Internal handler for closing the device.
  338. """
  339. self.on_close(args, kwargs)
  340. def _on_read(self, sender, *args, **kwargs):
  341. """
  342. Internal handler for reading from the device.
  343. """
  344. self.on_read(args, kwargs)
  345. msg = self._handle_message(kwargs['data'])
  346. if msg:
  347. self.on_message(message=msg)
  348. def _on_write(self, sender, *args, **kwargs):
  349. """
  350. Internal handler for writing to the device.
  351. """
  352. self.on_write(args, kwargs)
  353. def _on_zone_fault(self, sender, *args, **kwargs):
  354. """
  355. Internal handler for zone faults.
  356. """
  357. self.on_zone_fault(*args, **kwargs)
  358. def _on_zone_restore(self, sender, *args, **kwargs):
  359. """
  360. Internal handler for zone restoration.
  361. """
  362. self.on_zone_restore(*args, **kwargs)