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.

264 lines
7.2 KiB

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