diff --git a/alarmdecoder/decoder.py b/alarmdecoder/decoder.py index 97e4c16..021bd4f 100644 --- a/alarmdecoder/decoder.py +++ b/alarmdecoder/decoder.py @@ -18,6 +18,7 @@ except ImportError: from .event import event from .util import InvalidMessageError from .messages import Message, ExpanderMessage, RFMessage, LRRMessage +from .messages.lrr import LRRSystem from .zonetracking import Zonetracker from .panels import PANEL_TYPES, ADEMCO, DSC @@ -99,7 +100,7 @@ class AlarmDecoder(object): version_flags = "" """Device flags enabled""" - def __init__(self, device): + def __init__(self, device, ignore_message_states=False): """ Constructor @@ -109,7 +110,9 @@ class AlarmDecoder(object): """ self._device = device self._zonetracker = Zonetracker(self) + self._lrr_system = LRRSystem(self) + self._ignore_message_states = ignore_message_states self._battery_timeout = AlarmDecoder.BATTERY_TIMEOUT self._fire_timeout = AlarmDecoder.FIRE_TIMEOUT self._power_status = None @@ -402,10 +405,12 @@ class AlarmDecoder(object): :returns: :py:class:`~alarmdecoder.messages.Message` """ + msg = Message(data) if self._internal_address_mask & msg.mask > 0: - self._update_internal_states(msg) + if not self._ignore_message_states: + self._update_internal_states(msg) self.on_message(message=msg) @@ -453,14 +458,16 @@ class AlarmDecoder(object): """ msg = LRRMessage(data) - if msg.event_type == 'ALARM_PANIC': - self._panic_status = True - self.on_panic(status=True) + # if msg.event_type == 'ALARM_PANIC': + # self._panic_status = True + # self.on_panic(status=True) + # + # elif msg.event_type == 'CANCEL': + # if self._panic_status is True: + # self._panic_status = False + # self.on_panic(status=False) - elif msg.event_type == 'CANCEL': - if self._panic_status is True: - self._panic_status = False - self.on_panic(status=False) + self._lrr_system.update(msg) self.on_lrr_message(message=msg) @@ -534,7 +541,7 @@ class AlarmDecoder(object): :param message: :py:class:`~alarmdecoder.messages.Message` to update internal states with :type message: :py:class:`~alarmdecoder.messages.Message`, :py:class:`~alarmdecoder.messages.ExpanderMessage`, :py:class:`~alarmdecoder.messages.LRRMessage`, or :py:class:`~alarmdecoder.messages.RFMessage` """ - if isinstance(message, Message): + if isinstance(message, Message) and not self._ignore_message_states: self._update_power_status(message) self._update_alarm_status(message) self._update_zone_bypass_status(message) @@ -547,7 +554,7 @@ class AlarmDecoder(object): self._update_zone_tracker(message) - def _update_power_status(self, message): + def _update_power_status(self, message=None, status=None): """ Uses the provided message to update the AC power state. @@ -556,8 +563,15 @@ class AlarmDecoder(object): :returns: bool indicating the new status """ - if message.ac_power != self._power_status: - self._power_status, old_status = message.ac_power, self._power_status + power_status = status + if isinstance(message, Message): + power_status = message.ac_power + + if power_status is None: + return + + if power_status != self._power_status: + self._power_status, old_status = power_status, self._power_status if old_status is not None: self.on_power_changed(status=self._power_status) @@ -585,7 +599,7 @@ class AlarmDecoder(object): return self._alarm_status - def _update_zone_bypass_status(self, message): + def _update_zone_bypass_status(self, message=None, status=None): """ Uses the provided message to update the zone bypass state. @@ -594,16 +608,22 @@ class AlarmDecoder(object): :returns: bool indicating the new status """ + bypass_status = status + if isinstance(message, Message): + bypass_status = message.zone_bypassed - if message.zone_bypassed != self._bypass_status: - self._bypass_status, old_status = message.zone_bypassed, self._bypass_status + if bypass_status is None: + return + + if bypass_status != self._bypass_status: + self._bypass_status, old_status = bypass_status, self._bypass_status if old_status is not None: self.on_bypass(status=self._bypass_status) return self._bypass_status - def _update_armed_status(self, message): + def _update_armed_status(self, message=None, status=None, status_stay=None): """ Uses the provided message to update the armed state. @@ -612,19 +632,28 @@ class AlarmDecoder(object): :returns: bool indicating the new status """ + arm_status = status + stay_status = status_stay - self._armed_status, old_status = message.armed_away, self._armed_status - self._armed_stay, old_stay = message.armed_home, self._armed_stay - if message.armed_away != old_status or message.armed_home != old_stay: - if old_status is not None: + if isinstance(message, Message): + arm_status = message.armed_away + stay_status = message.armed_home + + if arm_status is None or stay_status is None: + return + + self._armed_status, old_status = arm_status, self._armed_status + self._armed_stay, old_stay = stay_status, self._armed_stay + if arm_status != old_status or stay_status != old_stay: + if old_status is not None or message is None: if self._armed_status or self._armed_stay: - self.on_arm(stay=message.armed_home) + self.on_arm(stay=stay_status) else: self.on_disarm() return self._armed_status or self._armed_stay - def _update_battery_status(self, message): + def _update_battery_status(self, message=None, status=None): """ Uses the provided message to update the battery state. @@ -633,18 +662,24 @@ class AlarmDecoder(object): :returns: boolean indicating the new status """ + battery_status = status + if isinstance(message, Message): + battery_status = message.battery_low + + if battery_status is None: + return last_status, last_update = self._battery_status - if message.battery_low == last_status: + if battery_status == last_status: self._battery_status = (last_status, time.time()) else: - if message.battery_low is True or time.time() > last_update + self._battery_timeout: - self._battery_status = (message.battery_low, time.time()) - self.on_low_battery(status=message.battery_low) + if battery_status is True or time.time() > last_update + self._battery_timeout: + self._battery_status = (battery_status, time.time()) + self.on_low_battery(status=battery_status) return self._battery_status[0] - def _update_fire_status(self, message): + def _update_fire_status(self, message=None, status=None): """ Uses the provided message to update the fire alarm state. @@ -653,17 +688,43 @@ class AlarmDecoder(object): :returns: boolean indicating the new status """ + fire_status = status + if isinstance(message, Message): + fire_status = message.fire_alarm + + if fire_status is None: + return last_status, last_update = self._fire_status - if message.fire_alarm == last_status: + if fire_status == last_status: self._fire_status = (last_status, time.time()) else: - if message.fire_alarm is True or time.time() > last_update + self._fire_timeout: - self._fire_status = (message.fire_alarm, time.time()) - self.on_fire(status=message.fire_alarm) + if fire_status is True or time.time() > last_update + self._fire_timeout: + self._fire_status = (fire_status, time.time()) + self.on_fire(status=fire_status) return self._fire_status[0] + def _update_panic_status(self, status=None): + """ + Updates the panic status of the alarm panel. + + :param status: status to use to update + :type status: boolean + + :returns: boolean indicating the new status + """ + if status is None: + return + + if status != self._panic_status: + self._panic_status, old_status = status, self._panic_status + + if old_status is not None: + self.on_panic(status=self._panic_status) + + return self._panic_status + def _update_expander_status(self, message): """ Uses the provided message to update the expander states. diff --git a/alarmdecoder/messages/lrr/events.py b/alarmdecoder/messages/lrr/events.py index b0b99db..0bea717 100644 --- a/alarmdecoder/messages/lrr/events.py +++ b/alarmdecoder/messages/lrr/events.py @@ -702,7 +702,8 @@ LRR_PANIC_EVENTS = [ LRR_CID_EVENT.PANIC_SILENT, LRR_CID_EVENT.PANIC_AUDIBLE, LRR_CID_EVENT.PANIC_DURESS_ACCESS_GRANTED, - LRR_CID_EVENT.PANIC_DURESS_EGRESS_GRANTED + LRR_CID_EVENT.PANIC_DURESS_EGRESS_GRANTED, + LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER # Canceled panic ] LRR_ARM_EVENTS = [ @@ -710,10 +711,14 @@ LRR_ARM_EVENTS = [ LRR_CID_EVENT.OPENCLOSE_BY_USER, LRR_CID_EVENT.OPENCLOSE_GROUP, LRR_CID_EVENT.OPENCLOSE_AUTOMATIC, - LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER, LRR_CID_EVENT.OPENCLOSE_REMOTE_ARMDISARM, LRR_CID_EVENT.OPENCLOSE_QUICK_ARM, LRR_CID_EVENT.OPENCLOSE_KEYSWITCH, LRR_CID_EVENT.OPENCLOSE_ARMED_STAY, LRR_CID_EVENT.OPENCLOSE_KEYSWITCH_ARMED_STAY ] + +LRR_STAY_EVENTS = [ + LRR_CID_EVENT.OPENCLOSE_ARMED_STAY, + LRR_CID_EVENT.OPENCLOSE_KEYSWITCH_ARMED_STAY +] diff --git a/alarmdecoder/messages/lrr/message.py b/alarmdecoder/messages/lrr/message.py index 30f24a3..3da409e 100644 --- a/alarmdecoder/messages/lrr/message.py +++ b/alarmdecoder/messages/lrr/message.py @@ -93,6 +93,6 @@ class LRRMessage(BaseMessage): event_prefix = self.event_prefix, event_source = self.event_source, event_status = self.event_status, - event_code = self.event_code, + event_code = hex(self.event_code), **kwargs ) diff --git a/alarmdecoder/messages/lrr/system.py b/alarmdecoder/messages/lrr/system.py new file mode 100644 index 0000000..1c09844 --- /dev/null +++ b/alarmdecoder/messages/lrr/system.py @@ -0,0 +1,86 @@ + +from .events import LRR_EVENT_TYPE, LRR_EVENT_STATUS, LRR_CID_EVENT +from .events import LRR_FIRE_EVENTS, LRR_POWER_EVENTS, LRR_BYPASS_EVENTS, LRR_BATTERY_EVENTS, \ + LRR_PANIC_EVENTS, LRR_ARM_EVENTS, LRR_STAY_EVENTS + + +class LRRSystem(object): + def __init__(self, alarmdecoder_object): + self._alarmdecoder = alarmdecoder_object + + def update(self, message): + handled = False + + print("LRR Message: {0}".format(message.dict())) + + source = message.event_source + if source == LRR_EVENT_TYPE.CID: + handled = self._handle_cid_message(message) + elif source == LRR_EVENT_TYPE.DSC: + handled = self._handle_dsc_message(message) + elif source == LRR_EVENT_TYPE.ADEMCO: + handled = self._handle_ademco_message(message) + elif source == LRR_EVENT_TYPE.ALARMDECODER: + handled = self._handle_alarmdecoder_message(message) + elif source == LRR_EVENT_TYPE.UNKNOWN: + handled = self._handle_unknown_message(message) + else: + pass + + return handled + + def _handle_cid_message(self, message): + handled = True + + status = self._get_event_status(message) + if status is None: + print("Unknown LRR event status: {0}".format(message)) + return + + if message.event_code in LRR_FIRE_EVENTS: + self._alarmdecoder._update_fire_status(status=status) + elif message.event_code in LRR_POWER_EVENTS: + self._alarmdecoder._update_power_status(status=status) + elif message.event_code in LRR_BYPASS_EVENTS: + self._alarmdecoder._update_zone_bypass_status(status=status) + elif message.event_code in LRR_BATTERY_EVENTS: + self._alarmdecoder._update_battery_status(status=status) + elif message.event_code in LRR_PANIC_EVENTS: + if message.event_code == LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER: + status = False + + self._alarmdecoder._update_panic_status(status=status) + elif message.event_code in LRR_ARM_EVENTS: + # NOTE: status on OPENCLOSE messages is backwards. + status_stay = (message.event_status == LRR_EVENT_STATUS.RESTORE \ + and message.event_code in LRR_STAY_EVENTS) + self._alarmdecoder._update_armed_status(status=not status, status_stay=status_stay) + else: + handled = False + + return handled + + def _handle_dsc_message(self, message): + return False + + def _handle_ademco_message(self, message): + return False + + def _handle_alarmdecoder_message(self, message): + return False + + def _handle_unknown_message(self, message): + # TODO: Log this somewhere useful. + print("UNKNOWN LRR EVENT: {0}".format(message)) + + return False + + def _get_event_status(self, message): + status = None + + if message.event_status == LRR_EVENT_STATUS.TRIGGER: + status = True + elif message.event_status == LRR_EVENT_STATUS.RESTORE: + status = False + + return status