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.

451 lines
15 KiB

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