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.

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