Compare commits
24 Commits
cd6b40688b
...
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 47c3fbabf0 | |||
| d397ce377e | |||
| d764c5c256 | |||
| a56781dca3 | |||
| fc64239c88 | |||
| f0ae7da2be | |||
| a30cd8c0d4 | |||
| a3378874fa | |||
| d21fb0ca3b | |||
| 19b8df8f7d | |||
| 6fc1e073c0 | |||
| 56351ee700 | |||
| f45c5b20c5 | |||
| 7a2b7add04 | |||
| ae0c2ef4e9 | |||
| d35c176aab | |||
| 9444405453 | |||
| a6d59c8c71 | |||
| 3fe5fc48d1 | |||
| 7da3db260a | |||
| 2c1e923920 | |||
| 8e42ac11c7 | |||
| 4d4324a1e9 | |||
| 4ba9c20d0f |
@@ -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
|
||||||
|
|||||||
@@ -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"] # Specify SOH channels, currently supported EX[1-3] and VEI
|
||||||
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,49 @@ 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
|
||||||
|
|
||||||
|
# ---------------------------------------- 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: ["Temperature (°C)", "230V/12V Status (V)", "Router/Charger State (V)", "Logger Voltage (V)"]
|
||||||
|
# specify y-ticks (and ylims) giving, (ymin, ymax, step) for each of the above channels (0: default)
|
||||||
|
CHANNEL_TICKS:
|
||||||
|
- [-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
39
stylesheets/desktop.css
Normal 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
43
stylesheets/mobile.css
Normal 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;}
|
||||||
|
}
|
||||||
631
survBot.py
631
survBot.py
@@ -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}'
|
||||||
|
|
||||||
@@ -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,56 @@ 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()
|
||||||
|
|
||||||
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 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 +731,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 +765,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 +778,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 +835,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 +935,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 +956,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 +968,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')
|
||||||
|
|||||||
@@ -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
112
utils.py
@@ -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)
|
||||||
@@ -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}'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user