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.

405 lines
12 KiB

  1. """
  2. Message representations received from the panel through the `AlarmDecoder`_ (AD2)
  3. devices.
  4. * :py:class:`Message`: The standard and most common message received from a panel.
  5. * :py:class:`ExpanderMessage`: Messages received from Relay or Zone expander modules.
  6. * :py:class:`RFMessage`: Message received from an RF receiver module.
  7. * :py:class:`LRRMessage`: Message received from a long-range radio module.
  8. .. _AlarmDecoder: http://www.alarmdecoder.com
  9. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  10. """
  11. import re
  12. import datetime
  13. from .util import InvalidMessageError
  14. from .panels import MODES, ADEMCO, DSC
  15. class BaseMessage(object):
  16. """
  17. Base class for messages.
  18. """
  19. raw = None
  20. """The raw message text"""
  21. timestamp = None
  22. """The timestamp of the message"""
  23. def __init__(self):
  24. """
  25. Constructor
  26. """
  27. self.timestamp = datetime.datetime.now()
  28. def __str__(self):
  29. """
  30. String conversion operator.
  31. """
  32. return self.raw
  33. def dict(self, **kwargs):
  34. """
  35. Dictionary representation.
  36. """
  37. return dict(
  38. time=self.timestamp,
  39. mesg=self.raw,
  40. **kwargs
  41. )
  42. def __repr__(self):
  43. """
  44. String representation.
  45. """
  46. return repr(self.dict())
  47. class Message(BaseMessage):
  48. """
  49. Represents a message from the alarm panel.
  50. """
  51. ready = False
  52. """Indicates whether or not the panel is in a ready state."""
  53. armed_away = False
  54. """Indicates whether or not the panel is armed away."""
  55. armed_home = False
  56. """Indicates whether or not the panel is armed home."""
  57. backlight_on = False
  58. """Indicates whether or not the keypad backlight is on."""
  59. programming_mode = False
  60. """Indicates whether or not we're in programming mode."""
  61. beeps = -1
  62. """Number of beeps associated with a message."""
  63. zone_bypassed = False
  64. """Indicates whether or not a zone is bypassed."""
  65. ac_power = False
  66. """Indicates whether or not the panel is on AC power."""
  67. chime_on = False
  68. """Indicates whether or not the chime is enabled."""
  69. alarm_event_occurred = False
  70. """Indicates whether or not an alarm event has occurred."""
  71. alarm_sounding = False
  72. """Indicates whether or not an alarm is sounding."""
  73. battery_low = False
  74. """Indicates whether or not there is a low battery."""
  75. entry_delay_off = False
  76. """Indicates whether or not the entry delay is enabled."""
  77. fire_alarm = False
  78. """Indicates whether or not a fire alarm is sounding."""
  79. check_zone = False
  80. """Indicates whether or not there are zones that require attention."""
  81. perimeter_only = False
  82. """Indicates whether or not the perimeter is armed."""
  83. system_fault = False
  84. """Indicates whether a system fault has occurred."""
  85. panel_type = ADEMCO
  86. """Indicates which panel type was the source of this message."""
  87. numeric_code = None
  88. """The numeric code associated with the message."""
  89. text = None
  90. """The human-readable text to be displayed on the panel LCD."""
  91. cursor_location = -1
  92. """Current cursor location on the keypad."""
  93. mask = None
  94. """Address mask this message is intended for."""
  95. bitfield = None
  96. """The bitfield associated with this message."""
  97. panel_data = None
  98. """The panel data field associated with this message."""
  99. def __init__(self, data=None):
  100. """
  101. Constructor
  102. :param data: message data to parse
  103. :type data: string
  104. """
  105. BaseMessage.__init__(self)
  106. self._regex = re.compile('^(!KPM:){0,1}(\[[a-fA-F0-9\-]+\]),([a-fA-F0-9]+),(\[[a-fA-F0-9]+\]),(".+")$')
  107. if data is not None:
  108. self._parse_message(data)
  109. def _parse_message(self, data):
  110. """
  111. Parse the message from the device.
  112. :param data: message data
  113. :type data: string
  114. :raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
  115. """
  116. match = self._regex.match(data)
  117. if match is None:
  118. raise InvalidMessageError('Received invalid message: {0}'.format(data))
  119. header, self.bitfield, self.numeric_code, self.panel_data, alpha = match.group(1, 2, 3, 4, 5)
  120. self.mask = int(self.panel_data[3:3+8], 16)
  121. is_bit_set = lambda bit: not self.bitfield[bit] == "0"
  122. self.raw = data
  123. self.ready = is_bit_set(1)
  124. self.armed_away = is_bit_set(2)
  125. self.armed_home = is_bit_set(3)
  126. self.backlight_on = is_bit_set(4)
  127. self.programming_mode = is_bit_set(5)
  128. self.beeps = int(self.bitfield[6], 16)
  129. self.zone_bypassed = is_bit_set(7)
  130. self.ac_power = is_bit_set(8)
  131. self.chime_on = is_bit_set(9)
  132. self.alarm_event_occurred = is_bit_set(10)
  133. self.alarm_sounding = is_bit_set(11)
  134. self.battery_low = is_bit_set(12)
  135. self.entry_delay_off = is_bit_set(13)
  136. self.fire_alarm = is_bit_set(14)
  137. self.check_zone = is_bit_set(15)
  138. self.perimeter_only = is_bit_set(16)
  139. self.system_fault = is_bit_set(17)
  140. if self.bitfield[18] in MODES.keys():
  141. self.panel_type = MODES.keys()[MODES.values().index(self.bitfield[18])])
  142. # pos 20-21 - Unused.
  143. self.text = alpha.strip('"')
  144. if int(self.panel_data[19:21], 16) & 0x01 > 0:
  145. # Current cursor location on the alpha display.
  146. self.cursor_location = int(self.bitfield[21:23], 16)
  147. def dict(self, **kwargs):
  148. """
  149. Dictionary representation.
  150. """
  151. return dict(
  152. time = self.timestamp,
  153. bitfield = self.bitfield,
  154. numeric_code = self.numeric_code,
  155. panel_data = self.panel_data,
  156. mask = self.mask,
  157. ready = self.ready,
  158. armed_away = self.armed_away,
  159. armed_home = self.armed_home,
  160. backlight_on = self.backlight_on,
  161. programming_mode = self.programming_mode,
  162. beeps = self.beeps,
  163. zone_bypassed = self.zone_bypassed,
  164. ac_power = self.ac_power,
  165. chime_on = self.chime_on,
  166. alarm_event_occurred = self.alarm_event_occurred,
  167. alarm_sounding = self.alarm_sounding,
  168. battery_low = self.battery_low,
  169. entry_delay_off = self.entry_delay_off,
  170. fire_alarm = self.fire_alarm,
  171. check_zone = self.check_zone,
  172. perimeter_only = self.perimeter_only,
  173. text = self.text,
  174. cursor_location = self.cursor_location,
  175. **kwargs
  176. )
  177. class ExpanderMessage(BaseMessage):
  178. """
  179. Represents a message from a zone or relay expansion module.
  180. """
  181. ZONE = 0
  182. """Flag indicating that the expander message relates to a Zone Expander."""
  183. RELAY = 1
  184. """Flag indicating that the expander message relates to a Relay Expander."""
  185. type = None
  186. """Expander message type: ExpanderMessage.ZONE or ExpanderMessage.RELAY"""
  187. address = -1
  188. """Address of expander"""
  189. channel = -1
  190. """Channel on the expander"""
  191. value = -1
  192. """Value associated with the message"""
  193. def __init__(self, data=None):
  194. """
  195. Constructor
  196. :param data: message data to parse
  197. :type data: string
  198. """
  199. BaseMessage.__init__(self)
  200. if data is not None:
  201. self._parse_message(data)
  202. def _parse_message(self, data):
  203. """
  204. Parse the raw message from the device.
  205. :param data: message data
  206. :type data: string
  207. :raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
  208. """
  209. try:
  210. header, values = data.split(':')
  211. address, channel, value = values.split(',')
  212. self.raw = data
  213. self.address = int(address)
  214. self.channel = int(channel)
  215. self.value = int(value)
  216. except ValueError:
  217. raise InvalidMessageError('Received invalid message: {0}'.format(data))
  218. if header == '!EXP':
  219. self.type = ExpanderMessage.ZONE
  220. elif header == '!REL':
  221. self.type = ExpanderMessage.RELAY
  222. else:
  223. raise InvalidMessageError('Unknown expander message header: {0}'.format(data))
  224. def dict(self, **kwargs):
  225. """
  226. Dictionary representation.
  227. """
  228. return dict(
  229. time = self.timestamp,
  230. address = self.address,
  231. channel = self.channel,
  232. value = self.value,
  233. **kwargs
  234. )
  235. class RFMessage(BaseMessage):
  236. """
  237. Represents a message from an RF receiver.
  238. """
  239. serial_number = None
  240. """Serial number of the RF device."""
  241. value = -1
  242. """Value associated with this message."""
  243. battery = False
  244. """Low battery indication"""
  245. supervision = False
  246. """Supervision required indication"""
  247. loop = [False for _ in range(4)]
  248. """Loop indicators"""
  249. def __init__(self, data=None):
  250. """
  251. Constructor
  252. :param data: message data to parse
  253. :type data: string
  254. """
  255. BaseMessage.__init__(self)
  256. if data is not None:
  257. self._parse_message(data)
  258. def _parse_message(self, data):
  259. """
  260. Parses the raw message from the device.
  261. :param data: message data
  262. :type data: string
  263. :raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
  264. """
  265. try:
  266. self.raw = data
  267. _, values = data.split(':')
  268. self.serial_number, self.value = values.split(',')
  269. self.value = int(self.value, 16)
  270. is_bit_set = lambda b: self.value & (1 << (b - 1)) > 0
  271. # Bit 1 = unknown
  272. self.battery = is_bit_set(2)
  273. self.supervision = is_bit_set(3)
  274. # Bit 4 = unknown
  275. self.loop[2] = is_bit_set(5)
  276. self.loop[1] = is_bit_set(6)
  277. self.loop[3] = is_bit_set(7)
  278. self.loop[0] = is_bit_set(8)
  279. except ValueError:
  280. raise InvalidMessageError('Received invalid message: {0}'.format(data))
  281. def dict(self, **kwargs):
  282. """
  283. Dictionary representation.
  284. """
  285. return dict(
  286. time = self.timestamp,
  287. serial_number = self.serial_number,
  288. value = self.value,
  289. battery = self.battery,
  290. supervision = self.supervision,
  291. **kwargs
  292. )
  293. class LRRMessage(BaseMessage):
  294. """
  295. Represent a message from a Long Range Radio.
  296. """
  297. event_data = None
  298. """Data associated with the LRR message. Usually user ID or zone."""
  299. partition = -1
  300. """The partition that this message applies to."""
  301. event_type = None
  302. """The type of the event that occurred."""
  303. def __init__(self, data=None):
  304. """
  305. Constructor
  306. :param data: message data to parse
  307. :type data: string
  308. """
  309. BaseMessage.__init__(self)
  310. if data is not None:
  311. self._parse_message(data)
  312. def _parse_message(self, data):
  313. """
  314. Parses the raw message from the device.
  315. :param data: message data to parse
  316. :type data: string
  317. :raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
  318. """
  319. try:
  320. self.raw = data
  321. _, values = data.split(':')
  322. self.event_data, self.partition, self.event_type = values.split(',')
  323. except ValueError:
  324. raise InvalidMessageError('Received invalid message: {0}'.format(data))
  325. def dict(self, **kwargs):
  326. """
  327. Dictionary representation.
  328. """
  329. return dict(
  330. time = self.timestamp,
  331. event_data = self.event_data,
  332. event_type = self.event_type,
  333. partition = self.partition,
  334. **kwargs
  335. )