36 Commits

Author SHA1 Message Date
c8c3aff2fb Merge remote-tracking branch 'origin/issue/1' into issue/1 2022-12-20 12:23:23 +01:00
f2e322230e fixed paramter file 2022-12-20 12:19:32 +01:00
735abac249 use function calc_occurences 2022-12-20 12:19:05 +01:00
541815d81f Add quality check for clock quality (LCQ) 2022-12-20 12:19:05 +01:00
47c3fbabf0 Merge pull request 'release/1.0' (#3) from release/1.0 into main
Reviewed-on: #3
2022-12-20 12:05:45 +01:00
91fa60851d Merge branch 'develop' into issue/1 2022-12-20 11:39:53 +01:00
2a62fe406f Merge branch 'issue/1' into develop 2022-12-20 11:37:58 +01:00
d397ce377e [update] add network/station blacklists for mail functionality 2022-12-20 10:23:25 +01:00
8690a50899 Merge remote-tracking branch 'origin/develop' into issue/1 2022-12-08 16:02:36 +01:00
d764c5c256 Merge remote-tracking branch 'origin/develop' into develop 2022-12-08 13:23:45 +01:00
a56781dca3 [minor] renamed and added stylesheets to git repository 2022-12-08 13:23:21 +01:00
ac9f83d856 fixed paramter file 2022-12-06 16:46:19 +01:00
124c4413e1 Merge branch 'develop' into issue/1 2022-12-06 16:34:36 +01:00
fc64239c88 don't analysis values of data gaps when cheching for thresholds.
use stream.merge(fill_value=np.nan)
2022-12-06 16:34:08 +01:00
69412dc5fe use function calc_occurences 2022-12-06 15:51:24 +01:00
ac1ce5f6fa Merge branch 'develop' into issue/1 2022-12-06 15:47:56 +01:00
cb3623e4a9 Add quality check for clock quality (LCQ) 2022-12-06 15:43:13 +01:00
f0ae7da2be [update] add min_sample parameter, which controls ne number of samples that have to be of a specific voltage state before counting them as fail-state 2022-12-06 15:31:09 +01:00
a30cd8c0d4 [update] add trace axis ticking/min-max 2022-12-02 11:15:37 +01:00
a3378874fa [minor] parameter change 2022-12-02 11:15:01 +01:00
d21fb0ca3b [minor] parameter change 2022-11-29 10:42:29 +01:00
19b8df8f7d [minor] add html class for mobile (WIP) 2022-11-29 10:42:15 +01:00
6fc1e073c0 [bugfix] compared timedelta with int 2022-11-24 10:13:21 +01:00
56351ee700 [update] send message (mail) on station timeout 2022-11-23 11:52:26 +01:00
f45c5b20c5 [bugfix] set error state to active until mail is sent 2022-11-23 11:29:05 +01:00
7a2b7add04 [minor] parameter changes 2022-11-23 11:28:36 +01:00
ae0c2ef4e9 [minor] track activity status, modify html output for stylesheet 2022-11-22 18:06:25 +01:00
d35c176aab [update] add channel naming for plots 2022-11-22 15:51:15 +01:00
9444405453 [minor] reformat yaml file 2022-11-22 13:34:58 +01:00
a6d59c8c71 [update] added possibility to modify voltage level in plots from settings in parameters.yaml 2022-11-22 12:07:16 +01:00
3fe5fc48d1 [minor] re-ordered parameters 2022-11-21 10:36:46 +01:00
7da3db260a [update] error tracking + send email functionality 2022-11-17 09:52:04 +01:00
2c1e923920 [minor] add stylesheet to html header 2022-11-16 11:24:48 +01:00
8e42ac11c7 [update] complete rework of status handling (added Warn/Error classes etc.) 2022-11-15 17:19:39 +01:00
4d4324a1e9 [refactor] st_id -> nwst_id 2022-11-15 13:48:56 +01:00
4ba9c20d0f [update] add possibility to add columns with links to other web pages (e.g. seedlink monitor) 2022-11-15 13:44:19 +01:00
8 changed files with 827 additions and 199 deletions

View File

@@ -32,6 +32,8 @@ The main program with html output is executed by entering
python survBot.py -html path_for_html_output python survBot.py -html path_for_html_output
``` ```
There are example stylesheets in the folder *stylesheets* that can be copied into the path_for_html_output if desired.
The GUI can be loaded via The GUI can be loaded via
```shell script ```shell script

View File

@@ -1,24 +1,27 @@
# Parameters file for Surveillance Bot # Parameters file for Surveillance Bot
datapath: '/data/SDS/' # SC3 Datapath datapath: "/data/SDS/" # SC3 Datapath
networks: ['1Y', 'HA'] networks: ["1Y", "HA"] # select networks, list or str
stations: '*' stations: "*" # select stations, list or str
locations: '*' locations: "*" # select locations, list or str
channels: ['EX1', 'EX2', 'EX3', 'VEI'] # Specify SOH channels, currently supported EX[1-3] and VEI channels: ["EX1", "EX2", "EX3", "VEI", "LCQ"] # Specify SOH channels, currently supported EX[1-3], VEI and LCQ
stations_blacklist: ['TEST', 'EREA'] stations_blacklist: ["TEST", "EREA"] # exclude these stations
networks_blacklist: [] networks_blacklist: [] # exclude these networks
interval: 60 # Perform checks every x seconds interval: 60 # Perform checks every x seconds
timespan: 3 # Check data of the recent x days n_track: 300 # wait n_track * intervals before performing an action (i.e. send mail/end highlight status)
verbosity: 0 timespan: 7 # Check data of the recent x days
reread_parameters: True # reread parameters file (change parameters on runtime, not for itself/GUI refresh/datapath) verbosity: 0 # verbosity flag
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
min_sample: 3 # minimum samples for raising Warn/FAIL
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)
POWBOX: POWBOX:
pb_ok: 1 # Voltage for PowBox OK pb_ok: 1 # Voltage for PowBox OK
pb_SOH2: # PowBox channel 2 voltage translations pb_SOH2: # PowBox channel 2 voltage translations
-1: {"230V": "PBox under 1V", "12V": "PBox under 1V"} -1: {"230V": "PBox under 1V", "12V": "PBox under 1V"}
1: {"230V": 'OK', "12V": "OK"} 1: {"230V": "OK", "12V": "OK"}
2: {"230V": "OFF", "12V": "OK"} 2: {"230V": "OFF", "12V": "OK"}
3: {"230V": "OK", "12V": "overvoltage"} 3: {"230V": "OK", "12V": "overvoltage"}
4: {"230V": "OK", "12V": "undervoltage"} 4: {"230V": "OK", "12V": "undervoltage"}
@@ -33,10 +36,52 @@ 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
clockquality_warn: 90 # clock quality ranges from 0 % to 100 % with 100 % being the best level
clockquality_fail: 70
# ---------------------------------------- OPTIONAL PARAMETERS ---------------------------------------------------------
# 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)
add_links:
# for example: slmon: {"URL": "path/{nw}_{st}.html", "text": "link"}
slmon: {"URL": "../slmon/{nw}_{st}.html", "text": "show"}
24h-plot: {"URL": "../scheli/{nw}/{st}.png", "text": "plot"}
# 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
stations_blacklist: ['GR33'] # do not send emails for specific stations
networks_blacklist: [] # do not send emails for specific network
# names for plotting of the above defined parameter "channels" in the same order
channel_names: ["Clock Quality (%)", "Temperature (°C)", "230V/12V Status (V)", "Router/Charger State (V)", "Logger Voltage (V)"] # names for plotting (optional)
# specify y-ticks (and ylims) giving, (ymin, ymax, step) for each of the above channels (0: default)
CHANNEL_TICKS:
- [0, 100, 20]
- [-10, 50, 10]
- [1, 5, 1]
- [1, 5, 1]
- [9, 15, 1]
# Factor for channel to SI-units (for plotting)
CHANNEL_UNITS:
EX1: 1e-6
EX2: 1e-6
EX3: 1e-6
VEI: 1e-3
# Transform channel for plotting, perform arithmetic operations in given order, e.g.: PBox EX1 V to deg C: 20 * x -20
CHANNEL_TRANSFORM:
EX1:
- ["*", 20]
- ["-", 20]

39
stylesheets/desktop.css Normal file
View File

@@ -0,0 +1,39 @@
body {
background-color: #ffffff;
place-items: center;
text-align: center;
}
td {
border-radius: 4px;
padding: 0px;
}
th {
background-color: #999;
border-radius: 4px;
padding: 3px 1px;
}
a:link, a:visited {
background-color: #ccc;
color: #000;
text-decoration: none;
display: block;
border-radius: 4px;
border: 1px solid #bbb;
}
a:hover {
background-color: #aaa;
display: block;
}
.blink-bg {
animation: blinkingBackground 2s infinite;
}
@keyframes blinkingBackground{
0% { background-color: #ffcc00;}
50% { background-color: #ff3200;}
100% { background-color: #ffcc00;}
}

43
stylesheets/mobile.css Normal file
View File

@@ -0,0 +1,43 @@
body {
background-color: #ffffff;
place-items: center;
text-align: center;
}
td {
border-radius: 4px;
padding: 10px 2px;
}
th {
background-color: #999;
border-radius: 4px;
padding: 10px, 2px;
}
a:link {
background-color: #ccc;
color: #000;
text-decoration: none;
display: block;
border-radius: 4px;
border: 1px solid #bbb;
}
a:hover {
background-color: #aaa;
display: block;
}
.hidden-mobile {
display: none;
}
.blink-bg {
animation: blinkingBackground 2s infinite;
}
@keyframes blinkingBackground{
0% { background-color: #ffee00;}
50% { background-color: #ff3200;}
100% { background-color: #ffee00;}
}

View File

@@ -17,9 +17,18 @@ import matplotlib.pyplot as plt
from obspy import read, UTCDateTime, Stream from obspy import read, UTCDateTime, Stream
from obspy.clients.filesystem.sds import Client from obspy.clients.filesystem.sds import Client
from write_utils import write_html_text, write_html_row, write_html_footer, write_html_header, get_print_title_str,\ from write_utils import write_html_text, write_html_row, write_html_footer, write_html_header, get_print_title_str, \
init_html_table, finish_html_table init_html_table, finish_html_table
from utils import get_bg_color from utils import get_bg_color, modify_stream_for_plot, trace_ylabels, trace_yticks
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"
@@ -32,12 +41,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}'
@@ -52,7 +61,7 @@ def fancy_timestr(dt, thresh=600, modif='+'):
class SurveillanceBot(object): class SurveillanceBot(object):
def __init__(self, parameter_path, outpath_html=None): def __init__(self, parameter_path, outpath_html=None):
self.keys = ['last active', '230V', '12V', 'router', 'charger', 'voltage', 'temp', 'other'] self.keys = ['last active', '230V', '12V', 'router', 'charger', 'voltage', 'clock', 'temp', 'other']
self.parameter_path = parameter_path self.parameter_path = parameter_path
self.update_parameters() self.update_parameters()
self.starttime = UTCDateTime() self.starttime = UTCDateTime()
@@ -64,6 +73,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 +92,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 +115,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 = []
@@ -145,7 +157,7 @@ class SurveillanceBot(object):
if filename in self.filenames_read: if filename in self.filenames_read:
continue continue
try: try:
st_new = read(filename) st_new = read(filename, dtype=float)
# add file to read filenames to prevent re-reading in case it is not the current day (or end of # add file to read filenames to prevent re-reading in case it is not the current day (or end of
# previous day) # previous day)
if not filename.endswith(f'{current_day:03}') and not ( if not filename.endswith(f'{current_day:03}') and not (
@@ -155,14 +167,14 @@ class SurveillanceBot(object):
print(f'Could not read file {filename}:', e) print(f'Could not read file {filename}:', e)
continue continue
self.dataStream += st_new self.dataStream += st_new
self.dataStream.merge() self.dataStream.merge(fill_value=np.nan)
# 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 +185,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(self, 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:
@@ -248,7 +278,7 @@ class SurveillanceBot(object):
if self.outpath_html: if self.outpath_html:
self.write_html_table() self.write_html_table()
if self.parameters.get('html_figures'): if self.parameters.get('html_figures'):
self.write_html_figures(check_plot_time=not(first_exec)) self.write_html_figures(check_plot_time=not (first_exec))
else: else:
self.print_analysis() self.print_analysis()
time.sleep(self.refresh_period) time.sleep(self.refresh_period)
@@ -276,11 +306,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)
@@ -292,23 +322,50 @@ class SurveillanceBot(object):
os.mkdir(self.outpath_html) os.mkdir(self.outpath_html)
def write_html_figures(self, check_plot_time=True): def write_html_figures(self, check_plot_time=True):
""" Write figures for html, right now hardcoded hourly """ """ Write figures for html (e.g. hourly) """
if check_plot_time and not self.check_plot_hour(): if check_plot_time and not self.check_plot_hour():
return return
for nwst_id in self.station_list:
self.write_html_figure(nwst_id)
def write_html_figure(self, nwst_id):
""" Write figure for html for specified station """
self.check_fig_dir() self.check_fig_dir()
for st_id in self.station_list: fig = plt.figure(figsize=(16, 9))
fig = plt.figure(figsize=(16, 9)) fnout = self.get_fig_path_abs(nwst_id)
fnout = self.get_fig_path_abs(st_id) st = self.data.get(nwst_id)
st = self.data.get(st_id) if st:
if st: # TODO: this section might fail, adding try-except block for analysis and to prevent program from crashing
try:
st = modify_stream_for_plot(st, parameters=self.parameters)
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')
trace_ylabels(fig, self.parameters, self.verbosity)
trace_yticks(fig, self.parameters, self.verbosity)
except Exception as e:
print(f'Could not generate plot for {nwst_id}:')
print(traceback.format_exc())
if len(fig.axes) > 0:
ax = fig.axes[0] ax = fig.axes[0]
ax.set_title(f'Hourly refreshed plot at (UTC) {UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S")}') ax.set_title(f'Plot refreshed at (UTC) {UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S")}. '
f'Refreshed hourly or on FAIL status.')
for ax in fig.axes:
ax.grid(True, alpha=0.1)
fig.savefig(fnout, dpi=150., bbox_inches='tight') fig.savefig(fnout, dpi=150., bbox_inches='tight')
plt.close(fig) plt.close(fig)
def write_html_table(self, default_color='#e6e6e6', default_header_color='#999', hide_keys_mobile=('other')):
def get_html_class(status=None, check_key=None):
""" helper function for html class if a certain condition is fulfilled """
html_class = None
if status and status.is_active:
html_class = 'blink-bg'
if check_key in hide_keys_mobile:
html_class = 'hidden-mobile'
return html_class
def write_html_table(self, default_color='#e6e6e6'):
self.check_html_dir() self.check_html_dir()
fnout = pjoin(self.outpath_html, 'survBot_out.html') fnout = pjoin(self.outpath_html, 'survBot_out.html')
if not fnout: if not fnout:
@@ -316,38 +373,58 @@ class SurveillanceBot(object):
try: try:
with open(fnout, 'w') as outfile: with open(fnout, 'w') as outfile:
write_html_header(outfile, self.refresh_period) write_html_header(outfile, self.refresh_period)
#write_html_table_title(outfile, self.parameters) # write_html_table_title(outfile, self.parameters)
init_html_table(outfile) init_html_table(outfile)
# First write header items # First write header items
header_items = [dict(text='Station', color=default_color)] header = self.keys.copy()
for check_key in self.keys: # add columns for additional links
item = dict(text=check_key, color=default_color) for key in self.add_links:
header.insert(-1, key)
header_items = [dict(text='Station', color=default_header_color)]
for check_key in header:
html_class = get_html_class(check_key=check_key)
item = dict(text=check_key, color=default_header_color, html_class=html_class)
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)] nwst_id_str = nwst_id.rstrip('.')
for check_key in self.keys: col_items = [dict(text=nwst_id_str, color=default_color, hyperlink=fig_name,
status_dict, detailed_dict = self.analysis_results.get(st_id) bold=True, tooltip=f'Show plot of {nwst_id_str}')]
status = status_dict.get(check_key) for check_key in header:
if check_key in self.keys:
status_dict = self.analysis_results.get(nwst_id)
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]
bg_color = get_bg_color(check_key, status, dt_thresh, hex=True) bg_color = get_bg_color(check_key, status, dt_thresh, hex=True)
if not bg_color: if not bg_color:
bg_color = default_color bg_color = default_color
# 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)), html_class = get_html_class(status=status, check_key=check_key)
color=bg_color) item = dict(text=str(message), tooltip=str(detailed_message), color=bg_color,
html_class=html_class)
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)
@@ -361,7 +438,7 @@ class SurveillanceBot(object):
timespan = timedelta(seconds=int(self.parameters.get('timespan') * 24 * 3600)) timespan = timedelta(seconds=int(self.parameters.get('timespan') * 24 * 3600))
self.status_message = f'Program starttime (UTC) {self.starttime.strftime("%Y-%m-%d %H:%M:%S")} | ' \ self.status_message = f'Program starttime (UTC) {self.starttime.strftime("%Y-%m-%d %H:%M:%S")} | ' \
f'Current time (UTC) {UTCDateTime().strftime("%Y-%m-%d %H:%M:%S")} | ' \ f'Current time (UTC) {UTCDateTime().strftime("%Y-%m-%d %H:%M:%S")} | ' \
f'Refresh period: {self.refresh_period}s | '\ f'Refresh period: {self.refresh_period}s | ' \
f'Showing data of last {timespan}' f'Showing data of last {timespan}'
def print(self, string, **kwargs): def print(self, string, **kwargs):
@@ -378,12 +455,13 @@ class SurveillanceBot(object):
class StationQC(object): class StationQC(object):
def __init__(self, stream, nsl, parameters, keys, starttime, verbosity, print_func): def __init__(self, parent, 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)
:param parameters: parameters dictionary from parameters.yaml file :param parameters: parameters dictionary from parameters.yaml file
""" """
self.parent = parent
self.stream = stream self.stream = stream
self.nsl = nsl self.nsl = nsl
self.network = nsl.get('network') self.network = nsl.get('network')
@@ -395,51 +473,186 @@ 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'): @property
# update detailed status if already existing def nwst_id(self):
current_message = self.detailed_status_dict.get(key) return f'{self.network}.{self.station}'
current_message = '' if current_message in [None, '-'] else current_message + ' | '
self.detailed_status_dict[key] = current_message + detailed_message
# this is becoming a little bit too complicated (adding warnings to existing) def status_ok(self, key, detailed_message="Everything OK", status_message='OK', overwrite=False):
current_status_message = self.status_dict.get(key) current_status = self.status_dict.get(key)
current_status_message = '' if current_status_message in [None, 'OK', '-'] else current_status_message + ' | ' # do not overwrite existing warnings or errors
self.status_dict[key] = current_status_message + status_message if not overwrite and (current_status.is_warn or current_status.is_error):
return
self.status_dict[key] = StatusOK(message=status_message, detailed_messages=[detailed_message])
def warn(self, key, detailed_message, last_occurrence=None, count=1):
if key == 'other':
self.status_other(detailed_message, last_occurrence, count)
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
# if error is new and not on program-startup set active and refresh plot (using parent class)
if self.search_previous_errors(key, n_errors=1) is True:
self.parent.write_html_figure(self.nwst_id)
if self.verbosity: if self.verbosity:
self.print(f'{UTCDateTime()}: {message}', flush=False) self.print(f'{UTCDateTime()}: {detailed_message}', flush=False)
# warnings.warn(message)
def activity_check(self): # 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) is True:
self.send_mail(key, status_type='FAIL', additional_message=detailed_message)
# set status to "inactive" after sending info mail
current_status.is_active = False
elif self.search_previous_errors(key) == 'active':
current_status.is_active = True
self._update_status(key, current_status, detailed_message, last_occurrence)
def search_previous_errors(self, key, n_errors=None):
"""
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)
If last item is error but not all items are error yet return keyword 'active' -> error active, no message sent
In all other cases return False.
This also prevents sending status (e.g. mail) in case of program startup
"""
if n_errors is not None:
n_errors = self.parameters.get('n_track') + 1
previous_errors = self.status_track.get(key)
# only if error list is filled n_track times
if previous_errors and len(previous_errors) == n_errors:
# 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
# in case previous_errors exists, last item is error but not all items are error, error still active
elif previous_errors and previous_errors[-1] and not all(previous_errors):
return 'active'
return False
def send_mail(self, key, status_type, additional_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
stations_blacklist = mail_params.get('stations_blacklist')
if stations_blacklist and self.station in stations_blacklist:
if self.verbosity:
print(f'Station {self.station} listed in blacklist. Return')
return
networks_blacklist = mail_params.get('networks_blacklist')
if networks_blacklist and self.network in networks_blacklist:
if self.verbosity:
print(f'Station {self.station} of network {self.network} listed in blacklist. 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
dt = self.get_dt_for_action()
text = f'{key}: Status {status_type} longer than {dt}: ' + additional_message
msg = MIMEText(text)
msg['Subject'] = f'new message on station {self.nwst_id}'
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 get_dt_for_action(self):
n_track = self.parameters.get('n_track')
interval = self.parameters.get('interval')
dt = timedelta(seconds=n_track * interval)
return dt
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, key='last_active'):
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)) dt_active = timedelta(seconds=int(self.program_starttime - self.last_active))
self.status_dict['last active'] = message status = Status(message=dt_active)
self.check_for_inactive_message(key, dt_active)
self.status_dict['last active'] = status
def last_activity(self): def last_activity(self):
if not self.stream: if not self.stream:
@@ -450,39 +663,95 @@ class StationQC(object):
if len(endtimes) > 0: if len(endtimes) > 0:
return max(endtimes) return max(endtimes)
def check_for_inactive_message(self, key, dt_active):
dt_action = self.get_dt_for_action()
interval = self.parameters.get('interval')
if dt_action <= dt_active < dt_action + timedelta(seconds=interval):
self.send_mail(key, status_type='Inactive')
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()
self.pb_rout_charge_analysis() self.pb_rout_charge_analysis()
self.clock_quality_analysis()
def return_print_analysis(self): def return_print_analysis(self):
items = [f'{self.network}.{self.station}'] items = [self.nwst_id]
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 clock_quality_analysis(self, channel='LCQ'):
""" Analyse clock quality """
key = 'clock'
st = self.stream.select(channel=channel)
trace = self.get_trace(st, key)
if not trace: return
clockQuality = trace.data
clockQuality_warn_level = self.parameters.get('THRESHOLDS').get('clockquality_warn')
clockQuality_fail_level = self.parameters.get('THRESHOLDS').get('clockquality_fail')
if self.verbosity > 1:
self.print(40 * '-')
self.print('Performing Clock Quality check', flush=False)
clockQuality_warn = np.where(clockQuality < clockQuality_warn_level)[0]
clockQuality_fail = np.where(clockQuality < clockQuality_fail_level)[0]
if len(clockQuality_warn) == 0 and len(clockQuality_fail) == 0:
self.status_ok(key, detailed_message=f'ClockQuality={(clockQuality[-1])}')
return
warn_message = f'Trace {trace.get_id()}:'
if len(clockQuality_warn) > 0:
# try calculate number of warn peaks from gaps between indices
n_qc_warn = self.calc_occurrences(clockQuality_warn)
detailed_message = warn_message + f' {n_qc_warn}x Qlock Quality less then {clockQuality_warn_level}' \
+ self.get_last_occurrence_timestring(trace, clockQuality_warn)
self.warn(key, detailed_message=detailed_message, count=n_qc_warn,
last_occurrence=self.get_last_occurrence(trace, clockQuality_warn))
if len(clockQuality_fail) > 0:
# try calculate number of fail peaks from gaps between indices
n_qc_fail = self.calc_occurrences(clockQuality_fail)
detailed_message = warn_message + f' {n_qc_fail}x Qlock Quality less then {clockQuality_fail_level}V ' \
+ self.get_last_occurrence_timestring(trace, clockQuality_fail)
self.error(key, detailed_message=detailed_message, count=n_qc_fail,
last_occurrence=self.get_last_occurrence(trace, clockQuality_fail))
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,24 +770,25 @@ 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_undervolt = 0
warn_message = f'Trace {trace.get_id()}:' warn_message = f'Trace {trace.get_id()}:'
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. """
@@ -534,7 +804,7 @@ class StationQC(object):
nsamp_av = int(trace.stats.sampling_rate) * timespan nsamp_av = int(trace.stats.sampling_rate) * timespan
av_temp_str = str(round(np.mean(temp[-nsamp_av:]), 1)) + deg_str av_temp_str = str(round(np.mean(temp[-nsamp_av:]), 1)) + deg_str
# dt of average # dt of average
dt_t_str = str(timedelta(seconds=int(timespan))) dt_t_str = str(timedelta(seconds=int(timespan))).replace(', 0:00:00', '')
# current temp # current temp
cur_temp = round(temp[-1], 1) cur_temp = round(temp[-1], 1)
if self.verbosity > 1: if self.verbosity > 1:
@@ -547,14 +817,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 +874,53 @@ 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
# try calculate number of voltage peaks from gaps between indices if volt_lvl > 1:
n_occurrences = len(np.where(np.diff(ind_array) > 1)[0]) + 1 n_occurrences = self.calc_occurrences(ind_array)
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 calc_occurrences(self, ind_array):
# try calculate number of voltage peaks/plateaus from gaps between indices
if len(ind_array) == 0:
return 0
else:
# start index at 1 if there are gaps (n_peaks = n_gaps + 1)
n_occurrences = 1
min_samples = self.parameters.get('min_sample')
if not min_samples:
min_samples = 1
# calculated differences in index array, diff > 1: gap, diff == 1: within peak/plateau
diffs = np.diff(ind_array)
gap_start_inds = np.where(np.diff(ind_array) > 1)[0]
# iterate over all gaps and check "min_samples" before the gap
for gsi in gap_start_inds:
# right boundary index of peak (gap index - 1)
peak_rb_ind = gsi - 1
# left boundary index of peak
peak_lb_ind = max([0, peak_rb_ind - min_samples])
if all(diffs[peak_lb_ind: peak_rb_ind] == 1):
n_occurrences += 1
return n_occurrences
def get_trace(self, stream, keys): def get_trace(self, stream, keys):
if not type(keys) == list: if not type(keys) == list:
@@ -673,12 +974,11 @@ 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), status_message='under 1V ({})'.format(n_occurrences))
status_message='under 1V ({})'.format(n_occurrences))
# classify last voltage values # classify last voltage values
for volt in voltage_levels: for volt in voltage_levels:
@@ -695,10 +995,10 @@ 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.')
return False, voltage_dict, last_val return False, voltage_dict, last_val
@@ -707,6 +1007,96 @@ 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=None, detailed_messages=None, count: int = 0, last_occurrence=None, show_count=True):
if message is None:
message = '-'
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
self.is_active = 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

@@ -22,7 +22,6 @@ except ImportError:
except ImportError: except ImportError:
raise ImportError('Could import neither of PySide2, PySide6 or PyQt5') raise ImportError('Could import neither of PySide2, PySide6 or PyQt5')
import matplotlib
from matplotlib.figure import Figure from matplotlib.figure import Figure
if QtGui.__package__ in ['PySide2', 'PyQt5', 'PySide6']: if QtGui.__package__ in ['PySide2', 'PyQt5', 'PySide6']:
@@ -35,7 +34,7 @@ from obspy import UTCDateTime
from survBot import SurveillanceBot from survBot import SurveillanceBot
from write_utils import * from write_utils import *
from utils import get_bg_color from utils import get_bg_color, modify_stream_for_plot, trace_ylabels, trace_yticks
try: try:
from rest_api.utils import get_station_iccid from rest_api.utils import get_station_iccid
@@ -130,10 +129,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 +179,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 +229,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 +252,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,12 +309,15 @@ 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 = modify_stream_for_plot(st, parameters=self.parameters)
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)
trace_ylabels(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
trace_yticks(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
self.plot_widget.show() self.plot_widget.show()
def notification(self, text): def notification(self, text):

112
utils.py
View File

@@ -3,18 +3,20 @@
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')
@@ -22,16 +24,18 @@ def get_bg_color(check_key, status, dt_thresh=None, hex=False):
bg_color = '#{:02x}{:02x}{:02x}'.format(*bg_color[:3]) bg_color = '#{:02x}{:02x}{:02x}'.format(*bg_color[:3])
return bg_color return bg_color
def get_color(key): 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)
def get_time_delay_color(dt, dt_thresh): def get_time_delay_color(dt, dt_thresh):
""" Set color of time delay after thresholds specified in self.dt_thresh """ """ Set color of time delay after thresholds specified in self.dt_thresh """
if dt < dt_thresh[0]: if dt < dt_thresh[0]:
@@ -40,6 +44,7 @@ def get_time_delay_color(dt, dt_thresh):
return get_color('WARN') return get_color('WARN')
return get_color('FAIL') return get_color('FAIL')
def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'): def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'):
""" Get an rgba temperature value back from specified cmap, linearly interpolated between vmin and vmax. """ """ Get an rgba temperature value back from specified cmap, linearly interpolated between vmin and vmax. """
if type(temp) in [str]: if type(temp) in [str]:
@@ -49,3 +54,92 @@ def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'):
rgba = [int(255 * c) for c in cmap(val)] rgba = [int(255 * c) for c in cmap(val)]
return rgba return rgba
def modify_stream_for_plot(st, parameters):
""" copy (if necessary) and modify stream for plotting """
ch_units = parameters.get('CHANNEL_UNITS')
ch_transf = parameters.get('CHANNEL_TRANSFORM')
# if either of both are defined make copy
if ch_units or ch_transf:
st = st.copy()
# modify trace for plotting by multiplying unit factor (e.g. 1e-3 mV to V)
if ch_units:
for tr in st:
channel = tr.stats.channel
unit_factor = ch_units.get(channel)
if unit_factor:
tr.data = tr.data * float(unit_factor)
# modify trace for plotting by other arithmetic expressions
if ch_transf:
for tr in st:
channel = tr.stats.channel
transf = ch_transf.get(channel)
if transf:
tr.data = transform_trace(tr.data, transf)
return st
def transform_trace(data, transf):
"""
Transform trace with arithmetic operations in order, specified in transf
@param data: numpy array
@param transf: list of lists with arithmetic operations (e.g. [['*', '20'], ] -> multiply data by 20
"""
# This looks a little bit hardcoded, however it is safer than using e.g. "eval"
for operator_str, val in transf:
if operator_str == '+':
data = data + val
elif operator_str == '-':
data = data - val
elif operator_str == '*':
data = data * val
elif operator_str == '/':
data = data / val
else:
raise IOError(f'Unknown arithmethic operator string: {operator_str}')
return data
def trace_ylabels(fig, parameters, verbosity=0):
"""
Adds channel names to y-axis if defined in parameters.
Can get mixed up if channel order in stream and channel names defined in parameters.yaml differ, but it is
difficult to assess the correct order from Obspy plotting routing.
"""
names = parameters.get('channel_names')
if not names: # or not len(st.traces):
return
if not len(names) == len(fig.axes):
if verbosity:
print('Mismatch in axis and label lengths. Not adding plot labels')
return
for channel_name, ax in zip(names, fig.axes):
if channel_name:
ax.set_ylabel(channel_name)
def trace_yticks(fig, parameters, verbosity=0):
"""
Adds channel names to y-axis if defined in parameters.
Can get mixed up if channel order in stream and channel names defined in parameters.yaml differ, but it is
difficult to assess the correct order from Obspy plotting routing.
"""
ticks = parameters.get('CHANNEL_TICKS')
if not ticks:
return
if not len(ticks) == len(fig.axes):
if verbosity:
print('Mismatch in axis tick and label lengths. Not changing plot ticks.')
return
for ytick_tripple, ax in zip(ticks, fig.axes):
if not ytick_tripple:
continue
ymin, ymax, step = ytick_tripple
yticks = list(range(ymin, ymax + step, step))
ax.set_yticks(yticks)
ax.set_ylim(ymin - step, ymax + step)

View File

@@ -1,55 +1,68 @@
from datetime import timedelta from datetime import timedelta
def write_html_table_title(fobj, parameters): def write_html_table_title(fobj, parameters):
title = get_print_title_str(parameters) title = get_print_title_str(parameters)
fobj.write(f'<h3>{title}</h3>\n') fobj.write(f'<h3>{title}</h3>\n')
def write_html_text(fobj, text): def write_html_text(fobj, text):
fobj.write(f'<p>{text}</p>\n') fobj.write(f'<p>{text}</p>\n')
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" media="only screen and (max-width: 400px)" href="mobile.css" />',
' <link rel="stylesheet" media="only screen and (min-width: 401px)" href="desktop.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">',
'<meta name="viewport" content="width=device-width, initial-scale=1">',
'<body>'] '<body>']
# style = ['<style>',
# 'table, th, td {',
# 'border:1px solid black;',
# '}',
# '</style>',]
for item in header: for item in header:
fobj.write(item + '\n') fobj.write(item + '\n')
def init_html_table(fobj): def init_html_table(fobj):
fobj.write('<table style="width:100%">\n') fobj.write('<table style="width:100%">\n')
def finish_html_table(fobj): def finish_html_table(fobj):
fobj.write('</table>\n') fobj.write('</table>\n')
def write_html_footer(fobj): def write_html_footer(fobj):
footer = ['</body>', footer = ['</body>',
'</html>'] '</html>']
for item in footer: for item in footer:
fobj.write(item + '\n') fobj.write(item + '\n')
def write_html_row(fobj, items, html_key='td'): def write_html_row(fobj, items, html_key='td'):
default_space = ' ' default_space = ' '
fobj.write(default_space + '<tr>\n') fobj.write(default_space + '<tr>\n')
for item in items: for item in items:
text = item.get('text') text = item.get('text')
if item.get('bold'):
text = '<b>' + text + '</b>'
if item.get('italic'):
text = '<i>' + text + '</i>'
tooltip = item.get('tooltip') tooltip = item.get('tooltip')
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}' html_class = item.get('html_class')
class_str = f' class="{html_class}"' if html_class else ''
fobj.write(2 * default_space + f'<{html_key}{class_str} 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')
def get_print_title_str(parameters): def get_print_title_str(parameters):
timespan = parameters.get('timespan') * 24 * 3600 timespan = parameters.get('timespan') * 24 * 3600
tdelta_str = str(timedelta(seconds=int(timespan))) tdelta_str = str(timedelta(seconds=int(timespan))).replace(', 0:00:00', '')
return f'Analysis table of router quality within the last {tdelta_str}' return f'Analysis table of router quality within the last {tdelta_str}'