Skip to content

Instantly share code, notes, and snippets.

@gordol
Created November 29, 2017 07:47
Show Gist options
  • Select an option

  • Save gordol/14570b0a8d8a74ca1c587b8c0b771136 to your computer and use it in GitHub Desktop.

Select an option

Save gordol/14570b0a8d8a74ca1c587b8c0b771136 to your computer and use it in GitHub Desktop.
from eventlet.green import socket
from helpers import dict_merge
SOH = u'\u0001'
ETX = u'\u0003'
alarm_category_code_mapping = dict(
name = 'alarm/warning category',
len = 2,
format = 'AA',
codes = {
'00': 'All Functions Normal',
'01': 'System Alarm',
'02': 'Tank Alarm',
'03': 'Liquid Sensor Alarm',
'04': 'Vapor Sensor Alarm',
'05': 'Input Alarm',
'06': 'Volumetric Line Leak Alarm',
'07': 'Groundwater Sensor Alarm',
'08': 'Type A Sensor Alarm',
'12': 'Type B Sensor Alarm',
'13': 'Universal Sensor Alarm',
'14': 'Auto-Dial Fax Alarm',
'18': 'Mechanical Dispenser Interface Alarm',
'19': 'Electronic Dispenser Interface Alarm',
'20': 'Product Alarm',
'21': 'Pressure Line Leak Alarm',
'26': 'Wireless PLLD Alarm',
'28': 'Smart Sensor Alarm',
'29': 'Modbus Alarm',
'30': 'ISD Site Alarm',
'31': 'ISD Hose Alarm',
'32': 'ISD Vapor Flow Meter Alarm',
'33': 'PMC Alarm',
'34': 'Pump Relay Monitor Alarm',
'35': 'VMCI Dispenser Interface Alarm (Version 28)',
'36': 'VMC Alarm (Version 28)',
'37': 'APM Alarm (Version 31)',
'99': 'Externally Detected Alarm (not reported by Console)',
},
)
alarm_type_code_mapping = dict(
name = 'alarm type',
len = 2,
format = 'NN',
codes = {
#- If AA is 01 and NN is:
('AA', '01'): {
'01': 'Printer out of Paper',
'02': 'Printer Error',
'03': 'EEPROM Configuration Error',
'04': 'Battery Off',
'05': 'Too Many Tanks',
'06': 'System Security Warning',
'07': 'ROM Revision Warning',
'08': 'Remote Display Communications Error',
'09': 'Autodial Error',
'10': 'Software Module Warning',
'11': 'Tank Test Shutdown Warning',
'12': 'Protective Cover Alarm',
'13': 'BIR Shift Close Pending',
'14': 'BIR Daily Close Pending',
'15': 'PC(H8) Revision Warning',
'16': 'System Self Test Error',
'17': 'System Clock Incorrect Warning',
'18': 'System Device Poll Timeout',
'19': 'Maintenance Tracker NVMem Removed',
'20': 'Maintenance Tracker Communication Module Removed'
},
#- If AA is 02 and NN is:
('AA', '02'): {
'01': 'Tank Setup Data Warning',
'02': 'Tank Leak Alarm',
'03': 'Tank High Water Alarm',
'04': 'Tank Overfill Alarm',
'05': 'Tank Low Product Alarm',
'06': 'Tank Sudden Loss Alarm',
'07': 'Tank High Product Alarm',
'08': 'Tank Invalid Fuel Level Alarm',
'09': 'Tank Probe Out Alarm',
'10': 'Tank High Water Warning',
'11': 'Tank Delivery Needed Warning',
'12': 'Tank Maximum Product Alarm',
'13': 'Tank Gross Leak Test Fail Alarm',
'14': 'Tank Periodic Leak Test Fail Alarm',
'15': 'Tank Annual Leak Test Fail Alarm',
'16': 'Tank Periodic Test Needed Warning',
'17': 'Tank Annual Test Needed Warning',
'18': 'Tank Periodic Test Needed Alarm',
'19': 'Tank Annual Test Needed Alarm',
'20': 'Tank Leak Test Active',
'21': 'Tank No CSLD Idle Time Warning',
'22': 'Tank Siphon Break Active Warning',
'23': 'Tank CSLD Rate Increase Warning',
'24': 'Tank AccuChart Calibration Warning',
'25': 'Tank HRM Reconciliation Warning',
'26': 'Tank HRM Reconciliation Alarm',
'27': 'Tank Cold Temperature Warning',
'28': 'Tank Missing Delivery Ticket Warning',
'29': 'Tank/Line Gross Leak Alarm',
'30': 'Delivery Density Warning',
'31': 'Density Warning',
'32': 'Fuel Quality Alarm',
},
#- If AA is 03, 04, 07, 08, 12, or 13 and NN is:
('AA', '03', '04', '07', '08', '12', '13'): {
'02': 'Sensor Setup Data Warning',
'03': 'Sensor Fuel Alarm',
'04': 'Sensor Out Alarm',
'05': 'Sensor Short Alarm',
'06': 'Sensor Water Alarm',
'07': 'Sensor Water Out Alarm',
'08': 'Sensor High Liquid Alarm',
'09': 'Sensor Low Liquid Alarm',
'10': 'Sensor Liquid Warning',
},
#- If AA is 05 and NN is:
('AA', '05'): {
'01': 'Input Setup Data Warning',
'02': 'Input Normal',
'03': 'Input Alarm',
},
#- If AA is 06 and NN is:
('AA', '06'): {
'01': 'VLLD Setup Data Warning',
'02': 'VLLD Self Test Alarm',
'03': 'VLLD Shutdown Alarm',
'04': 'VLLD Leak Test Fail Alarm',
'05': 'VLLD Selftest Invalid Warning',
'06': 'VLLD Continuous Handle On Warning',
'07': 'VLLD Gross Line Test Fail Alarm',
'08': 'VLLD Gross Line Selftest Fail Alarm',
'09': 'VLLD Gross Pump Test Fail Alarm',
'10': 'VLLD Gross Pump Selftest Fail Alarm',
'11': 'VLLD Periodic Test Needed Warning',
'12': 'VLLD Annual Test Needed Warning',
'13': 'VLLD Periodic Test Needed Alarm',
'14': 'VLLD Annual Test Needed Alarm',
'15': 'VLLD Periodic Line Test Fail Alarm',
'16': 'VLLD Periodic Line Selftest Fail Alarm',
'17': 'VLLD Periodic Pump Test Fail Alarm',
'18': 'VLLD Periodic Pump Selftest Fail Alarm',
'19': 'VLLD Annual Line Test Fail Alarm',
'20': 'VLLD Annual Line Selftest Fail Alarm',
'21': 'VLLD Annual Pump Test Fail Alarm',
'22': 'VLLD Annual Pump Selftest Fail Alarm',
'23': 'VLLD Pressure Warning',
'24': 'VLLD Pressure Alarm',
'25': 'VLLD Gross Test Fault Alarm',
'26': 'VLLD Periodic Test Fault Alarm',
'27': 'VLLD Annual Test Fault Alarm',
'28': 'VLLD Fuel Out Alarm',
},
#- If AA is 14 and NN is:
('AA', '14'): {
'01': 'Autodial Setup Data Warning',
'02': 'Autodial Failed Alarm',
'03': 'Autodial Service Report Warning (Added in V19)',
'04': 'Autodial Alarm Clear Warning (Added in V19)',
'05': 'Autodial Delivery Report Warning (Added in V19)',
},
#- If AA is 18, 19 and NN is:
('AA', '18', '19'): {
'02': 'DIM Disabled Alarm',
'03': 'DIM Communication Failure Alarm',
'04': 'DIM Transaction Alarm',
},
#- If AA is 20 and NN is:
('AA', '20'): {
'01': 'BIR Setup Data Warning',
'02': 'BIR Threshold Alarm',
'03': 'BIR Close Shift Warning',
'04': 'BIR Close Daily Warning',
},
#- If AA is 21 and NN is:
('AA', '21'): {
'01': 'PLLD Setup Data Warning',
'02': 'PLLD Gross Test Fail Alarm',
'03': 'PLLD Annual Test Fail Alarm',
'04': 'PLLD Periodic Test Needed Warning',
'05': 'PLLD Periodic Test Needed Alarm',
'06': 'PLLD Sensor Open Alarm',
'07': 'PLLD High Pressure Alarm (Obsolete V19)',
'08': 'PLLD Shutdown Alarm',
'09': 'PLLD High Pressure Warning (Obsolete V19)',
'10': 'PLLD Continuous Handle On Warning (Obsolete V19)',
'11': 'PLLD Periodic Test Fail Alarm',
'12': 'PLLD Annual Test Needed Warning',
'13': 'PLLD Annual Test Needed Alarm',
'14': 'PLLD Low Pressure Alarm',
'15': 'PLLD Sensor Short Alarm (Obsolete V19)',
'16': 'PLLD Continuous Handle On Alarm',
'17': 'PLLD Fuel Out Alarm',
'18': 'PLLD Line Equipment Alarm',
},
#- If AA is 26 and NN is:
('AA', '26'): {
'01': 'WPLLD Setup Data Warning',
'02': 'WPLLD Gross Test Fail Alarm',
'03': 'WPLLD Periodic Test Fail Alarm',
'04': 'WPLLD Periodic Test Needed Warning',
'05': 'WPLLD Periodic Test Needed Alarm',
'06': 'WPLLD Sensor Open Alarm',
'07': 'WPLLD Communications Alarm',
'08': 'WPLLD Shutdown Alarm',
'09': 'WPLLD Continuous Handle On Warning (Obsolete V19)',
'10': 'WPLLD Annual Test Fail Alarm',
'11': 'WPLLD Annual Test Needed Warning',
'12': 'WPLLD Annual Test Needed Alarm',
'13': 'WPLLD High Pressure Warning (Obsolete V19)',
'14': 'WPLLD High Pressure Alarm (Obsolete V19)',
'15': 'WPLLD Sensor Short Alarm (Obsolete V19)',
'16': 'WPLLD Continuous Handle On Alarm',
'17': 'WPLLD Fuel Out Alarm',
'18': 'WPLLD Line Equipment Alarm',
},
#- If AA is 28 and NN is:
('AA', '28'): {
'01': 'Smart Sensor Setup Data Warning',
'02': 'Smart Sensor Communication Alarm',
'03': 'Smart Sensor Fault Alarm',
'04': 'Smart Sensor Fuel Warning',
'05': 'Smart Sensor Fuel Alarm',
'06': 'Smart Sensor Water Warning',
'07': 'Smart Sensor Water Alarm',
'08': 'Smart Sensor High Liquid Warning',
'09': 'Smart Sensor High Liquid Alarm',
'10': 'Smart Sensor Low Liquid Warning',
'11': 'Smart Sensor Low Liquid Alarm',
'12': 'Smart Sensor Temperature Warning',
'13': 'Smart Sensor Relay Active',
'14': 'Smart Sensor Install Alarm',
'15': 'Smart Sensor Sensor Fault Warning',
'16': 'Smart Sensor Vacuum Warning',
'17': 'Smart Sensor No Vacuum Warning',
},
#- If AA is 29 and NN is:
('AA', '29'): {
'01': 'Improper Setup alarm',
'02': 'Communication Loss alarm',
},
#- If AA is 30 and NN is:
('AA', '30'): {
'01': 'Stage 1 Transfer Monitoring Failure warning (ISD only)',
'02': 'Containment Monitoring Gross Failure warning (ISD)',
'03': 'Containment Monitoring Gross Failure alarm (ISD)',
'04': 'Containment Monitoring Degradation Failure warning (ISD only)',
'05': 'Containment Monitoring Degradation Failure alarm (ISD only)',
'06': 'Containment Monitoring CVLD Failure warning (ISD)',
'07': 'Containment Monitoring CVLD Failure alarm (ISD)',
'08': 'Vapor Processor Over Pressure Failure warning (ISD only)',
'09': 'Vapor Processor Over Pressure Failure alarm (ISD only)',
'10': 'Vapor Processor Status Test warning (ISD only)',
'11': 'Vapor Processor Status Test alarm (ISD only)',
'12': 'Missing Relay Setup alarm (ISD only)',
'13': 'Missing Hose Setup alarm (ISD only)',
'14': 'Missing Tank Setup alarm (ISD)',
'15': 'Missing Vapor Flow Meter alarm (ISD only)',
'16': 'Missing Vapor Pressure Sensor alarm (ISD)',
'17': 'Missing Vapor Pressure Input alarm (ISD)',
'18': 'Setup Fail warning (ISD)',
'19': 'Setup Fail alarm (ISD)',
'20': 'Sensor Out warning (ISD)',
'21': 'Sensor Out alarm (ISD)',
'22': 'PC-ISD Offline (ISD)',
},
#- If AA is 31 and NN is:
('AA', '31'): {
'01': 'Collection Monitoring Gross Failure warning',
'02': 'Collection Monitoring Gross Failure alarm',
'03': 'Collection Monitoring Degradation Failure warning',
'04': 'Collection Monitoring Degradation Failure alarm',
'05': 'Flow Performance Hose Blockage Failure warning',
'06': 'Flow Performance Hose Blockage Failure alarm',
'07': 'Vapor Flow Meter Setup alarm',
},
#- If AA is 32 and NN is:
('AA', '32'): {
'01': 'Locked rotor alarm',
},
#- If AA is 33 and NN is:
('AA', '33'): {
'01': 'Vapor Processor Run Time Fault warning',
'02': 'Processor Monitoring Effluent Emissions Failure warning',
'03': 'Processor Monitoring Effluent Emissions Failure alarm',
'04': 'Processor Monitoring Over Pressure Failure warning',
'05': 'Processor Monitoring Over Pressure Failure alarm',
'06': 'Processor Monitoring Duty Cycle Failure warning',
'07': 'Processor Monitoring Duty Cycle Failure alarm',
'08': 'PMC (stand alone mode only) Setup warning',
},
#- If AA is 34 and NN is:
('AA', '34'): {
'01': 'Setup Data Warning',
'02': 'Pump Relay Alarm',
},
#- If AA is 35 and NN is:
('AA', '35'): {
'01': 'Setup Data Warning',
'02': 'Disabled VMCI Alarm',
},
#- If AA is 36 and NN is:
('AA', '36'): {
'01': 'VMC Comm timeout',
'02': 'Meter Not Connected',
'03': 'FP Shutdown Warning',
'04': 'FP Shutdown Alarm',
},
#- If AA is 37 and NN is:
('AA', '37'): {
'01': 'Gross Over-Pressure Test Warning',
'02': 'APM Gross Over-Pressure Test Failure warning',
'03': 'APM Gross Over-Pressure Test Failure alarm',
'04': 'APM Degradation Over-Pressure Test Failure warning',
'05': 'APM Degradation Over-Pressure Test Failure alarm',
'06': 'APM Sensor Test Failure warning',
'07': 'APM Sensor Test Failure alarm',
'08': 'APM Setup Failure warning',
'09': 'APM Sensor Out Failure warning',
'10': 'APM Sensor Out Failure alarm',
},
#- If AA is 99 and NN is:
('AA', '99'): {
'01': 'Externally Dectected Communication Alarm',
'02': 'Communications - Data Reception Timeout',
'03': 'Communications - Failed Checksum',
'04': 'Communications - Parity Error',
'05': 'Modem - Line Busy',
'06': 'Modem - No Answer',
'07': 'Modem - No Carrier',
'08': 'Modem - No Dial Tone',
'09': 'Modem - Modem Error',
'10': 'Modem - Modem Not Responding',
'11': 'Modem - Port Not Available',
'12': 'Polling - Could Not Update Queue',
'13': 'Polling - Invalid Data Type Requested',
},
},
)
computer_format_mapping = {
'i10100': [
dict(name='year', len=2, type='datetime', format='YY'),
dict(name='month', len=2, type='datetime', format='MM'),
dict(name='day', len=2, type='datetime', format='DD'),
dict(name='hour', len=2, type='datetime', format='HH'),
dict(name='minute', len=2, type='datetime', format='mm'),
dict_merge(alarm_category_code_mapping, {'start': True}),
alarm_type_code_mapping,
dict(name='tank/sensor', len=2, format='TT'),
dict(name='delimiter', len=2, format='&&'),
dict(name='checksum', len=4, format='CCCC'),
],
'i11100': [
dict(name='year', len=2, type='datetime', format='YY'),
dict(name='month', len=2, type='datetime', format='MM'),
dict(name='day', len=2, type='datetime', format='DD'),
dict(name='hour', len=2, type='datetime', format='HH'),
dict(name='minute', len=2, type='datetime', format='mm'),
dict_merge(alarm_category_code_mapping, {'start': True}),
dict(
name = 'sensor category',
len = 2,
format = 'cc',
codes = {
'00': 'Other',
'01': 'Annular',
'02': 'Dispenser Pan',
'03': 'Monitoring Well',
'04': 'STP Sump',
'05': 'Piping Sump',
},
),
alarm_type_code_mapping,
dict(name='tank/sensor', len=2, format='TT'),
dict(
name = 'alarm state',
len = 2,
format = 'cc',
codes = {
'01': 'Alarm occurred',
'02': 'Alarm cleared',
},
),
dict(
end = True,
name = 'alarm datetime',
len = 10,
format = 'YYMMDDHHmm',
),
dict(name='delimiter', len=2, format='&&'),
dict(name='checksum', len=4, format='CCCC'),
],
}
class ATG():
def __init__(self, addr, port):
self.addr = socket.gethostbyname(addr)
self.port = int(port)
self.sock = socket.socket()
self.sock.settimeout(5)
def connect(self):
self.sock.connect((self.addr, self.port))
def send(self, msg):
self.sock.sendall(msg)
def receive(self):
data = b''
while True:
try:
packet = self.sock.recv(1024)
data += packet
if chr(3) in packet:
break
except socket.timeout:
data += '!!!TIMEOUT'
break
except:
data += '!!!UNCAUGHT ERROR'
break
return data
def checksum(self, response):
msg, checksum = response[response.index(SOH):response.index(ETX)].split('&&')
return int(checksum, 16) + sum([ord(c) for c in msg + '&&']) == 65536
def parseResponse(self, response, command):
command_code = command[:6]
command_data = command[6:]
if command[0] in [u'i', u's']:
checksum_valid = self.checksum(response)
parsed_results = None
if checksum_valid:
try:
parse_template = computer_format_mapping[command_code]
except:
parse_template = None
if parse_template:
parsed_response = response[response.index(SOH):response.index(ETX)].split(command_code)[1]
checksum = parsed_response.split('&&')[1]
backrefs = []
parsed_results = dict(groups=[])
delimiter_triggered = False
start_idx = None
end_idx = None
num_groups = 0
ci = 0 #chunk index for template parser
while ci < len(parse_template):
chunk = parse_template[ci]
parsed_value = parsed_response[:chunk['len']]
if 'start' in chunk:
start_idx = ci
if 'end' in chunk:
end_idx = ci
ci += 1
parsed_result = dict()
if 'type' in chunk and chunk['type']:
if chunk['type'] not in parsed_results:
parsed_results[chunk['type']] = {}
parsed_results[chunk['type']] = dict_merge(parsed_results[chunk['type']], {chunk['name']: parsed_response[:chunk['len']]})
else:
if chunk['name'] != 'delimiter':
parsed_result[chunk['name']] = parsed_response[:chunk['len']]
if chunk['name'] == 'checksum':
if parsed_value != checksum:
parsed_result['checksum_valid'] = False
#this shouldn't happen, here for debugging purposes
raise(Exception('checksum mismatch'))
elif parsed_value == checksum:
parsed_result['checksum_valid'] = True
#jump if delimter triggered early
if delimiter_triggered and chunk['name'] != 'delimiter':
continue
#until we get to actual delimiter chunk, then resume
if delimiter_triggered and chunk['name'] == 'delimiter':
delimiter_triggered = False
if start_idx and ci > start_idx and ((not end_idx) or (ci - 1 <= end_idx)):
try:
parsed_results['groups'][num_groups] = dict_merge(parsed_results['groups'][num_groups], parsed_result)
except IndexError:
parsed_results['groups'].append(parsed_result)
else:
parsed_results = dict_merge(parsed_results, parsed_result)
parsed_response = parsed_response[chunk['len']:]
continue
elif parsed_value == '&&':
delimiter_triggered = True
continue
if parsed_value:
backrefs.append((chunk['format'], parsed_value))
if 'codes' in chunk:
backref_triggered = False
for code_point, description in chunk['codes'].iteritems():
#handle code resolution via simple lookups
if type(code_point) is str:
if (chunk['format'], code_point) in backrefs:
parsed_result[chunk['name']] = description
#handle code resolution via backref lookup
elif type(code_point) is tuple:
backref_key = code_point[0]
#backref counter for multiple lookups
bi = 1
while bi < len(code_point):
backref_value = code_point[bi]
if (backref_key, backref_value) in backrefs:
parsed_result[chunk['name']] = description[backref_value]
backref_triggered = True
break
bi += 1
if backref_triggered:
break
if start_idx and ci > start_idx and ((not end_idx) or (ci - 1 <= end_idx)):
try:
parsed_results['groups'][num_groups] = dict_merge(parsed_results['groups'][num_groups], parsed_result)
except IndexError:
parsed_results['groups'].append(parsed_result)
else:
parsed_results = dict_merge(parsed_results, parsed_result)
parsed_response = parsed_response[chunk['len']:]
#if we're at the end, go back to the start to look for more data
if type(end_idx) is int and ci == end_idx + 1:
ci = start_idx
num_groups += 1
#if only one group was parsed, don't group the results
#todo: fix this upstream so this isn't necessary
if 'groups' in parsed_results and len(parsed_results['groups']) == 1:
parsed_results = dict_merge(parsed_results, parsed_results['groups'][0])
del parsed_results['groups']
return dict(response=response, command_code=command_code, command_data=command_data, type='computer', parsed_results=parsed_results)#, backrefs=backrefs)
#if command is display format
elif command[0] in [u'I', u'S']:
return dict(response=response, command=command, type='display')
return dict(response=response, command=command, type='unknown', parsed_results='!!!UNKNOWN COMMAND TYPE')
def runCommand(self, command):
self.connect()
self.send('%s%s\r\n' % (chr(1), command))
response = self.receive()
self.disconnect()
return self.parseResponse(response, command)
def disconnect(self):
self.sock.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment