Compare commits

...

6 Commits

5 changed files with 420 additions and 150 deletions

View File

@ -7,12 +7,20 @@ channels: ['EX1', 'EX2', 'EX3', 'VEI'] # Specify SOH channels, currently supp
stations_blacklist: ['TEST', 'EREA'] stations_blacklist: ['TEST', 'EREA']
networks_blacklist: [] networks_blacklist: []
interval: 60 # Perform checks every x seconds interval: 60 # Perform checks every x seconds
n_track: 120 # wait number of intervals after FAIL before performing an action (i.e. send mail)
timespan: 3 # Check data of the recent x days timespan: 3 # Check data of the recent x days
verbosity: 0 verbosity: 0
reread_parameters: True # reread parameters file (change parameters on runtime, not for itself/GUI refresh/datapath)
track_changes: True # tracks all changes since GUI startup by text highlighting (GUI only) track_changes: True # tracks all changes since GUI startup by text highlighting (GUI only)
warn_count: False # show number of warnings and errors in table
dt_thresh: [300, 1800] # threshold (s) for timing delay colourisation (yellow/red) dt_thresh: [300, 1800] # threshold (s) for timing delay colourisation (yellow/red)
html_figures: True # Create html figure directory and links html_figures: True # Create html figure directory and links
reread_parameters: True # reread parameters file (change parameters on runtime, not for itself/GUI refresh/datapath)
# add links to html table with specified key as column and value as relative link, interpretable string parameters:
# nw (e.g. 1Y), st (e.g. GR01A), nwst_id (e.g. 1Y.GR01A)
# can also be empty!
add_links:
slmon: {"URL": "{nw}_{st}.html", "text": "show"} # for example: slmon: {"URL": "{nw}_{st}.html", "text": "link"}
POWBOX: POWBOX:
pb_ok: 1 # Voltage for PowBox OK pb_ok: 1 # Voltage for PowBox OK
@ -33,10 +41,17 @@ POWBOX:
4: {"router": "FAIL", "charger": "0 < resets < 3"} 4: {"router": "FAIL", "charger": "0 < resets < 3"}
5: {"router": "FAIL", "charger": "locked"} 5: {"router": "FAIL", "charger": "locked"}
# Thresholds for program warnings/voltage classifications
THRESHOLDS: THRESHOLDS:
pb_thresh: 0.2 # Threshold for PowBox Voltage check +/- (V) pb_thresh: 0.2 # Threshold for PowBox Voltage check +/- (V)
max_temp: 50 # max temperature for temperature warning max_temp: 50 # max temperature for temperature warning
low_volt: 12 # min voltage for low voltage warning low_volt: 12 # min voltage for low voltage warning
high_volt: 14.8 # max voltage for over voltage warning high_volt: 14.8 # max voltage for over voltage warning
unclassified: 5 # min voltage samples not classified for warning unclassified: 5 # min voltage samples not classified for warning
# E-mail notifications
EMAIL:
mailserver: 'localhost'
addresses: ['marcel.paffrath@rub.de', 'kasper.fischer@rub.de'] # list of mail addresses for info mails
sender: 'webmaster@geophysik.ruhr-uni-bochum.de' # mail sender

View File

@ -21,6 +21,14 @@ from write_utils import write_html_text, write_html_row, write_html_footer, writ
init_html_table, finish_html_table init_html_table, finish_html_table
from utils import get_bg_color from utils import get_bg_color
try:
import smtplib
from email.mime.text import MIMEText
mail_functionality = True
except ImportError:
print('Could not import smtplib or mail. Disabled sending mails.')
mail_functionality = False
pjoin = os.path.join pjoin = os.path.join
UP = "\x1B[{length}A" UP = "\x1B[{length}A"
CLR = "\x1B[0K" CLR = "\x1B[0K"
@ -32,12 +40,12 @@ def read_yaml(file_path):
return yaml.safe_load(f) return yaml.safe_load(f)
def nsl_from_id(st_id): def nsl_from_id(nwst_id):
network, station, location = st_id.split('.') network, station, location = nwst_id.split('.')
return dict(network=network, station=station, location=location) return dict(network=network, station=station, location=location)
def get_st_id(trace): def get_nwst_id(trace):
stats = trace.stats stats = trace.stats
return f'{stats.network}.{stats.station}.' # {stats.location}' return f'{stats.network}.{stats.station}.' # {stats.location}'
@ -64,6 +72,7 @@ class SurveillanceBot(object):
self.station_list = [] self.station_list = []
self.analysis_print_list = [] self.analysis_print_list = []
self.analysis_results = {} self.analysis_results = {}
self.status_track = {}
self.dataStream = Stream() self.dataStream = Stream()
self.data = {} self.data = {}
self.print_count = 0 self.print_count = 0
@ -82,6 +91,8 @@ class SurveillanceBot(object):
self.networks_blacklist = self.parameters.get('networks_blacklist') self.networks_blacklist = self.parameters.get('networks_blacklist')
self.refresh_period = self.parameters.get('interval') self.refresh_period = self.parameters.get('interval')
self.transform_parameters() self.transform_parameters()
add_links = self.parameters.get('add_links')
self.add_links = add_links if add_links else {}
def transform_parameters(self): def transform_parameters(self):
for key in ['networks', 'stations', 'locations', 'channels']: for key in ['networks', 'stations', 'locations', 'channels']:
@ -103,8 +114,8 @@ class SurveillanceBot(object):
if self.networks_blacklist and nw in self.networks_blacklist: if self.networks_blacklist and nw in self.networks_blacklist:
continue continue
if (networks == ['*'] or nw in networks) and (stations == ['*'] or st in stations): if (networks == ['*'] or nw in networks) and (stations == ['*'] or st in stations):
st_id = f'{nw}.{st}.' nwst_id = f'{nw}.{st}.'
self.station_list.append(st_id) self.station_list.append(nwst_id)
def get_filenames(self): def get_filenames(self):
self.filenames = [] self.filenames = []
@ -159,10 +170,10 @@ class SurveillanceBot(object):
# organise data in dictionary with key for each station # organise data in dictionary with key for each station
for trace in self.dataStream: for trace in self.dataStream:
st_id = get_st_id(trace) nwst_id = get_nwst_id(trace)
if not st_id in self.data.keys(): if not nwst_id in self.data.keys():
self.data[st_id] = Stream() self.data[nwst_id] = Stream()
self.data[st_id].append(trace) self.data[nwst_id].append(trace)
def execute_qc(self): def execute_qc(self):
if self.reread_parameters: if self.reread_parameters:
@ -173,47 +184,65 @@ class SurveillanceBot(object):
self.analysis_print_list = [] self.analysis_print_list = []
self.analysis_results = {} self.analysis_results = {}
for st_id in sorted(self.station_list): for nwst_id in sorted(self.station_list):
stream = self.data.get(st_id) stream = self.data.get(nwst_id)
if stream: if stream:
nsl = nsl_from_id(st_id) nsl = nsl_from_id(nwst_id)
station_qc = StationQC(stream, nsl, self.parameters, self.keys, qc_starttime, self.verbosity, station_qc = StationQC(stream, nsl, self.parameters, self.keys, qc_starttime,
print_func=self.print) self.verbosity, print_func=self.print,
status_track=self.status_track.get(nwst_id))
analysis_print_result = station_qc.return_print_analysis() analysis_print_result = station_qc.return_print_analysis()
station_dict, warn_dict = station_qc.return_analysis() station_dict = station_qc.return_analysis()
else: else:
analysis_print_result = self.get_no_data_station(st_id, to_print=True) analysis_print_result = self.get_no_data_station(nwst_id, to_print=True)
station_dict, warn_dict = self.get_no_data_station(st_id) station_dict = self.get_no_data_station(nwst_id)
self.analysis_print_list.append(analysis_print_result) self.analysis_print_list.append(analysis_print_result)
self.analysis_results[st_id] = (station_dict, warn_dict) self.analysis_results[nwst_id] = station_dict
self.track_status()
self.update_status_message() self.update_status_message()
return 'ok' return 'ok'
def get_no_data_station(self, st_id, no_data='-', to_print=False): def track_status(self):
delay = self.get_station_delay(st_id) """
tracks error status of the last n_track + 1 errors.
"""
n_track = self.parameters.get('n_track')
if not n_track or n_track < 1:
return
for nwst_id, analysis_dict in self.analysis_results.items():
if not nwst_id in self.status_track.keys():
self.status_track[nwst_id] = {}
for key, status in analysis_dict.items():
if not key in self.status_track[nwst_id].keys():
self.status_track[nwst_id][key] = []
track_lst = self.status_track[nwst_id][key]
# pop list until length is n_track + 1
while len(track_lst) > n_track:
track_lst.pop(0)
track_lst.append(status.is_error)
def get_no_data_station(self, nwst_id, no_data='-', to_print=False):
delay = self.get_station_delay(nwst_id)
if not to_print: if not to_print:
status_dict = {} status_dict = {}
warn_dict = {}
for key in self.keys: for key in self.keys:
if key == 'last active': if key == 'last active':
status_dict[key] = timedelta(seconds=int(delay)) status_dict[key] = Status(message=timedelta(seconds=int(delay)), detailed_messages=['No data'])
warn_dict[key] = 'No data within set timespan'
else: else:
status_dict[key] = no_data status_dict[key] = Status(message=no_data, detailed_messages=['No data'])
warn_dict[key] = 'No data' return status_dict
return status_dict, warn_dict
else: else:
items = [st_id.rstrip('.')] + [fancy_timestr(timedelta(seconds=int(delay)))] items = [nwst_id.rstrip('.')] + [fancy_timestr(timedelta(seconds=int(delay)))]
for _ in range(len(self.keys) - 1): for _ in range(len(self.keys) - 1):
items.append(no_data) items.append(no_data)
return items return items
def get_station_delay(self, st_id): def get_station_delay(self, nwst_id):
""" try to get station delay from SDS archive using client""" """ try to get station delay from SDS archive using client"""
locations = ['', '0', '00'] locations = ['', '0', '00']
channels = ['HHZ', 'HHE', 'HHN', 'VEI', 'EX1', 'EX2', 'EX3'] channels = ['HHZ', 'HHE', 'HHN', 'VEI', 'EX1', 'EX2', 'EX3']
network, station = st_id.split('.')[:2] network, station = nwst_id.split('.')[:2]
times = [] times = []
for channel in channels: for channel in channels:
@ -276,11 +305,11 @@ class SurveillanceBot(object):
self.plot_hour += 1 self.plot_hour += 1
return True return True
def get_fig_path_abs(self, st_id): def get_fig_path_abs(self, nwst_id):
return pjoin(self.outpath_html, self.get_fig_path_rel(st_id)) return pjoin(self.outpath_html, self.get_fig_path_rel(nwst_id))
def get_fig_path_rel(self, st_id, fig_format='png'): def get_fig_path_rel(self, nwst_id, fig_format='png'):
return os.path.join(self.html_fig_dir, f'{st_id.rstrip(".")}.{fig_format}') return os.path.join(self.html_fig_dir, f'{nwst_id.rstrip(".")}.{fig_format}')
def check_fig_dir(self): def check_fig_dir(self):
fdir = pjoin(self.outpath_html, self.html_fig_dir) fdir = pjoin(self.outpath_html, self.html_fig_dir)
@ -297,10 +326,10 @@ class SurveillanceBot(object):
return return
self.check_fig_dir() self.check_fig_dir()
for st_id in self.station_list: for nwst_id in self.station_list:
fig = plt.figure(figsize=(16, 9)) fig = plt.figure(figsize=(16, 9))
fnout = self.get_fig_path_abs(st_id) fnout = self.get_fig_path_abs(nwst_id)
st = self.data.get(st_id) st = self.data.get(nwst_id)
if st: if st:
st.plot(fig=fig, show=False, draw=False, block=False, equal_scale=False, method='full') st.plot(fig=fig, show=False, draw=False, block=False, equal_scale=False, method='full')
ax = fig.axes[0] ax = fig.axes[0]
@ -320,19 +349,25 @@ class SurveillanceBot(object):
init_html_table(outfile) init_html_table(outfile)
# First write header items # First write header items
header = self.keys.copy()
# add columns for additional links
for key in self.add_links:
header.insert(-1, key)
header_items = [dict(text='Station', color=default_color)] header_items = [dict(text='Station', color=default_color)]
for check_key in self.keys: for check_key in header:
item = dict(text=check_key, color=default_color) item = dict(text=check_key, color=default_color)
header_items.append(item) header_items.append(item)
write_html_row(outfile, header_items, html_key='th') write_html_row(outfile, header_items, html_key='th')
# Write all cells # Write all cells
for st_id in self.station_list: for nwst_id in self.station_list:
fig_name = self.get_fig_path_rel(st_id) fig_name = self.get_fig_path_rel(nwst_id)
col_items = [dict(text=st_id.rstrip('.'), color=default_color, image_src=fig_name)] col_items = [dict(text=nwst_id.rstrip('.'), color=default_color, hyperlink=fig_name)]
for check_key in self.keys: for check_key in header:
status_dict, detailed_dict = self.analysis_results.get(st_id) if check_key in self.keys:
status_dict = self.analysis_results.get(nwst_id)
status = status_dict.get(check_key) status = status_dict.get(check_key)
message, detailed_message = status.get_status_str()
# get background color # get background color
dt_thresh = [timedelta(seconds=sec) for sec in self.dt_thresh] dt_thresh = [timedelta(seconds=sec) for sec in self.dt_thresh]
@ -342,12 +377,21 @@ class SurveillanceBot(object):
# add degree sign for temp # add degree sign for temp
if check_key == 'temp': if check_key == 'temp':
if not type(status) in [str]: if not type(message) in [str]:
status = str(status) + deg_str message = str(message) + deg_str
item = dict(text=str(status), tooltip=str(detailed_dict.get(check_key)), item = dict(text=str(message), tooltip=str(detailed_message), color=bg_color)
color=bg_color) elif check_key in self.add_links:
value = self.add_links.get(check_key).get('URL')
link_text = self.add_links.get(check_key).get('text')
if not value:
continue
nw, st = nwst_id.split('.')[:2]
hyperlink_dict = dict(nw=nw, st=st, nwst_id=nwst_id)
link = value.format(**hyperlink_dict)
item = dict(text=link_text, tooltip=link, hyperlink=link, color=default_color)
col_items.append(item) col_items.append(item)
write_html_row(outfile, col_items) write_html_row(outfile, col_items)
finish_html_table(outfile) finish_html_table(outfile)
@ -378,7 +422,7 @@ class SurveillanceBot(object):
class StationQC(object): class StationQC(object):
def __init__(self, stream, nsl, parameters, keys, starttime, verbosity, print_func): def __init__(self, stream, nsl, parameters, keys, starttime, verbosity, print_func, status_track={}):
""" """
Station Quality Check class. Station Quality Check class.
:param nsl: dictionary containing network, station and location (key: str) :param nsl: dictionary containing network, station and location (key: str)
@ -395,51 +439,150 @@ class StationQC(object):
self.last_active = False self.last_active = False
self.print = print_func self.print = print_func
timespan = self.parameters.get('timespan') * 24 * 3600
self.analysis_starttime = self.program_starttime - timespan
self.keys = keys self.keys = keys
self.detailed_status_dict = {key: None for key in self.keys} self.status_dict = {key: Status() for key in self.keys}
self.status_dict = {key: '-' for key in self.keys}
self.activity_check()
self.analyse_channels() if not status_track:
status_track = {}
self.status_track = status_track
def status_ok(self, key, message=None, status_message='OK'): self.start()
self.status_dict[key] = status_message
self.detailed_status_dict[key] = message
def warn(self, key, detailed_message, status_message='WARN'): def status_ok(self, key, detailed_message="Everything OK", status_message='OK', overwrite=False):
# update detailed status if already existing current_status = self.status_dict.get(key)
current_message = self.detailed_status_dict.get(key) # do not overwrite existing warnings or errors
current_message = '' if current_message in [None, '-'] else current_message + ' | ' if not overwrite and (current_status.is_warn or current_status.is_error):
self.detailed_status_dict[key] = current_message + detailed_message return
self.status_dict[key] = StatusOK(message=status_message, detailed_messages=[detailed_message])
# this is becoming a little bit too complicated (adding warnings to existing) def warn(self, key, detailed_message, last_occurrence=None, count=1):
current_status_message = self.status_dict.get(key) if key == 'other':
current_status_message = '' if current_status_message in [None, 'OK', '-'] else current_status_message + ' | ' self.status_other(detailed_message, last_occurrence, count)
self.status_dict[key] = current_status_message + status_message
new_warn = StatusWarn(count=count, show_count=self.parameters.get('warn_count'))
current_status = self.status_dict.get(key)
# change this to something more useful, SMS/EMAIL/PUSH # change this to something more useful, SMS/EMAIL/PUSH
if self.verbosity: if self.verbosity:
self.print(f'{UTCDateTime()}: {detailed_message}', flush=False) self.print(f'{UTCDateTime()}: {detailed_message}', flush=False)
# if error, do not overwrite with warning
if current_status.is_error:
return
if current_status.is_warn:
current_status.count += count
else:
current_status = new_warn
self._update_status(key, current_status, detailed_message, last_occurrence)
# warnings.warn(message) # warnings.warn(message)
def error(self, key, message): # # update detailed status if already existing
self.detailed_status_dict[key] = message # current_message = self.detailed_status_dict.get(key)
self.status_dict[key] = 'FAIL' # current_message = '' if current_message in [None, '-'] else current_message + ' | '
# change this to something more useful, SMS/EMAIL/PUSH # self.detailed_status_dict[key] = current_message + detailed_message
#
# # this is becoming a little bit too complicated (adding warnings to existing)
# current_status_message = self.status_dict.get(key)
# current_status_message = '' if current_status_message in [None, 'OK', '-'] else current_status_message + ' | '
# self.status_dict[key] = current_status_message + status_message
def error(self, key, detailed_message, last_occurrence=None, count=1):
new_error = StatusError(count=count, show_count=self.parameters.get('warn_count'))
current_status = self.status_dict.get(key)
if current_status.is_error:
current_status.count += count
else:
current_status = new_error
self._update_status(key, current_status, detailed_message, last_occurrence)
if self.verbosity: if self.verbosity:
self.print(f'{UTCDateTime()}: {message}', flush=False) self.print(f'{UTCDateTime()}: {detailed_message}', flush=False)
# warnings.warn(message)
# do not send error mail if this is the first run (e.g. program startup) or state was already error (unchanged)
if self.search_previous_errors(key):
self.send_mail(key, detailed_message)
def search_previous_errors(self, key):
"""
Check n_track + 1 previous statuses for errors.
If first item in list is no error but all others are return True (first time n_track errors appeared --
if ALL n_track + 1 are error: error is old)
In all other cases return True.
This also prevents sending status (e.g. mail) in case of program startup
"""
previous_errors = self.status_track.get(key)
# only if error list is filled n_track times
if previous_errors and len(previous_errors) == self.parameters.get('n_track') + 1:
# if first entry was no error but all others are, return True (-> new Fail n_track times)
if not previous_errors[0] and all(previous_errors[1:]):
return True
else:
return False
def send_mail(self, key, message):
""" Send info mail using parameters specified in parameters file """
if not mail_functionality:
if self.verbosity:
print('Mail functionality disabled. Return')
return
mail_params = self.parameters.get('EMAIL')
if not mail_params:
if self.verbosity:
print('parameter "EMAIL" not set in parameter file. Return')
return
sender = mail_params.get('sender')
addresses = mail_params.get('addresses')
server = mail_params.get('mailserver')
if not sender or not addresses:
if self.verbosity:
print('Mail sender or addresses not correctly defined. Return')
return
n_track = self.parameters.get('n_track')
interval = self.parameters.get('interval')
dt = timedelta(seconds=n_track * interval)
text = f'{key} FAIL status longer than {dt}: ' + message
msg = MIMEText(text)
msg['Subject'] = f'new FAIL status on station {self.network}.{self.station}'
msg['From'] = sender
msg['To'] = ', '.join(addresses)
# send message via SMTP server
s = smtplib.SMTP(server)
s.sendmail(sender, addresses, msg.as_string())
s.quit()
def status_other(self, detailed_message, status_message, last_occurrence=None, count=1):
key = 'other'
new_status = StatusOther(count=count, messages=[status_message])
current_status = self.status_dict.get(key)
if current_status.is_other:
current_status.count += count
current_status.messages.append(status_message)
else:
current_status = new_status
self._update_status(key, current_status, detailed_message, last_occurrence)
def _update_status(self, key, current_status, detailed_message, last_occurrence):
current_status.detailed_messages.append(detailed_message)
current_status.last_occurrence = last_occurrence
self.status_dict[key] = current_status
def activity_check(self): def activity_check(self):
self.last_active = self.last_activity() self.last_active = self.last_activity()
if not self.last_active: if not self.last_active:
message = 'FAIL' status = StatusError()
else: else:
message = timedelta(seconds=int(self.program_starttime - self.last_active)) message = timedelta(seconds=int(self.program_starttime - self.last_active))
self.status_dict['last active'] = message status = Status(message=message)
self.status_dict['last active'] = status
def last_activity(self): def last_activity(self):
if not self.stream: if not self.stream:
@ -450,11 +593,18 @@ class StationQC(object):
if len(endtimes) > 0: if len(endtimes) > 0:
return max(endtimes) return max(endtimes)
def start(self):
self.analyse_channels()
def analyse_channels(self): def analyse_channels(self):
timespan = self.parameters.get('timespan') * 24 * 3600
self.analysis_starttime = self.program_starttime - timespan
if self.verbosity > 0: if self.verbosity > 0:
self.print(150 * '#') self.print(150 * '#')
self.print('This is StationQT. Calculating quality for station' self.print('This is StationQT. Calculating quality for station'
' {network}.{station}.{location}'.format(**self.nsl)) ' {network}.{station}.{location}'.format(**self.nsl))
self.activity_check()
self.voltage_analysis() self.voltage_analysis()
self.pb_temp_analysis() self.pb_temp_analysis()
self.pb_power_analysis() self.pb_power_analysis()
@ -463,26 +613,30 @@ class StationQC(object):
def return_print_analysis(self): def return_print_analysis(self):
items = [f'{self.network}.{self.station}'] items = [f'{self.network}.{self.station}']
for key in self.keys: for key in self.keys:
item = self.status_dict[key] status = self.status_dict[key]
message = status.message
if key == 'last active': if key == 'last active':
items.append(fancy_timestr(item)) items.append(fancy_timestr(message))
elif key == 'temp': elif key == 'temp':
items.append(str(item) + deg_str) items.append(str(message) + deg_str)
else: else:
items.append(str(item)) items.append(str(message))
return items return items
def return_analysis(self): def return_analysis(self):
return self.status_dict, self.detailed_status_dict return self.status_dict
def get_last_occurrence_timestring(self, trace, indices): def get_last_occurrence_timestring(self, trace, indices):
""" returns a nicely formatted string of the timedelta since program starttime and occurrence and abs time""" """ returns a nicely formatted string of the timedelta since program starttime and occurrence and abs time"""
last_occur = self.get_time(trace, indices[-1]) last_occur = self.get_last_occurrence(trace, indices)
if not last_occur: if not last_occur:
return '' return ''
last_occur_dt = timedelta(seconds=int(self.program_starttime - last_occur)) last_occur_dt = timedelta(seconds=int(self.program_starttime - last_occur))
return f', Last occurrence: {last_occur_dt} ({last_occur.strftime("%Y-%m-%d %H:%M:%S")})' return f', Last occurrence: {last_occur_dt} ({last_occur.strftime("%Y-%m-%d %H:%M:%S")})'
def get_last_occurrence(self, trace, indices):
return self.get_time(trace, indices[-1])
def voltage_analysis(self, channel='VEI'): def voltage_analysis(self, channel='VEI'):
""" Analyse voltage channel for over/undervoltage """ """ Analyse voltage channel for over/undervoltage """
key = 'voltage' key = 'voltage'
@ -501,7 +655,7 @@ class StationQC(object):
undervolt = np.where(voltage < low_volt)[0] undervolt = np.where(voltage < low_volt)[0]
if len(overvolt) == 0 and len(undervolt) == 0: if len(overvolt) == 0 and len(undervolt) == 0:
self.status_ok(key, message=f'U={(voltage[-1])}V') self.status_ok(key, detailed_message=f'U={(voltage[-1])}V')
return return
n_overvolt = 0 n_overvolt = 0
@ -511,14 +665,19 @@ class StationQC(object):
if len(overvolt) > 0: if len(overvolt) > 0:
# try calculate number of voltage peaks from gaps between indices # try calculate number of voltage peaks from gaps between indices
n_overvolt = len(np.where(np.diff(overvolt) > 1)[0]) + 1 n_overvolt = len(np.where(np.diff(overvolt) > 1)[0]) + 1
warn_message += f' {n_overvolt}x Voltage over {high_volt}V' \ detailed_message = warn_message + f' {n_overvolt}x Voltage over {high_volt}V' \
+ self.get_last_occurrence_timestring(trace, overvolt) + self.get_last_occurrence_timestring(trace, overvolt)
self.warn(key, detailed_message=detailed_message, count=n_overvolt,
last_occurrence=self.get_last_occurrence(trace, overvolt))
if len(undervolt) > 0: if len(undervolt) > 0:
# try calculate number of voltage peaks from gaps between indices # try calculate number of voltage peaks from gaps between indices
n_undervolt = len(np.where(np.diff(undervolt) > 1)[0]) + 1 n_undervolt = len(np.where(np.diff(undervolt) > 1)[0]) + 1
warn_message += f' {n_undervolt}x Voltage under {low_volt}V ' \ detailed_message = warn_message + f' {n_undervolt}x Voltage under {low_volt}V '\
+ self.get_last_occurrence_timestring(trace, undervolt) + self.get_last_occurrence_timestring(trace, undervolt)
self.warn(key, detailed_message=warn_message, status_message='WARN ({})'.format(n_overvolt + n_undervolt)) self.warn(key, detailed_message=detailed_message, count=n_undervolt,
last_occurrence=self.get_last_occurrence(trace, undervolt))
def pb_temp_analysis(self, channel='EX1'): def pb_temp_analysis(self, channel='EX1'):
""" Analyse PowBox temperature output. """ """ Analyse PowBox temperature output. """
@ -547,14 +706,14 @@ class StationQC(object):
t_check = np.where(temp > max_temp)[0] t_check = np.where(temp > max_temp)[0]
if len(t_check) > 0: if len(t_check) > 0:
self.warn(key=key, self.warn(key=key,
status_message=cur_temp,
detailed_message=f'Trace {trace.get_id()}: ' detailed_message=f'Trace {trace.get_id()}: '
f'Temperature over {max_temp}\N{DEGREE SIGN} at {trace.get_id()}!' f'Temperature over {max_temp}\N{DEGREE SIGN} at {trace.get_id()}!'
+ self.get_last_occurrence_timestring(trace, t_check)) + self.get_last_occurrence_timestring(trace, t_check),
last_occurrence=self.get_last_occurrence(trace, t_check))
else: else:
self.status_ok(key, self.status_ok(key,
status_message=cur_temp, status_message=cur_temp,
message=f'Average temperature of last {dt_t_str}: {av_temp_str}') detailed_message=f'Average temperature of last {dt_t_str}: {av_temp_str}')
def pb_power_analysis(self, channel='EX2', pb_dict_key='pb_SOH2'): def pb_power_analysis(self, channel='EX2', pb_dict_key='pb_SOH2'):
""" Analyse EX2 channel of PowBox """ """ Analyse EX2 channel of PowBox """
@ -604,22 +763,28 @@ class StationQC(object):
def in_depth_voltage_check(self, trace, voltage_dict, soh_params, last_val): def in_depth_voltage_check(self, trace, voltage_dict, soh_params, last_val):
""" Associate values in voltage_dict to error messages specified in SOH_params and warn.""" """ Associate values in voltage_dict to error messages specified in SOH_params and warn."""
for volt_lvl, ind_array in voltage_dict.items(): for volt_lvl, ind_array in voltage_dict.items():
if volt_lvl == 1: continue # No need to do anything here if volt_lvl == 1:
continue # No need to do anything here
if len(ind_array) > 0: if len(ind_array) > 0:
# get result from parameter dictionary for voltage level
result = soh_params.get(volt_lvl) result = soh_params.get(volt_lvl)
for key, message in result.items(): for key, message in result.items():
# if result is OK, continue with next voltage level
if message == 'OK': if message == 'OK':
self.status_ok(key) self.status_ok(key)
continue continue
if volt_lvl > 1:
# try calculate number of voltage peaks from gaps between indices # try calculate number of voltage peaks from gaps between indices
n_occurrences = len(np.where(np.diff(ind_array) > 1)[0]) + 1 n_occurrences = len(np.where(np.diff(ind_array) > 1)[0]) + 1
self.warn(key=key, self.warn(key=key,
detailed_message=f'Trace {trace.get_id()}: ' detailed_message=f'Trace {trace.get_id()}: '
f'Found {n_occurrences} occurrence(s) of {volt_lvl}V: {key}: {message}' f'Found {n_occurrences} occurrence(s) of {volt_lvl}V: {key}: {message}'
+ self.get_last_occurrence_timestring(trace, ind_array), + self.get_last_occurrence_timestring(trace, ind_array),
status_message='WARN ({})'.format(n_occurrences)) count=n_occurrences,
if last_val != 1: last_occurrence=self.get_last_occurrence(trace, ind_array))
self.error(key, message=f'Last PowBox voltage state {last_val}V: {message}') # if last_val == current voltage (which is not 1) -> FAIL or last_val < 1: PBox no data
if volt_lvl == last_val or (volt_lvl == -1 and last_val < 1):
self.error(key, detailed_message=f'Last PowBox voltage state {last_val}V: {message}')
def get_trace(self, stream, keys): def get_trace(self, stream, keys):
if not type(keys) == list: if not type(keys) == list:
@ -673,8 +838,7 @@ class StationQC(object):
# try calculate number of occurences from gaps between indices # try calculate number of occurences from gaps between indices
n_occurrences = len(np.where(np.diff(under) > 1)[0]) + 1 n_occurrences = len(np.where(np.diff(under) > 1)[0]) + 1
voltage_dict[-1] = under voltage_dict[-1] = under
self.warn(key='other', self.status_other(detailed_message=f'Trace {trace.get_id()}: '
detailed_message=f'Trace {trace.get_id()}: '
f'Voltage below {pb_ok}V in {len(under)} samples, {n_occurrences} time(s). ' f'Voltage below {pb_ok}V in {len(under)} samples, {n_occurrences} time(s). '
f'Mean voltage: {np.mean(voltage):.2}' f'Mean voltage: {np.mean(voltage):.2}'
+ self.get_last_occurrence_timestring(trace, under), + self.get_last_occurrence_timestring(trace, under),
@ -695,7 +859,7 @@ class StationQC(object):
n_unclassified = len(unclassified_indices) n_unclassified = len(unclassified_indices)
max_uncl = self.parameters.get('THRESHOLDS').get('unclassified') max_uncl = self.parameters.get('THRESHOLDS').get('unclassified')
if max_uncl and n_unclassified > max_uncl: if max_uncl and n_unclassified > max_uncl:
self.warn(key='other', detailed_message=f'Trace {trace.get_id()}: ' self.status_other(detailed_message=f'Trace {trace.get_id()}: '
f'{n_unclassified}/{len(all_indices)} ' f'{n_unclassified}/{len(all_indices)} '
f'unclassified voltage values in channel {trace.get_id()}', f'unclassified voltage values in channel {trace.get_id()}',
status_message=f'{channel}: {n_unclassified} uncl.') status_message=f'{channel}: {n_unclassified} uncl.')
@ -707,6 +871,93 @@ class StationQC(object):
return trace.stats.starttime + trace.stats.delta * index return trace.stats.starttime + trace.stats.delta * index
class Status(object):
def __init__(self, message='-', detailed_messages=None, count: int = 0, last_occurrence=None, show_count=True):
if detailed_messages is None:
detailed_messages = []
self.show_count = show_count
self.message = message
self.messages = [message]
self.detailed_messages = detailed_messages
self.count = count
self.last_occurrence = last_occurrence
self.is_warn = None
self.is_error = None
self.is_other = False
def set_warn(self):
self.is_warn = True
def set_error(self):
self.is_warn = False
self.is_error = True
def set_ok(self):
self.is_warn = False
self.is_error = False
def get_status_str(self):
message = self.message
if self.count > 1 and self.show_count:
message += f' ({self.count})'
detailed_message = ''
for index, dm in enumerate(self.detailed_messages):
if index > 0:
detailed_message += ' | '
detailed_message += dm
return message, detailed_message
class StatusOK(Status):
def __init__(self, message='OK', detailed_messages=None):
super(StatusOK, self).__init__(message=message, detailed_messages=detailed_messages)
self.set_ok()
class StatusWarn(Status):
def __init__(self, message='WARN', count=1, last_occurence=None, detailed_messages=None, show_count=False):
super(StatusWarn, self).__init__(message=message, count=count, last_occurrence=last_occurence,
detailed_messages=detailed_messages, show_count=show_count)
self.set_warn()
class StatusError(Status):
def __init__(self, message='FAIL', count=1, last_occurence=None, detailed_messages=None, show_count=False):
super(StatusError, self).__init__(message=message, count=count, last_occurrence=last_occurence,
detailed_messages=detailed_messages, show_count=show_count)
self.set_error()
class StatusOther(Status):
def __init__(self, messages=None, count=1, last_occurence=None, detailed_messages=None):
super(StatusOther, self).__init__(count=count, last_occurrence=last_occurence,
detailed_messages=detailed_messages)
if messages is None:
messages = []
self.messages = messages
self.is_other = True
def get_status_str(self):
if self.messages == []:
return '-'
message = ''
for index, mes in enumerate(self.messages):
if index > 0:
message += ' | '
message += mes
detailed_message = ''
for index, dm in enumerate(self.detailed_messages):
if index > 0:
detailed_message += ' | '
detailed_message += dm
return message, detailed_message
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Call survBot') parser = argparse.ArgumentParser(description='Call survBot')
parser.add_argument('-html', dest='html_path', default=None, help='filepath for HTML output') parser.add_argument('-html', dest='html_path', default=None, help='filepath for HTML output')

View File

@ -130,10 +130,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.table.setRowCount(len(station_list)) self.table.setRowCount(len(station_list))
self.table.setHorizontalHeaderLabels(keys) self.table.setHorizontalHeaderLabels(keys)
for index, st_id in enumerate(station_list): for index, nwst_id in enumerate(station_list):
item = QtWidgets.QTableWidgetItem() item = QtWidgets.QTableWidgetItem()
item.setText(str(st_id.rstrip('.'))) item.setText(str(nwst_id.rstrip('.')))
item.setData(QtCore.Qt.UserRole, st_id) item.setData(QtCore.Qt.UserRole, nwst_id)
self.table.setVerticalHeaderItem(index, item) self.table.setVerticalHeaderItem(index, item)
self.main_layout.addWidget(self.table) self.main_layout.addWidget(self.table)
@ -180,38 +180,38 @@ class MainWindow(QtWidgets.QMainWindow):
header_item = self.table.verticalHeaderItem(row_ind) header_item = self.table.verticalHeaderItem(row_ind)
if not header_item: if not header_item:
return return
st_id = header_item.data(QtCore.Qt.UserRole) nwst_id = header_item.data(QtCore.Qt.UserRole)
context_menu = QtWidgets.QMenu() context_menu = QtWidgets.QMenu()
read_sms = context_menu.addAction('Get last SMS') read_sms = context_menu.addAction('Get last SMS')
send_sms = context_menu.addAction('Send SMS') send_sms = context_menu.addAction('Send SMS')
action = context_menu.exec_(self.mapToGlobal(self.last_mouse_loc)) action = context_menu.exec_(self.mapToGlobal(self.last_mouse_loc))
if action == read_sms: if action == read_sms:
self.read_sms(st_id) self.read_sms(nwst_id)
elif action == send_sms: elif action == send_sms:
self.send_sms(st_id) self.send_sms(nwst_id)
def read_sms(self, st_id): def read_sms(self, nwst_id):
""" Read recent SMS over rest_api using whereversim portal """ """ Read recent SMS over rest_api using whereversim portal """
station = st_id.split('.')[1] station = nwst_id.split('.')[1]
iccid = get_station_iccid(station) iccid = get_station_iccid(station)
if not iccid: if not iccid:
print('Could not find iccid for station', st_id) print('Could not find iccid for station', nwst_id)
return return
sms_widget = ReadSMSWidget(parent=self, iccid=iccid) sms_widget = ReadSMSWidget(parent=self, iccid=iccid)
sms_widget.setWindowTitle(f'Recent SMS of station: {st_id}') sms_widget.setWindowTitle(f'Recent SMS of station: {nwst_id}')
if sms_widget.data: if sms_widget.data:
sms_widget.show() sms_widget.show()
else: else:
self.notification('No recent messages found.') self.notification('No recent messages found.')
def send_sms(self, st_id): def send_sms(self, nwst_id):
""" Send SMS over rest_api using whereversim portal """ """ Send SMS over rest_api using whereversim portal """
station = st_id.split('.')[1] station = nwst_id.split('.')[1]
iccid = get_station_iccid(station) iccid = get_station_iccid(station)
sms_widget = SendSMSWidget(parent=self, iccid=iccid) sms_widget = SendSMSWidget(parent=self, iccid=iccid)
sms_widget.setWindowTitle(f'Send SMS to station: {st_id}') sms_widget.setWindowTitle(f'Send SMS to station: {nwst_id}')
sms_widget.show() sms_widget.show()
def set_clear_on_refresh(self): def set_clear_on_refresh(self):
@ -230,19 +230,19 @@ class MainWindow(QtWidgets.QMainWindow):
self.fill_status_bar() self.fill_status_bar()
for col_ind, check_key in enumerate(self.survBot.keys): for col_ind, check_key in enumerate(self.survBot.keys):
for row_ind, st_id in enumerate(self.survBot.station_list): for row_ind, nwst_id in enumerate(self.survBot.station_list):
status_dict, detailed_dict = self.survBot.analysis_results.get(st_id) status_dict = self.survBot.analysis_results.get(nwst_id)
status = status_dict.get(check_key) status = status_dict.get(check_key)
detailed_message = detailed_dict.get(check_key) message, detailed_message = status.get_status_str()
dt_thresh = [timedelta(seconds=sec) for sec in self.dt_thresh] dt_thresh = [timedelta(seconds=sec) for sec in self.dt_thresh]
bg_color = get_bg_color(check_key, status, dt_thresh) bg_color = get_bg_color(check_key, status, dt_thresh)
if check_key == 'temp': if check_key == 'temp':
if not type(status) in [str]: if not type(message) in [str]:
status = str(status) + deg_str message = str(message) + deg_str
# Continue if nothing changed # Continue if nothing changed
text = str(status) text = str(message)
cur_item = self.table.item(row_ind, col_ind) cur_item = self.table.item(row_ind, col_ind)
if cur_item and text == cur_item.text(): if cur_item and text == cur_item.text():
if not self.parameters.get('track_changes') or self.clear_on_refresh: if not self.parameters.get('track_changes') or self.clear_on_refresh:
@ -253,9 +253,9 @@ class MainWindow(QtWidgets.QMainWindow):
# Create new data item # Create new data item
item = QtWidgets.QTableWidgetItem() item = QtWidgets.QTableWidgetItem()
item.setText(str(status)) item.setText(str(message))
item.setTextAlignment(QtCore.Qt.AlignCenter) item.setTextAlignment(QtCore.Qt.AlignCenter)
item.setData(QtCore.Qt.UserRole, (st_id, check_key)) item.setData(QtCore.Qt.UserRole, (nwst_id, check_key))
# if text changed (known from above) set highlight color/font else (new init) set to default # if text changed (known from above) set highlight color/font else (new init) set to default
cur_item = self.table.item(row_ind, col_ind) cur_item = self.table.item(row_ind, col_ind)
@ -310,11 +310,11 @@ class MainWindow(QtWidgets.QMainWindow):
vheader.setSectionResizeMode(index, QtWidgets.QHeaderView.Stretch) vheader.setSectionResizeMode(index, QtWidgets.QHeaderView.Stretch)
def plot_stream(self, item): def plot_stream(self, item):
st_id, check = item.data(QtCore.Qt.UserRole) nwst_id, check = item.data(QtCore.Qt.UserRole)
st = self.survBot.data.get(st_id) st = self.survBot.data.get(nwst_id)
if st: if st:
self.plot_widget = PlotWidget(self) self.plot_widget = PlotWidget(self)
self.plot_widget.setWindowTitle(st_id) self.plot_widget.setWindowTitle(nwst_id)
st.plot(equal_scale=False, method='full', block=False, fig=self.plot_widget.canvas.fig) st.plot(equal_scale=False, method='full', block=False, fig=self.plot_widget.canvas.fig)
self.plot_widget.show() self.plot_widget.show()

View File

@ -4,17 +4,18 @@
import matplotlib import matplotlib
def get_bg_color(check_key, status, dt_thresh=None, hex=False): def get_bg_color(check_key, status, dt_thresh=None, hex=False):
message = status.message
if check_key == 'last active': if check_key == 'last active':
bg_color = get_time_delay_color(status, dt_thresh) bg_color = get_time_delay_color(message, dt_thresh)
elif check_key == 'temp': elif check_key == 'temp':
bg_color = get_temp_color(status) bg_color = get_temp_color(message)
else: else:
statussplit = status.split(' ') if status.is_warn:
if len(statussplit) > 1 and statussplit[0] == 'WARN': bg_color = get_color('WARNX')(status.count)
x = int(status.split(' ')[-1].lstrip('(').rstrip(')')) elif status.is_error:
bg_color = get_color('WARNX')(x) bg_color = get_color('FAIL')
else: else:
bg_color = get_color(status) bg_color = get_color(message)
if not bg_color: if not bg_color:
bg_color = get_color('undefined') bg_color = get_color('undefined')
@ -26,8 +27,8 @@ def get_color(key):
# some GUI default colors # some GUI default colors
colors_dict = {'FAIL': (255, 50, 0, 255), colors_dict = {'FAIL': (255, 50, 0, 255),
'NO DATA': (255, 255, 125, 255), 'NO DATA': (255, 255, 125, 255),
'WARN': (255, 255, 125, 255), 'WARN': (255, 255, 80, 255),
'WARNX': lambda x: (min([255, 200 + x ** 2]), 255, 125, 255), 'WARNX': lambda x: (min([255, 200 + x ** 2]), 255, 80, 255),
'OK': (125, 255, 125, 255), 'OK': (125, 255, 125, 255),
'undefined': (230, 230, 230, 255)} 'undefined': (230, 230, 230, 255)}
return colors_dict.get(key) return colors_dict.get(key)

View File

@ -10,6 +10,9 @@ def write_html_text(fobj, text):
def write_html_header(fobj, refresh_rate=10): def write_html_header(fobj, refresh_rate=10):
header = ['<!DOCTYPE html>', header = ['<!DOCTYPE html>',
'<html>', '<html>',
'<head>',
'<link rel="stylesheet" href="stylesheet.css">',
'</head>',
f'<meta http-equiv="refresh" content="{refresh_rate}" >', f'<meta http-equiv="refresh" content="{refresh_rate}" >',
'<meta charset="utf-8">', '<meta charset="utf-8">',
'<body>'] '<body>']
@ -42,8 +45,8 @@ def write_html_row(fobj, items, html_key='td'):
color = item.get('color') color = item.get('color')
# check for black background of headers (shouldnt happen anymore) # check for black background of headers (shouldnt happen anymore)
color = '#e6e6e6' if color == '#000000' else color color = '#e6e6e6' if color == '#000000' else color
image_src = item.get('image_src') hyperlink = item.get('hyperlink')
image_str = f'<a href="{image_src}">' if image_src else '' image_str = f'<a href="{hyperlink}">' if hyperlink else ''
fobj.write(2 * default_space + f'<{html_key} bgcolor="{color}" title="{tooltip}"> {image_str}' fobj.write(2 * default_space + f'<{html_key} bgcolor="{color}" title="{tooltip}"> {image_str}'
+ text + f'</{html_key}>\n') + text + f'</{html_key}>\n')
fobj.write(default_space + '</tr>\n') fobj.write(default_space + '</tr>\n')