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.

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