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.

315 lines
8.7 KiB

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