|
|
@@ -6,9 +6,12 @@ import time |
|
|
import threading |
|
|
import threading |
|
|
import re |
|
|
import re |
|
|
import logging |
|
|
import logging |
|
|
|
|
|
from collections import OrderedDict |
|
|
from .event import event |
|
|
from .event import event |
|
|
from . import devices |
|
|
from . import devices |
|
|
from . import util |
|
|
from . import util |
|
|
|
|
|
from . import messages |
|
|
|
|
|
from . import zonetracking |
|
|
|
|
|
|
|
|
class Overseer(object): |
|
|
class Overseer(object): |
|
|
""" |
|
|
""" |
|
|
@@ -158,6 +161,8 @@ class AD2USB(object): |
|
|
on_bypass = event.Event('Called when a zone is bypassed.') |
|
|
on_bypass = event.Event('Called when a zone is bypassed.') |
|
|
on_boot = event.Event('Called when the device finishes bootings.') |
|
|
on_boot = event.Event('Called when the device finishes bootings.') |
|
|
on_config_received = event.Event('Called when the device receives its configuration.') |
|
|
on_config_received = event.Event('Called when the device receives its configuration.') |
|
|
|
|
|
on_zone_fault = event.Event('Called when the device detects a zone fault.') |
|
|
|
|
|
on_zone_restore = event.Event('Called when the device detects that a fault is restored.') |
|
|
|
|
|
|
|
|
# Mid-level Events |
|
|
# Mid-level Events |
|
|
on_message = event.Event('Called when a message has been received from the device.') |
|
|
on_message = event.Event('Called when a message has been received from the device.') |
|
|
@@ -179,6 +184,8 @@ class AD2USB(object): |
|
|
Constructor |
|
|
Constructor |
|
|
""" |
|
|
""" |
|
|
self._device = device |
|
|
self._device = device |
|
|
|
|
|
self._zonetracker = zonetracking.Zonetracker() |
|
|
|
|
|
|
|
|
self._power_status = None |
|
|
self._power_status = None |
|
|
self._alarm_status = None |
|
|
self._alarm_status = None |
|
|
self._bypass_status = None |
|
|
self._bypass_status = None |
|
|
@@ -260,6 +267,13 @@ class AD2USB(object): |
|
|
""" |
|
|
""" |
|
|
Faults a zone if we are emulating a zone expander. |
|
|
Faults a zone if we are emulating a zone expander. |
|
|
""" |
|
|
""" |
|
|
|
|
|
# Allow ourselves to also be passed an address/channel combination |
|
|
|
|
|
# for zone expanders. |
|
|
|
|
|
# |
|
|
|
|
|
# Format (expander index, channel) |
|
|
|
|
|
if isinstance(zone, tuple): |
|
|
|
|
|
zone = self._zonetracker._expander_to_zone(*zone) |
|
|
|
|
|
|
|
|
status = 2 if simulate_wire_problem else 1 |
|
|
status = 2 if simulate_wire_problem else 1 |
|
|
|
|
|
|
|
|
self._device.write("L{0:02}{1}\r".format(zone, status)) |
|
|
self._device.write("L{0:02}{1}\r".format(zone, status)) |
|
|
@@ -278,6 +292,8 @@ class AD2USB(object): |
|
|
self._device.on_close += self._on_close |
|
|
self._device.on_close += self._on_close |
|
|
self._device.on_read += self._on_read |
|
|
self._device.on_read += self._on_read |
|
|
self._device.on_write += self._on_write |
|
|
self._device.on_write += self._on_write |
|
|
|
|
|
self._zonetracker.on_fault += self._on_zone_fault |
|
|
|
|
|
self._zonetracker.on_restore += self._on_zone_restore |
|
|
|
|
|
|
|
|
def _handle_message(self, data): |
|
|
def _handle_message(self, data): |
|
|
""" |
|
|
""" |
|
|
@@ -289,7 +305,7 @@ class AD2USB(object): |
|
|
msg = None |
|
|
msg = None |
|
|
|
|
|
|
|
|
if data[0] != '!': |
|
|
if data[0] != '!': |
|
|
msg = Message(data) |
|
|
|
|
|
|
|
|
msg = messages.Message(data) |
|
|
|
|
|
|
|
|
if self.address_mask & msg.mask > 0: |
|
|
if self.address_mask & msg.mask > 0: |
|
|
self._update_internal_states(msg) |
|
|
self._update_internal_states(msg) |
|
|
@@ -298,11 +314,12 @@ class AD2USB(object): |
|
|
header = data[0:4] |
|
|
header = data[0:4] |
|
|
|
|
|
|
|
|
if header == '!EXP' or header == '!REL': |
|
|
if header == '!EXP' or header == '!REL': |
|
|
msg = ExpanderMessage(data) |
|
|
|
|
|
|
|
|
msg = messages.ExpanderMessage(data) |
|
|
|
|
|
self._update_internal_states(msg) |
|
|
elif header == '!RFX': |
|
|
elif header == '!RFX': |
|
|
msg = RFMessage(data) |
|
|
|
|
|
|
|
|
msg = messages.RFMessage(data) |
|
|
elif header == '!LRR': |
|
|
elif header == '!LRR': |
|
|
msg = LRRMessage(data) |
|
|
|
|
|
|
|
|
msg = messages.LRRMessage(data) |
|
|
elif data.startswith('!Ready'): |
|
|
elif data.startswith('!Ready'): |
|
|
self.on_boot() |
|
|
self.on_boot() |
|
|
elif data.startswith('!CONFIG'): |
|
|
elif data.startswith('!CONFIG'): |
|
|
@@ -341,38 +358,51 @@ class AD2USB(object): |
|
|
""" |
|
|
""" |
|
|
Updates internal device states. |
|
|
Updates internal device states. |
|
|
""" |
|
|
""" |
|
|
if message.ac_power != self._power_status: |
|
|
|
|
|
self._power_status, old_status = message.ac_power, self._power_status |
|
|
|
|
|
|
|
|
if isinstance(message, messages.Message): |
|
|
|
|
|
if message.ac_power != self._power_status: |
|
|
|
|
|
self._power_status, old_status = message.ac_power, self._power_status |
|
|
|
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
self.on_power_changed(self._power_status) |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
self.on_power_changed(self._power_status) |
|
|
|
|
|
|
|
|
if message.alarm_sounding != self._alarm_status: |
|
|
|
|
|
self._alarm_status, old_status = message.alarm_sounding, self._alarm_status |
|
|
|
|
|
|
|
|
if message.alarm_sounding != self._alarm_status: |
|
|
|
|
|
self._alarm_status, old_status = message.alarm_sounding, self._alarm_status |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
self.on_alarm(self._alarm_status) |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
self.on_alarm(self._alarm_status) |
|
|
|
|
|
|
|
|
if message.zone_bypassed != self._bypass_status: |
|
|
|
|
|
self._bypass_status, old_status = message.zone_bypassed, self._bypass_status |
|
|
|
|
|
|
|
|
if message.zone_bypassed != self._bypass_status: |
|
|
|
|
|
self._bypass_status, old_status = message.zone_bypassed, self._bypass_status |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
self.on_bypass(self._bypass_status) |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
self.on_bypass(self._bypass_status) |
|
|
|
|
|
|
|
|
if (message.armed_away | message.armed_home) != self._armed_status: |
|
|
|
|
|
self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status |
|
|
|
|
|
|
|
|
if (message.armed_away | message.armed_home) != self._armed_status: |
|
|
|
|
|
self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
if self._armed_status: |
|
|
|
|
|
self.on_arm() |
|
|
|
|
|
else: |
|
|
|
|
|
self.on_disarm() |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
if self._armed_status: |
|
|
|
|
|
self.on_arm() |
|
|
|
|
|
else: |
|
|
|
|
|
self.on_disarm() |
|
|
|
|
|
|
|
|
if message.fire_alarm != self._fire_status: |
|
|
|
|
|
self._fire_status, old_status = message.fire_alarm, self._fire_status |
|
|
|
|
|
|
|
|
if message.fire_alarm != self._fire_status: |
|
|
|
|
|
self._fire_status, old_status = message.fire_alarm, self._fire_status |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
self.on_fire(self._fire_status) |
|
|
|
|
|
|
|
|
if old_status is not None: |
|
|
|
|
|
self.on_fire(self._fire_status) |
|
|
|
|
|
|
|
|
self._update_zone_tracker(message) |
|
|
|
|
|
|
|
|
|
|
|
def _update_zone_tracker(self, message): |
|
|
|
|
|
# Retrieve a list of faults. |
|
|
|
|
|
# NOTE: This only happens on first boot or after exiting programming mode. |
|
|
|
|
|
if isinstance(message, messages.Message): |
|
|
|
|
|
if not message.ready and "Hit * for faults" in message.text: |
|
|
|
|
|
self._device.write('*') |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
self._zonetracker.update(message) |
|
|
|
|
|
|
|
|
def _on_open(self, sender, args): |
|
|
def _on_open(self, sender, args): |
|
|
""" |
|
|
""" |
|
|
@@ -402,194 +432,14 @@ class AD2USB(object): |
|
|
""" |
|
|
""" |
|
|
self.on_write(args) |
|
|
self.on_write(args) |
|
|
|
|
|
|
|
|
class Message(object): |
|
|
|
|
|
""" |
|
|
|
|
|
Represents a message from the alarm panel. |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, data=None): |
|
|
|
|
|
""" |
|
|
|
|
|
Constructor |
|
|
|
|
|
""" |
|
|
|
|
|
self.ready = False |
|
|
|
|
|
self.armed_away = False |
|
|
|
|
|
self.armed_home = False |
|
|
|
|
|
self.backlight_on = False |
|
|
|
|
|
self.programming_mode = False |
|
|
|
|
|
self.beeps = -1 |
|
|
|
|
|
self.zone_bypassed = False |
|
|
|
|
|
self.ac_power = False |
|
|
|
|
|
self.chime_on = False |
|
|
|
|
|
self.alarm_event_occurred = False |
|
|
|
|
|
self.alarm_sounding = False |
|
|
|
|
|
self.battery_low = False |
|
|
|
|
|
self.entry_delay_off = False |
|
|
|
|
|
self.fire_alarm = False |
|
|
|
|
|
self.check_zone = False |
|
|
|
|
|
self.perimeter_only = False |
|
|
|
|
|
self.numeric_code = "" |
|
|
|
|
|
self.text = "" |
|
|
|
|
|
self.cursor_location = -1 |
|
|
|
|
|
self.data = "" |
|
|
|
|
|
self.mask = "" |
|
|
|
|
|
self.bitfield = "" |
|
|
|
|
|
self.panel_data = "" |
|
|
|
|
|
|
|
|
|
|
|
self._regex = re.compile('("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*)') |
|
|
|
|
|
|
|
|
|
|
|
if data is not None: |
|
|
|
|
|
self._parse_message(data) |
|
|
|
|
|
|
|
|
|
|
|
def _parse_message(self, data): |
|
|
|
|
|
""" |
|
|
|
|
|
Parse the message from the device. |
|
|
|
|
|
""" |
|
|
|
|
|
m = self._regex.match(data) |
|
|
|
|
|
|
|
|
|
|
|
if m is None: |
|
|
|
|
|
raise util.InvalidMessageError('Received invalid message: {0}'.format(data)) |
|
|
|
|
|
|
|
|
|
|
|
self.bitfield, self.numeric_code, self.panel_data, alpha = m.group(1, 2, 3, 4) |
|
|
|
|
|
self.mask = int(self.panel_data[3:3+8], 16) |
|
|
|
|
|
|
|
|
|
|
|
self.data = data |
|
|
|
|
|
self.ready = not self.bitfield[1:2] == "0" |
|
|
|
|
|
self.armed_away = not self.bitfield[2:3] == "0" |
|
|
|
|
|
self.armed_home = not self.bitfield[3:4] == "0" |
|
|
|
|
|
self.backlight_on = not self.bitfield[4:5] == "0" |
|
|
|
|
|
self.programming_mode = not self.bitfield[5:6] == "0" |
|
|
|
|
|
self.beeps = int(self.bitfield[6:7], 16) |
|
|
|
|
|
self.zone_bypassed = not self.bitfield[7:8] == "0" |
|
|
|
|
|
self.ac_power = not self.bitfield[8:9] == "0" |
|
|
|
|
|
self.chime_on = not self.bitfield[9:10] == "0" |
|
|
|
|
|
self.alarm_event_occurred = not self.bitfield[10:11] == "0" |
|
|
|
|
|
self.alarm_sounding = not self.bitfield[11:12] == "0" |
|
|
|
|
|
self.battery_low = not self.bitfield[12:13] == "0" |
|
|
|
|
|
self.entry_delay_off = not self.bitfield[13:14] == "0" |
|
|
|
|
|
self.fire_alarm = not self.bitfield[14:15] == "0" |
|
|
|
|
|
self.check_zone = not self.bitfield[15:16] == "0" |
|
|
|
|
|
self.perimeter_only = not self.bitfield[16:17] == "0" |
|
|
|
|
|
# bits 17-20 unused. |
|
|
|
|
|
self.text = alpha.strip('"') |
|
|
|
|
|
|
|
|
|
|
|
if int(self.panel_data[19:21], 16) & 0x01 > 0: |
|
|
|
|
|
self.cursor_location = int(self.bitfield[21:23], 16) # Alpha character index that the cursor is on. |
|
|
|
|
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
|
|
""" |
|
|
|
|
|
String conversion operator. |
|
|
|
|
|
""" |
|
|
|
|
|
return 'msg > {0:0<9} [{1}{2}{3}] -- ({4}) {5}'.format(hex(self.mask), 1 if self.ready else 0, 1 if self.armed_away else 0, 1 if self.armed_home else 0, self.numeric_code, self.text) |
|
|
|
|
|
|
|
|
|
|
|
class ExpanderMessage(object): |
|
|
|
|
|
""" |
|
|
|
|
|
Represents a message from a zone or relay expansion module. |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
ZONE = 0 |
|
|
|
|
|
RELAY = 1 |
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, data=None): |
|
|
|
|
|
""" |
|
|
|
|
|
Constructor |
|
|
|
|
|
""" |
|
|
|
|
|
self.type = None |
|
|
|
|
|
self.address = None |
|
|
|
|
|
self.channel = None |
|
|
|
|
|
self.value = None |
|
|
|
|
|
self.raw = None |
|
|
|
|
|
|
|
|
|
|
|
if data is not None: |
|
|
|
|
|
self._parse_message(data) |
|
|
|
|
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
|
|
""" |
|
|
|
|
|
String conversion operator. |
|
|
|
|
|
""" |
|
|
|
|
|
expander_type = 'UNKWN' |
|
|
|
|
|
if self.type == ExpanderMessage.ZONE: |
|
|
|
|
|
expander_type = 'ZONE' |
|
|
|
|
|
elif self.type == ExpanderMessage.RELAY: |
|
|
|
|
|
expander_type = 'RELAY' |
|
|
|
|
|
|
|
|
|
|
|
return 'exp > [{0: <5}] {1}/{2} -- {3}'.format(expander_type, self.address, self.channel, self.value) |
|
|
|
|
|
|
|
|
|
|
|
def _parse_message(self, data): |
|
|
|
|
|
""" |
|
|
|
|
|
Parse the raw message from the device. |
|
|
|
|
|
""" |
|
|
|
|
|
header, values = data.split(':') |
|
|
|
|
|
address, channel, value = values.split(',') |
|
|
|
|
|
|
|
|
|
|
|
self.raw = data |
|
|
|
|
|
self.address = address |
|
|
|
|
|
self.channel = channel |
|
|
|
|
|
self.value = value |
|
|
|
|
|
|
|
|
|
|
|
if header == '!EXP': |
|
|
|
|
|
self.type = ExpanderMessage.ZONE |
|
|
|
|
|
elif header == '!REL': |
|
|
|
|
|
self.type = ExpanderMessage.RELAY |
|
|
|
|
|
|
|
|
|
|
|
class RFMessage(object): |
|
|
|
|
|
""" |
|
|
|
|
|
Represents a message from an RF receiver. |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, data=None): |
|
|
|
|
|
""" |
|
|
|
|
|
Constructor |
|
|
|
|
|
""" |
|
|
|
|
|
self.raw = None |
|
|
|
|
|
self.serial_number = None |
|
|
|
|
|
self.value = None |
|
|
|
|
|
|
|
|
|
|
|
if data is not None: |
|
|
|
|
|
self._parse_message(data) |
|
|
|
|
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
|
|
|
|
|
def _on_zone_fault(self, sender, args): |
|
|
""" |
|
|
""" |
|
|
String conversion operator. |
|
|
|
|
|
|
|
|
Internal handler for zone faults. |
|
|
""" |
|
|
""" |
|
|
return 'rf > {0}: {1}'.format(self.serial_number, self.value) |
|
|
|
|
|
|
|
|
self.on_zone_fault(args) |
|
|
|
|
|
|
|
|
def _parse_message(self, data): |
|
|
|
|
|
|
|
|
def _on_zone_restore(self, sender, args): |
|
|
""" |
|
|
""" |
|
|
Parses the raw message from the device. |
|
|
|
|
|
|
|
|
Internal handler for zone restoration. |
|
|
""" |
|
|
""" |
|
|
self.raw = data |
|
|
|
|
|
|
|
|
|
|
|
_, values = data.split(':') |
|
|
|
|
|
self.serial_number, self.value = values.split(',') |
|
|
|
|
|
|
|
|
|
|
|
class LRRMessage(object): |
|
|
|
|
|
""" |
|
|
|
|
|
Represent a message from a Long Range Radio. |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, data=None): |
|
|
|
|
|
""" |
|
|
|
|
|
Constructor |
|
|
|
|
|
""" |
|
|
|
|
|
self.raw = None |
|
|
|
|
|
self._event_data = None |
|
|
|
|
|
self._partition = None |
|
|
|
|
|
self._event_type = None |
|
|
|
|
|
|
|
|
|
|
|
if data is not None: |
|
|
|
|
|
self._parse_message(data) |
|
|
|
|
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
|
|
""" |
|
|
|
|
|
String conversion operator. |
|
|
|
|
|
""" |
|
|
|
|
|
return 'lrr > {0} @ {1} -- {2}'.format(self._event_type, self._partition, self._event_data) |
|
|
|
|
|
|
|
|
|
|
|
def _parse_message(self, data): |
|
|
|
|
|
""" |
|
|
|
|
|
Parses the raw message from the device. |
|
|
|
|
|
""" |
|
|
|
|
|
self.raw = data |
|
|
|
|
|
|
|
|
|
|
|
_, values = data.split(':') |
|
|
|
|
|
self._event_data, self._partition, self._event_type = values.split(',') |
|
|
|
|
|
|
|
|
self.on_zone_restore(args) |