diff --git a/alarmdecoder/decoder.py b/alarmdecoder/decoder.py index e3e4843..eb50337 100644 --- a/alarmdecoder/decoder.py +++ b/alarmdecoder/decoder.py @@ -21,6 +21,7 @@ from .messages import Message, ExpanderMessage, RFMessage, LRRMessage from .messages.lrr import LRRSystem from .zonetracking import Zonetracker from .panels import PANEL_TYPES, ADEMCO, DSC +from .states import FireState class AlarmDecoder(object): @@ -100,6 +101,10 @@ class AlarmDecoder(object): version_flags = "" """Device flags enabled""" + FIRE_STATE_NONE = 0 + FIRE_STATE_FIRE = 1 + FIRE_STATE_ACKNOWLEDGED = 2 + def __init__(self, device, ignore_message_states=False): """ Constructor @@ -121,6 +126,9 @@ class AlarmDecoder(object): self._armed_status = None self._armed_stay = False self._fire_status = (False, 0) + self._fire_alarming = False + self._fire_alarming_changed = 0 + self._fire_state = FireState.NONE self._battery_status = (False, 0) self._panic_status = False self._relay_status = {} @@ -411,6 +419,8 @@ class AlarmDecoder(object): if self._internal_address_mask & msg.mask > 0: if not self._ignore_message_states: self._update_internal_states(msg) + else: + self._update_fire_status(status=None) self.on_message(message=msg) @@ -678,22 +688,66 @@ class AlarmDecoder(object): :returns: boolean indicating the new status """ + is_lrr = status is not None 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 fire_status == last_status: - self._fire_status = (last_status, time.time()) - else: - if fire_status is True or time.time() > last_update + self._fire_timeout: + print("_update_fire_status: fire_status={fire_status} last_status={last_status} last_update={last_update}".format(fire_status=fire_status, last_status=last_status, last_update=last_update)) + + + if self._fire_state == FireState.NONE: + # Always move to a FIRE state if detected + if fire_status == True: + print("FIRE STATE: NONE -> ALARM") + self._fire_state = FireState.ALARM + self._fire_status = (fire_status, time.time()) + + self.on_fire(status=FireState.ALARM) + + elif self._fire_state == FireState.ALARM: + # If we've received an LRR CANCEL message, move to ACKNOWLEDGED + if is_lrr and fire_status == False: + print("FIRE STATE: ALARM -> ACKNOWLEDGED") + self._fire_state = FireState.ACKNOWLEDGED + self._fire_status = (fire_status, time.time()) + self.on_fire(status=FireState.ACKNOWLEDGED) + else: + # Handle bouncing status changes and timeout in order to revert back to NONE. + if last_status != fire_status or fire_status == True: + self._fire_status = (fire_status, time.time()) + + if fire_status == False and time.time() > last_update + self._fire_timeout: + print("FIRE STATE: ALARM -> NONE") + self._fire_state = FireState.NONE + self.on_fire(status=FireState.NONE) + + elif self._fire_state == FireState.ACKNOWLEDGED: + # If we've received a second LRR FIRE message after a CANCEL, revert back to FIRE and trigger another event. + if is_lrr and fire_status == True: + print("FIRE STATE: ACKNOWLEDGED -> ALARM") + self._fire_state = FireState.ALARM self._fire_status = (fire_status, time.time()) - self.on_fire(status=fire_status) - return self._fire_status[0] + self.on_fire(status=FireState.ALARM) + else: + # Handle bouncing status changes and timeout in order to revert back to NONE. + if last_status != fire_status or fire_status == True: + self._fire_status = (fire_status, time.time()) + + # Handle timeout to revert back to NONE. + if fire_status != True and time.time() > last_update + self._fire_timeout: + print("FIRE STATE: ACKNOWLEDGED -> NONE") + self._fire_state = FireState.NONE + self.on_fire(status=FireState.NONE) + + else: + print("INVALID FIRE STATE={}".format(self._fire_state)) + + + return self._fire_state == FireState.ALARM + def _update_panic_status(self, status=None): """ diff --git a/alarmdecoder/messages/lrr/events.py b/alarmdecoder/messages/lrr/events.py index 9b70333..acd357b 100644 --- a/alarmdecoder/messages/lrr/events.py +++ b/alarmdecoder/messages/lrr/events.py @@ -677,7 +677,8 @@ LRR_FIRE_EVENTS = [ LRR_CID_EVENT.FIRE_HEAT, LRR_CID_EVENT.FIRE_PULL_STATION, LRR_CID_EVENT.FIRE_DUCT, - LRR_CID_EVENT.FIRE_FLAME + LRR_CID_EVENT.FIRE_FLAME, + LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER ] LRR_POWER_EVENTS = [ diff --git a/alarmdecoder/messages/lrr/message.py b/alarmdecoder/messages/lrr/message.py index 495d06e..218f3ad 100644 --- a/alarmdecoder/messages/lrr/message.py +++ b/alarmdecoder/messages/lrr/message.py @@ -42,7 +42,7 @@ class LRRMessage(BaseMessage): event_description = '' """Human-readable description of LRR event.""" - def __init__(self, data=None): + def __init__(self, data=None, skip_report_override=False): """ Constructor @@ -51,6 +51,8 @@ class LRRMessage(BaseMessage): """ BaseMessage.__init__(self) + self.skip_report_override = skip_report_override + if data is not None: self._parse_message(data) @@ -80,6 +82,10 @@ class LRRMessage(BaseMessage): self.event_source = _get_event_source(self.event_prefix) self.event_status = int(event_type_data[1][0]) self.event_code = int(event_type_data[1][1:], 16) + + # replace last 2 digits of event_code with report_code, if applicable. + if not self.skip_report_override and self.report_code not in ['00', 'ff']: + self.event_code = int(event_type_data[1][1] + self.report_code, 16) self.event_description = get_event_description(self.event_source, self.event_code) except ValueError: diff --git a/alarmdecoder/messages/lrr/system.py b/alarmdecoder/messages/lrr/system.py index 1f21cbe..8514c12 100644 --- a/alarmdecoder/messages/lrr/system.py +++ b/alarmdecoder/messages/lrr/system.py @@ -40,7 +40,7 @@ class LRRSystem(object): return handled def _handle_cid_message(self, message): - handled = True + handled = False status = self._get_event_status(message) if status is None: @@ -48,25 +48,39 @@ class LRRSystem(object): return if message.event_code in LRR_FIRE_EVENTS: + if message.event_code == LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER: + status = False + + print("FIRE, status={}".format(status)) self._alarmdecoder._update_fire_status(status=status) - elif message.event_code in LRR_POWER_EVENTS: + handled = True + + if message.event_code in LRR_POWER_EVENTS: self._alarmdecoder._update_power_status(status=status) - elif message.event_code in LRR_BYPASS_EVENTS: + handled = True + + if message.event_code in LRR_BYPASS_EVENTS: self._alarmdecoder._update_zone_bypass_status(status=status) - elif message.event_code in LRR_BATTERY_EVENTS: + handled = True + + if message.event_code in LRR_BATTERY_EVENTS: self._alarmdecoder._update_battery_status(status=status) - elif message.event_code in LRR_PANIC_EVENTS: + handled = True + + if 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: + handled = True + + if 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 + handled = True + return handled diff --git a/alarmdecoder/states.py b/alarmdecoder/states.py new file mode 100644 index 0000000..b33cf2e --- /dev/null +++ b/alarmdecoder/states.py @@ -0,0 +1,4 @@ +class FireState: + NONE = 0 + ALARM = 1 + ACKNOWLEDGED = 2