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.

314 lines
8.7 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. raw = None
  12. """The raw message text"""
  13. def __init__(self):
  14. """
  15. Constructor
  16. """
  17. pass
  18. def __str__(self):
  19. """
  20. String conversion operator.
  21. """
  22. return self.raw
  23. class Message(BaseMessage):
  24. """
  25. Represents a message from the alarm panel.
  26. """
  27. ready = False
  28. """Indicates whether or not the panel is in a ready state"""
  29. armed_away = False
  30. """Indicates whether or not the panel is armed away"""
  31. armed_home = False
  32. """Indicates whether or not the panel is armed home"""
  33. backlight_on = False
  34. """Indicates whether or not the keypad backlight is on"""
  35. programming_mode = False
  36. """Indicates whether or not we're in programming mode"""
  37. beeps = -1
  38. """Number of beeps associated with a message"""
  39. zone_bypassed = False
  40. """Indicates whether or not a zone is bypassed"""
  41. ac_power = False
  42. """Indicates whether or not the panel is on AC power"""
  43. chime_on = False
  44. """Indicates whether or not the chime is enabled"""
  45. alarm_event_occurred = False
  46. """Indicates whether or not an alarm event has occurred"""
  47. alarm_sounding = False
  48. """Indicates whether or not an alarm is sounding"""
  49. battery_low = False
  50. """Indicates whether or not there is a low battery"""
  51. entry_delay_off = False
  52. """Indicates whether or not the entry delay is enabled"""
  53. fire_alarm = False
  54. """Indicates whether or not a fire alarm is sounding"""
  55. check_zone = False
  56. """Indicates whether or not there are zones that require attention."""
  57. perimeter_only = False
  58. """Indicates whether or not the perimeter is armed"""
  59. numeric_code = None
  60. """The numeric code associated with the message"""
  61. text = None
  62. """The human-readable text to be displayed on the panel LCD"""
  63. cursor_location = -1
  64. """Current cursor location on the keypad"""
  65. mask = None
  66. """Address mask this message is intended for"""
  67. bitfield = None
  68. """The bitfield associated with this message"""
  69. panel_data = None
  70. """The panel data field associated with this message"""
  71. def __init__(self, data=None):
  72. """
  73. Constructor
  74. :param data: Message data to parse.
  75. :type data: str
  76. """
  77. BaseMessage.__init__(self)
  78. self._regex = re.compile('("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*)')
  79. if data is not None:
  80. self._parse_message(data)
  81. def __str__(self):
  82. """
  83. String conversion operator.
  84. """
  85. return self.raw
  86. def _parse_message(self, data):
  87. """
  88. Parse the message from the device.
  89. :param data: The message data.
  90. :type data: str
  91. :raises: InvalidMessageError
  92. """
  93. m = self._regex.match(data)
  94. if m is None:
  95. raise InvalidMessageError('Received invalid message: {0}'.format(data))
  96. self.bitfield, self.numeric_code, self.panel_data, alpha = m.group(1, 2, 3, 4)
  97. self.mask = int(self.panel_data[3:3+8], 16)
  98. is_bit_set = lambda bit: not self.bitfield[bit] == "0"
  99. self.raw = data
  100. self.ready = is_bit_set(1)
  101. self.armed_away = is_bit_set(2)
  102. self.armed_home = is_bit_set(3)
  103. self.backlight_on = is_bit_set(4)
  104. self.programming_mode = is_bit_set(5)
  105. self.beeps = int(self.bitfield[6], 16)
  106. self.zone_bypassed = is_bit_set(7)
  107. self.ac_power = is_bit_set(8)
  108. self.chime_on = is_bit_set(9)
  109. self.alarm_event_occurred = is_bit_set(10)
  110. self.alarm_sounding = is_bit_set(11)
  111. self.battery_low = is_bit_set(12)
  112. self.entry_delay_off = is_bit_set(13)
  113. self.fire_alarm = is_bit_set(14)
  114. self.check_zone = is_bit_set(15)
  115. self.perimeter_only = is_bit_set(16)
  116. # bits 17-20 unused.
  117. self.text = alpha.strip('"')
  118. if int(self.panel_data[19:21], 16) & 0x01 > 0:
  119. self.cursor_location = int(self.bitfield[21:23], 16) # Alpha character index that the cursor is on.
  120. class ExpanderMessage(BaseMessage):
  121. """
  122. Represents a message from a zone or relay expansion module.
  123. """
  124. ZONE = 0
  125. """Flag indicating that the expander message relates to a Zone Expander."""
  126. RELAY = 1
  127. """Flag indicating that the expander message relates to a Relay Expander."""
  128. type = None
  129. """Expander message type: ExpanderMessage.ZONE or ExpanderMessage.RELAY"""
  130. address = -1
  131. """Address of expander"""
  132. channel = -1
  133. """Channel on the expander"""
  134. value = -1
  135. """Value associated with the message"""
  136. def __init__(self, data=None):
  137. """
  138. Constructor
  139. :param data: The message data to parse.
  140. :type data: str
  141. """
  142. BaseMessage.__init__(self)
  143. if data is not None:
  144. self._parse_message(data)
  145. def __str__(self):
  146. """
  147. String conversion operator.
  148. """
  149. return self.raw
  150. def _parse_message(self, data):
  151. """
  152. Parse the raw message from the device.
  153. :param data: The message data
  154. :type data: str
  155. """
  156. try:
  157. header, values = data.split(':')
  158. address, channel, value = values.split(',')
  159. self.raw = data
  160. self.address = int(address)
  161. self.channel = int(channel)
  162. self.value = int(value)
  163. except ValueError:
  164. raise InvalidMessageError('Received invalid message: {0}'.format(data))
  165. if header == '!EXP':
  166. self.type = ExpanderMessage.ZONE
  167. elif header == '!REL':
  168. self.type = ExpanderMessage.RELAY
  169. else:
  170. raise InvalidMessageError('Unknown expander message header: {0}'.format(data))
  171. class RFMessage(BaseMessage):
  172. """
  173. Represents a message from an RF receiver.
  174. """
  175. serial_number = None
  176. """Serial number of the RF device"""
  177. value = -1
  178. """Value associated with this message"""
  179. battery = False
  180. """Battery low indication"""
  181. supervision = False
  182. """Supervision required indication"""
  183. loop = [False for x in range(4)]
  184. """Loop indicators"""
  185. def __init__(self, data=None):
  186. """
  187. Constructor
  188. :param data: The message data to parse
  189. :type data: str
  190. """
  191. BaseMessage.__init__(self)
  192. if data is not None:
  193. self._parse_message(data)
  194. def __str__(self):
  195. """
  196. String conversion operator.
  197. """
  198. return self.raw
  199. def _parse_message(self, data):
  200. """
  201. Parses the raw message from the device.
  202. :param data: The message data.
  203. :type data: str
  204. """
  205. try:
  206. self.raw = data
  207. _, values = data.split(':')
  208. self.serial_number, self.value = values.split(',')
  209. self.value = int(self.value, 16)
  210. is_bit_set = lambda b: self.value & (1 << (b - 1)) > 0
  211. # Bit 1 = unknown
  212. self.battery = is_bit_set(2)
  213. self.supervision = is_bit_set(3)
  214. # Bit 4 = unknown
  215. self.loop[2] = is_bit_set(5)
  216. self.loop[1] = is_bit_set(6)
  217. self.loop[3] = is_bit_set(7)
  218. self.loop[0] = is_bit_set(8)
  219. except ValueError:
  220. raise InvalidMessageError('Received invalid message: {0}'.format(data))
  221. class LRRMessage(BaseMessage):
  222. """
  223. Represent a message from a Long Range Radio.
  224. """
  225. event_data = None
  226. """Data associated with the LRR message. Usually user ID or zone."""
  227. partition = -1
  228. """The partition that this message applies to"""
  229. event_type = None
  230. """The type of the event that occurred"""
  231. def __init__(self, data=None):
  232. """
  233. Constructor
  234. :param data: The message data to parse.
  235. :type data: str
  236. """
  237. BaseMessage.__init__(self)
  238. if data is not None:
  239. self._parse_message(data)
  240. def __str__(self):
  241. """
  242. String conversion operator.
  243. """
  244. return self.raw
  245. def _parse_message(self, data):
  246. """
  247. Parses the raw message from the device.
  248. :param data: The message data.
  249. :type data: str
  250. """
  251. try:
  252. self.raw = data
  253. _, values = data.split(':')
  254. self.event_data, self.partition, self.event_type = values.split(',')
  255. except ValueError:
  256. raise InvalidMessageError('Received invalid message: {0}'.format(data))