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.

250 lines
6.7 KiB

  1. """
  2. Message representations received from the panel through the AD2USB.
  3. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  4. """
  5. import re
  6. class BaseMessage(object):
  7. """
  8. Base class for messages.
  9. """
  10. def __init__(self):
  11. self.raw = None
  12. class Message(BaseMessage):
  13. """
  14. Represents a message from the alarm panel.
  15. """
  16. def __init__(self, data=None):
  17. """
  18. Constructor
  19. :param data: Message data to parse.
  20. :type data: str
  21. """
  22. self.ready = False
  23. self.armed_away = False
  24. self.armed_home = False
  25. self.backlight_on = False
  26. self.programming_mode = False
  27. self.beeps = -1
  28. self.zone_bypassed = False
  29. self.ac_power = False
  30. self.chime_on = False
  31. self.alarm_event_occurred = False
  32. self.alarm_sounding = False
  33. self.battery_low = False
  34. self.entry_delay_off = False
  35. self.fire_alarm = False
  36. self.check_zone = False
  37. self.perimeter_only = False
  38. self.numeric_code = ""
  39. self.text = ""
  40. self.cursor_location = -1
  41. self.data = ""
  42. self.mask = ""
  43. self.bitfield = ""
  44. self.panel_data = ""
  45. self._regex = re.compile('("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*)')
  46. if data is not None:
  47. self._parse_message(data)
  48. def _parse_message(self, data):
  49. """
  50. Parse the message from the device.
  51. :param data: The message data.
  52. :type data: str
  53. :raises: util.InvalidMessageError
  54. """
  55. m = self._regex.match(data)
  56. if m is None:
  57. raise util.InvalidMessageError('Received invalid message: {0}'.format(data))
  58. self.bitfield, self.numeric_code, self.panel_data, alpha = m.group(1, 2, 3, 4)
  59. self.mask = int(self.panel_data[3:3+8], 16)
  60. is_bit_set = lambda bit: not self.bitfield[bit] == "0"
  61. self.raw = data
  62. self.ready = is_bit_set(1)
  63. self.armed_away = is_bit_set(2)
  64. self.armed_home = is_bit_set(3)
  65. self.backlight_on = is_bit_set(4)
  66. self.programming_mode = is_bit_set(5)
  67. self.beeps = int(self.bitfield[6], 16)
  68. self.zone_bypassed = is_bit_set(7)
  69. self.ac_power = is_bit_set(8)
  70. self.chime_on = is_bit_set(9)
  71. self.alarm_event_occurred = is_bit_set(10)
  72. self.alarm_sounding = is_bit_set(11)
  73. self.battery_low = is_bit_set(12)
  74. self.entry_delay_off = is_bit_set(13)
  75. self.fire_alarm = is_bit_set(14)
  76. self.check_zone = is_bit_set(15)
  77. self.perimeter_only = is_bit_set(16)
  78. # bits 17-20 unused.
  79. self.text = alpha.strip('"')
  80. if int(self.panel_data[19:21], 16) & 0x01 > 0:
  81. self.cursor_location = int(self.bitfield[21:23], 16) # Alpha character index that the cursor is on.
  82. def __str__(self):
  83. """
  84. String conversion operator.
  85. """
  86. 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)
  87. class ExpanderMessage(BaseMessage):
  88. """
  89. Represents a message from a zone or relay expansion module.
  90. """
  91. ZONE = 0
  92. RELAY = 1
  93. def __init__(self, data=None):
  94. """
  95. Constructor
  96. :param data: The message data to parse.
  97. :type data: str
  98. """
  99. self.type = None
  100. self.address = None
  101. self.channel = None
  102. self.value = None
  103. self.raw = None
  104. if data is not None:
  105. self._parse_message(data)
  106. def __str__(self):
  107. """
  108. String conversion operator.
  109. """
  110. expander_type = 'UNKWN'
  111. if self.type == ExpanderMessage.ZONE:
  112. expander_type = 'ZONE'
  113. elif self.type == ExpanderMessage.RELAY:
  114. expander_type = 'RELAY'
  115. return 'exp > [{0: <5}] {1}/{2} -- {3}'.format(expander_type, self.address, self.channel, self.value)
  116. def _parse_message(self, data):
  117. """
  118. Parse the raw message from the device.
  119. :param data: The message data
  120. :type data: str
  121. """
  122. header, values = data.split(':')
  123. address, channel, value = values.split(',')
  124. self.raw = data
  125. self.address = int(address)
  126. self.channel = int(channel)
  127. self.value = int(value)
  128. if header == '!EXP':
  129. self.type = ExpanderMessage.ZONE
  130. elif header == '!REL':
  131. self.type = ExpanderMessage.RELAY
  132. class RFMessage(BaseMessage):
  133. """
  134. Represents a message from an RF receiver.
  135. """
  136. def __init__(self, data=None):
  137. """
  138. Constructor
  139. :param data: The message data to parse
  140. :type data: str
  141. """
  142. self.raw = None
  143. self.serial_number = None
  144. self.value = None
  145. self.battery = None
  146. self.supervision = None
  147. self.loop = {}
  148. if data is not None:
  149. self._parse_message(data)
  150. def __str__(self):
  151. """
  152. String conversion operator.
  153. """
  154. return 'rf > {0}: {1:x}'.format(self.serial_number, self.value)
  155. def _parse_message(self, data):
  156. """
  157. Parses the raw message from the device.
  158. :param data: The message data.
  159. :type data: str
  160. """
  161. self.raw = data
  162. _, values = data.split(':')
  163. self.serial_number, self.value = values.split(',')
  164. self.value = int(self.value, 16)
  165. is_bit_set = lambda b: self.value & (1 << b) > 0
  166. # Bit 1 = unknown
  167. self.battery = is_bit_set(2)
  168. self.supervision = is_bit_set(3)
  169. # Bit 8 = unknown
  170. self.loop[0] = is_bit_set(5)
  171. self.loop[1] = is_bit_set(6)
  172. self.loop[2] = is_bit_set(7)
  173. self.loop[3] = is_bit_set(8)
  174. class LRRMessage(BaseMessage):
  175. """
  176. Represent a message from a Long Range Radio.
  177. """
  178. def __init__(self, data=None):
  179. """
  180. Constructor
  181. :param data: The message data to parse.
  182. :type data: str
  183. """
  184. self.raw = None
  185. self.event_data = None
  186. self.partition = None
  187. self.event_type = None
  188. if data is not None:
  189. self._parse_message(data)
  190. def __str__(self):
  191. """
  192. String conversion operator.
  193. """
  194. return 'lrr > {0} @ {1} -- {2}'.format(self.event_data, self.partition, self.event_type)
  195. def _parse_message(self, data):
  196. """
  197. Parses the raw message from the device.
  198. :param data: The message data.
  199. :type data: str
  200. """
  201. self.raw = data
  202. _, values = data.split(':')
  203. self.event_data, self.partition, self.event_type = values.split(',')