"""
Message representations received from the panel through the `AlarmDecoder`_ (AD2)
devices.
* :py:class:`Message`: The standard and most common message received from a panel.
* :py:class:`ExpanderMessage`: Messages received from Relay or Zone expander modules.
* :py:class:`RFMessage`: Message received from an RF receiver module.
* :py:class:`LRRMessage`: Message received from a long-range radio module.
.. _AlarmDecoder: http://www.alarmdecoder.com
.. moduleauthor:: Scott Petersen <scott@nutech.com>
"""
import re
import datetime
from .util import InvalidMessageError
from .panels import PANEL_TYPES, ADEMCO, DSC
[docs]class BaseMessage(object):
"""
Base class for messages.
"""
raw = None
"""The raw message text"""
timestamp = None
"""The timestamp of the message"""
def __init__(self):
"""
Constructor
"""
self.timestamp = datetime.datetime.now()
def __str__(self):
"""
String conversion operator.
"""
return self.raw
[docs] def dict(self, **kwargs):
"""
Dictionary representation.
"""
return dict(
time=self.timestamp,
mesg=self.raw,
**kwargs
)
def __repr__(self):
"""
String representation.
"""
return repr(self.dict())
[docs]class Message(BaseMessage):
"""
Represents a message from the alarm panel.
"""
ready = False
"""Indicates whether or not the panel is in a ready state."""
armed_away = False
"""Indicates whether or not the panel is armed away."""
armed_home = False
"""Indicates whether or not the panel is armed home."""
backlight_on = False
"""Indicates whether or not the keypad backlight is on."""
programming_mode = False
"""Indicates whether or not we're in programming mode."""
beeps = -1
"""Number of beeps associated with a message."""
zone_bypassed = False
"""Indicates whether or not a zone is bypassed."""
ac_power = False
"""Indicates whether or not the panel is on AC power."""
chime_on = False
"""Indicates whether or not the chime is enabled."""
alarm_event_occurred = False
"""Indicates whether or not an alarm event has occurred."""
alarm_sounding = False
"""Indicates whether or not an alarm is sounding."""
battery_low = False
"""Indicates whether or not there is a low battery."""
entry_delay_off = False
"""Indicates whether or not the entry delay is enabled."""
fire_alarm = False
"""Indicates whether or not a fire alarm is sounding."""
check_zone = False
"""Indicates whether or not there are zones that require attention."""
perimeter_only = False
"""Indicates whether or not the perimeter is armed."""
system_fault = False
"""Indicates whether a system fault has occurred."""
panel_type = ADEMCO
"""Indicates which panel type was the source of this message."""
numeric_code = None
"""The numeric code associated with the message."""
text = None
"""The human-readable text to be displayed on the panel LCD."""
cursor_location = -1
"""Current cursor location on the keypad."""
mask = 0xFFFFFFFF
"""Address mask this message is intended for."""
bitfield = None
"""The bitfield associated with this message."""
panel_data = None
"""The panel data field associated with this message."""
def __init__(self, data=None):
"""
Constructor
:param data: message data to parse
:type data: string
"""
BaseMessage.__init__(self)
self._regex = re.compile('^(!KPM:){0,1}(\[[a-fA-F0-9\-]+\]),([a-fA-F0-9]+),(\[[a-fA-F0-9]+\]),(".+")$')
if data is not None:
self._parse_message(data)
def _parse_message(self, data):
"""
Parse the message from the device.
:param data: message data
:type data: string
:raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
"""
match = self._regex.match(data)
if match is None:
raise InvalidMessageError('Received invalid message: {0}'.format(data))
header, self.bitfield, self.numeric_code, self.panel_data, alpha = match.group(1, 2, 3, 4, 5)
is_bit_set = lambda bit: not self.bitfield[bit] == "0"
self.raw = data
self.ready = is_bit_set(1)
self.armed_away = is_bit_set(2)
self.armed_home = is_bit_set(3)
self.backlight_on = is_bit_set(4)
self.programming_mode = is_bit_set(5)
self.beeps = int(self.bitfield[6], 16)
self.zone_bypassed = is_bit_set(7)
self.ac_power = is_bit_set(8)
self.chime_on = is_bit_set(9)
self.alarm_event_occurred = is_bit_set(10)
self.alarm_sounding = is_bit_set(11)
self.battery_low = is_bit_set(12)
self.entry_delay_off = is_bit_set(13)
self.fire_alarm = is_bit_set(14)
self.check_zone = is_bit_set(15)
self.perimeter_only = is_bit_set(16)
self.system_fault = is_bit_set(17)
if self.bitfield[18] in PANEL_TYPES.keys():
self.panel_type = PANEL_TYPES[self.bitfield[18]]
# pos 20-21 - Unused.
self.text = alpha.strip('"')
if self.panel_type == ADEMCO:
self.mask = int(self.panel_data[3:3+8], 16)
if int(self.panel_data[19:21], 16) & 0x01 > 0:
# Current cursor location on the alpha display.
self.cursor_location = int(self.bitfield[21:23], 16)
[docs] def dict(self, **kwargs):
"""
Dictionary representation.
"""
return dict(
time = self.timestamp,
bitfield = self.bitfield,
numeric_code = self.numeric_code,
panel_data = self.panel_data,
mask = self.mask,
ready = self.ready,
armed_away = self.armed_away,
armed_home = self.armed_home,
backlight_on = self.backlight_on,
programming_mode = self.programming_mode,
beeps = self.beeps,
zone_bypassed = self.zone_bypassed,
ac_power = self.ac_power,
chime_on = self.chime_on,
alarm_event_occurred = self.alarm_event_occurred,
alarm_sounding = self.alarm_sounding,
battery_low = self.battery_low,
entry_delay_off = self.entry_delay_off,
fire_alarm = self.fire_alarm,
check_zone = self.check_zone,
perimeter_only = self.perimeter_only,
text = self.text,
cursor_location = self.cursor_location,
**kwargs
)
[docs]class ExpanderMessage(BaseMessage):
"""
Represents a message from a zone or relay expansion module.
"""
ZONE = 0
"""Flag indicating that the expander message relates to a Zone Expander."""
RELAY = 1
"""Flag indicating that the expander message relates to a Relay Expander."""
type = None
"""Expander message type: ExpanderMessage.ZONE or ExpanderMessage.RELAY"""
address = -1
"""Address of expander"""
channel = -1
"""Channel on the expander"""
value = -1
"""Value associated with the message"""
def __init__(self, data=None):
"""
Constructor
:param data: message data to parse
:type data: string
"""
BaseMessage.__init__(self)
if data is not None:
self._parse_message(data)
def _parse_message(self, data):
"""
Parse the raw message from the device.
:param data: message data
:type data: string
:raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
"""
try:
header, values = data.split(':')
address, channel, value = values.split(',')
self.raw = data
self.address = int(address)
self.channel = int(channel)
self.value = int(value)
except ValueError:
raise InvalidMessageError('Received invalid message: {0}'.format(data))
if header == '!EXP':
self.type = ExpanderMessage.ZONE
elif header == '!REL':
self.type = ExpanderMessage.RELAY
else:
raise InvalidMessageError('Unknown expander message header: {0}'.format(data))
[docs] def dict(self, **kwargs):
"""
Dictionary representation.
"""
return dict(
time = self.timestamp,
address = self.address,
channel = self.channel,
value = self.value,
**kwargs
)
[docs]class RFMessage(BaseMessage):
"""
Represents a message from an RF receiver.
"""
serial_number = None
"""Serial number of the RF device."""
value = -1
"""Value associated with this message."""
battery = False
"""Low battery indication"""
supervision = False
"""Supervision required indication"""
loop = [False for _ in range(4)]
"""Loop indicators"""
def __init__(self, data=None):
"""
Constructor
:param data: message data to parse
:type data: string
"""
BaseMessage.__init__(self)
if data is not None:
self._parse_message(data)
def _parse_message(self, data):
"""
Parses the raw message from the device.
:param data: message data
:type data: string
:raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
"""
try:
self.raw = data
_, values = data.split(':')
self.serial_number, self.value = values.split(',')
self.value = int(self.value, 16)
is_bit_set = lambda b: self.value & (1 << (b - 1)) > 0
# Bit 1 = unknown
self.battery = is_bit_set(2)
self.supervision = is_bit_set(3)
# Bit 4 = unknown
self.loop[2] = is_bit_set(5)
self.loop[1] = is_bit_set(6)
self.loop[3] = is_bit_set(7)
self.loop[0] = is_bit_set(8)
except ValueError:
raise InvalidMessageError('Received invalid message: {0}'.format(data))
[docs] def dict(self, **kwargs):
"""
Dictionary representation.
"""
return dict(
time = self.timestamp,
serial_number = self.serial_number,
value = self.value,
battery = self.battery,
supervision = self.supervision,
**kwargs
)
[docs]class LRRMessage(BaseMessage):
"""
Represent a message from a Long Range Radio.
"""
event_data = None
"""Data associated with the LRR message. Usually user ID or zone."""
partition = -1
"""The partition that this message applies to."""
event_type = None
"""The type of the event that occurred."""
def __init__(self, data=None):
"""
Constructor
:param data: message data to parse
:type data: string
"""
BaseMessage.__init__(self)
if data is not None:
self._parse_message(data)
def _parse_message(self, data):
"""
Parses the raw message from the device.
:param data: message data to parse
:type data: string
:raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
"""
try:
self.raw = data
_, values = data.split(':')
self.event_data, self.partition, self.event_type = values.split(',')
except ValueError:
raise InvalidMessageError('Received invalid message: {0}'.format(data))
[docs] def dict(self, **kwargs):
"""
Dictionary representation.
"""
return dict(
time = self.timestamp,
event_data = self.event_data,
event_type = self.event_type,
partition = self.partition,
**kwargs
)