@@ -6,9 +6,12 @@ import time | |||
import threading | |||
import re | |||
import logging | |||
from collections import OrderedDict | |||
from .event import event | |||
from . import devices | |||
from . import util | |||
from . import messages | |||
from . import zonetracking | |||
class Overseer(object): | |||
""" | |||
@@ -158,6 +161,8 @@ class AD2USB(object): | |||
on_bypass = event.Event('Called when a zone is bypassed.') | |||
on_boot = event.Event('Called when the device finishes bootings.') | |||
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 | |||
on_message = event.Event('Called when a message has been received from the device.') | |||
@@ -179,6 +184,8 @@ class AD2USB(object): | |||
Constructor | |||
""" | |||
self._device = device | |||
self._zonetracker = zonetracking.Zonetracker() | |||
self._power_status = None | |||
self._alarm_status = None | |||
self._bypass_status = None | |||
@@ -260,6 +267,13 @@ class AD2USB(object): | |||
""" | |||
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 | |||
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_read += self._on_read | |||
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): | |||
""" | |||
@@ -289,7 +305,7 @@ class AD2USB(object): | |||
msg = None | |||
if data[0] != '!': | |||
msg = Message(data) | |||
msg = messages.Message(data) | |||
if self.address_mask & msg.mask > 0: | |||
self._update_internal_states(msg) | |||
@@ -298,11 +314,12 @@ class AD2USB(object): | |||
header = data[0:4] | |||
if header == '!EXP' or header == '!REL': | |||
msg = ExpanderMessage(data) | |||
msg = messages.ExpanderMessage(data) | |||
self._update_internal_states(msg) | |||
elif header == '!RFX': | |||
msg = RFMessage(data) | |||
msg = messages.RFMessage(data) | |||
elif header == '!LRR': | |||
msg = LRRMessage(data) | |||
msg = messages.LRRMessage(data) | |||
elif data.startswith('!Ready'): | |||
self.on_boot() | |||
elif data.startswith('!CONFIG'): | |||
@@ -341,38 +358,51 @@ class AD2USB(object): | |||
""" | |||
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): | |||
""" | |||
@@ -402,194 +432,14 @@ class AD2USB(object): | |||
""" | |||
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) |
@@ -0,0 +1,197 @@ | |||
""" | |||
Message representations received from the panel through the AD2USB. | |||
""" | |||
import re | |||
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): | |||
""" | |||
String conversion operator. | |||
""" | |||
return 'rf > {0}: {1}'.format(self.serial_number, self.value) | |||
def _parse_message(self, data): | |||
""" | |||
Parses the raw message from the device. | |||
""" | |||
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() | |||
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(',') |
@@ -0,0 +1,209 @@ | |||
""" | |||
Provides zone tracking functionality for the AD2USB device family. | |||
""" | |||
import time | |||
from .event import event | |||
from . import messages | |||
class Zone(object): | |||
""" | |||
Representation of a panel zone. | |||
""" | |||
CLEAR = 0 | |||
FAULT = 1 | |||
CHECK = 2 # Wire fault | |||
STATUS = { CLEAR: 'CLEAR', FAULT: 'FAULT', CHECK: 'CHECK' } | |||
def __init__(self, zone=0, name='', status=CLEAR): | |||
self.zone = zone | |||
self.name = name | |||
self.status = status | |||
self.timestamp = time.time() | |||
def __str__(self): | |||
return 'Zone {0} {1}'.format(self.zone, self.name) | |||
def __repr__(self): | |||
return 'Zone({0}, {1}, ts {2})'.format(self.zone, Zone.STATUS[self.status], self.timestamp) | |||
class Zonetracker(object): | |||
""" | |||
Handles tracking of zone and their statuses. | |||
""" | |||
on_fault = event.Event('Called when the device detects a zone fault.') | |||
on_restore = event.Event('Called when the device detects that a fault is restored.') | |||
EXPIRE = 30 | |||
def __init__(self): | |||
""" | |||
Constructor | |||
""" | |||
self._zones = {} | |||
self._zones_faulted = [] | |||
self._last_zone_fault = 0 | |||
def update(self, message): | |||
""" | |||
Update zone statuses based on the current message. | |||
""" | |||
zone = -1 | |||
if isinstance(message, messages.ExpanderMessage): | |||
zone = self._expander_to_zone(int(message.address), int(message.channel)) | |||
status = Zone.CLEAR | |||
if int(message.value) == 1: | |||
status = Zone.FAULT | |||
elif int(message.value) == 2: | |||
status = Zone.CHECK | |||
try: | |||
self._update_zone(zone, status=status) | |||
except IndexError: | |||
self._add_zone(zone, status=status) | |||
else: | |||
# Panel is ready, restore all zones. | |||
if message.ready: | |||
for idx, z in enumerate(self._zones_faulted): | |||
self._update_zone(z, Zone.CLEAR) | |||
self._last_zone_fault = 0 | |||
# Process fault | |||
elif "FAULT" in message.text or message.check_zone: | |||
# Apparently this representation can be both base 10 | |||
# or base 16, depending on where the message came | |||
# from. | |||
try: | |||
zone = int(message.numeric_code) | |||
except ValueError: | |||
zone = int(message.numeric_code, 16) | |||
# Add new zones and clear expired ones. | |||
if zone in self._zones_faulted: | |||
self._update_zone(zone) | |||
self._clear_zones(zone) | |||
else: | |||
status = Zone.FAULT | |||
if message.check_zone: | |||
status = Zone.CHECK | |||
self._add_zone(zone, status=status) | |||
self._zones_faulted.append(zone) | |||
self._zones_faulted.sort() | |||
# Save our spot for the next message. | |||
self._last_zone_fault = zone | |||
self._clear_expired_zones() | |||
def _clear_zones(self, zone): | |||
""" | |||
Clear all expired zones from our status list. | |||
""" | |||
cleared_zones = [] | |||
found_last = found_new = at_end = False | |||
# First pass: Find our start spot. | |||
it = iter(self._zones_faulted) | |||
try: | |||
while not found_last: | |||
z = it.next() | |||
if z == self._last_zone_fault: | |||
found_last = True | |||
break | |||
except StopIteration: | |||
at_end = True | |||
# Continue until we find our end point and add zones in | |||
# between to our clear list. | |||
try: | |||
while not at_end and not found_new: | |||
z = it.next() | |||
if z == zone: | |||
found_new = True | |||
break | |||
else: | |||
cleared_zones += [z] | |||
except StopIteration: | |||
pass | |||
# Second pass: roll through the list again if we didn't find | |||
# our end point and remove everything until we do. | |||
if not found_new: | |||
it = iter(self._zones_faulted) | |||
try: | |||
while not found_new: | |||
z = it.next() | |||
if z == zone: | |||
found_new = True | |||
break | |||
else: | |||
cleared_zones += [z] | |||
except StopIteration: | |||
pass | |||
# Actually remove the zones and trigger the restores. | |||
for idx, z in enumerate(cleared_zones): | |||
self._update_zone(z, Zone.CLEAR) | |||
def _clear_expired_zones(self): | |||
zones = [] | |||
for z in self._zones.keys(): | |||
zones += [z] | |||
for z in zones: | |||
if self._zones[z].status != Zone.CLEAR and self._zone_expired(z): | |||
self._update_zone(z, Zone.CLEAR) | |||
def _add_zone(self, zone, name='', status=Zone.CLEAR): | |||
""" | |||
Adds a zone to the internal zone list. | |||
""" | |||
if not zone in self._zones: | |||
self._zones[zone] = Zone(zone=zone, name=name, status=status) | |||
if status != Zone.CLEAR: | |||
self.on_fault(zone) | |||
def _update_zone(self, zone, status=None): | |||
""" | |||
Updates a zones status. | |||
""" | |||
if not zone in self._zones: | |||
raise IndexError('Zone does not exist and cannot be updated: %d', zone) | |||
if status is not None: | |||
self._zones[zone].status = status | |||
self._zones[zone].timestamp = time.time() | |||
if status == Zone.CLEAR: | |||
if zone in self._zones_faulted: | |||
self._zones_faulted.remove(zone) | |||
self.on_restore(zone) | |||
def _zone_expired(self, zone): | |||
if time.time() > self._zones[zone].timestamp + Zonetracker.EXPIRE: | |||
return True | |||
return False | |||
def _expander_to_zone(self, address, channel): | |||
idx = address - 7 # Expanders start at address 7. | |||
return address + channel + (idx * 7) + 1 |
@@ -84,6 +84,12 @@ def handle_boot(sender, args): | |||
def handle_config(sender, args): | |||
print 'config', args | |||
def handle_fault(sender, args): | |||
print 'zone fault', args | |||
def handle_restore(sender, args): | |||
print 'zone restored', args | |||
def upload_usb(): | |||
dev = pyad2usb.ad2usb.devices.USBDevice() | |||
@@ -228,12 +234,36 @@ def test_socket(): | |||
a2u.on_config_received += handle_config | |||
a2u.on_arm += handle_arm | |||
a2u.on_disarm += handle_disarm | |||
a2u.on_zone_fault += handle_fault | |||
a2u.on_zone_restore += handle_restore | |||
a2u.open() | |||
#a2u.save_config() | |||
#a2u.reboot() | |||
a2u.get_config() | |||
#a2u.address = 18 | |||
#a2u.configbits = 0xff00 | |||
#a2u.address_mask = 0xFFFFFFFF | |||
#a2u.emulate_zone[0] = False | |||
#a2u.emulate_relay[0] = False | |||
#a2u.emulate_lrr = False | |||
#a2u.deduplicate = False | |||
#time.sleep(3) | |||
#a2u.emulate_zone[1] = True | |||
#a2u.save_config() | |||
time.sleep(1) | |||
a2u.fault_zone(17, True) | |||
time.sleep(15) | |||
a2u.clear_zone(17) | |||
#time.sleep(1) | |||
#a2u.fault_zone((2, 2), True) | |||
while running: | |||
time.sleep(0.1) | |||