46 Commits

Author SHA1 Message Date
94cac54716 [update] add parameter to disable PowBox usage for selected stations 2024-08-13 16:40:22 +02:00
c57763c016 [update] added coloring for temperature warning and a critical temperature level, improved tooltip message 2024-08-13 13:38:50 +02:00
3c6ea1ffd0 [minor] add axis-shading for a better distinction of different axes 2023-12-18 16:18:20 +01:00
fde000ec0d [minor] revert accidental change 2023-12-18 16:05:24 +01:00
f41f24f626 [minor] added an option to get warn thresholds for PowBox channels from corresponding parameters, added warn state annotations to html figures 2023-12-18 15:26:53 +01:00
c621b31f6e [update] use logging module instead of print/verbosity 2023-12-18 15:24:56 +01:00
20b586f96b [bugfix] warn threshold type check had wrong parentheses 2023-12-18 15:11:02 +01:00
08b12aeb9d [minor] catch matplotlib write error 2023-09-04 15:30:37 +02:00
00d2d0119c Merge tag 'v0.2' into develop
new version release 0.2
2023-06-01 10:10:26 +02:00
e1a3b498e5 Merge branch 'release0.2' 2023-06-01 10:09:58 +02:00
9e1ebebeb2 [release] version 0.2 2023-06-01 10:08:26 +02:00
2af30f3a32 [bugfix] count index exceeded array length 2023-05-25 15:28:11 +02:00
f3ccaaefd8 [minor] changed colors in examples stylesheets back to default 2023-04-21 17:18:16 +02:00
a15aee1da6 [minor] update stylesheets 2023-04-21 16:40:58 +02:00
353f073d12 [update] added color palette suggested by AM, some visual tweaks 2023-04-21 16:33:02 +02:00
10e2322882 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	utils.py
2023-04-21 14:17:28 +02:00
a6475f2c3b tweak website design in html and css 2023-04-21 13:52:56 +02:00
72e3ede52f [minor] small adjustments 2023-04-20 15:41:20 +02:00
fb4d5c2929 [minor] optimized stylesheets 2023-04-19 15:44:31 +02:00
1983cc3b1e [new] add logo on html webpage 2023-04-19 14:59:45 +02:00
acc8575d70 [update] can add global (station-independent) links on html web page 2023-04-17 17:59:09 +02:00
1b010ecb61 [minor] add missing </a> at the end of html link 2023-04-17 17:20:35 +02:00
174a6148bf [update] disconnect status if PowBox voltage < 1V 2023-04-12 13:13:04 +02:00
5a4733b031 [minor] only send mail on inactive when no FAIL state is present 2023-02-28 11:25:43 +01:00
477727018f [minor] added call argument "parameter path" 2023-02-23 11:50:38 +01:00
9d9ced7c83 [bugfix] no warning for volt_lvl = -1 2023-02-17 16:14:59 +01:00
305ab25ab2 [bugfix] slightly wrong if condition 2023-02-17 14:16:38 +01:00
b875e63f83 [bugfix] check for upcoming error 2023-02-16 16:05:12 +01:00
20635e3433 [new] add file mailing_list.yaml 2023-02-16 15:57:13 +01:00
a6c792b271 [bugfix] some bugfixes/tweaks 2023-02-16 15:49:07 +01:00
5632bab07b [update] re-wrote html part, increased flexibility, can now send html via mail incl. figure 2023-02-01 16:22:26 +01:00
d7cbbe6876 [minor] add missing verbosity flag 2023-01-31 17:37:17 +01:00
3a384fd7b5 [update] add external mail list for detailed specification of info mail recipients 2023-01-31 16:12:07 +01:00
c736048811 [update] re-read only modified files 2023-01-31 16:10:57 +01:00
908535fcc8 [minor] modified email message for activity check, plot figures in same time intervals 2023-01-06 10:38:07 +01:00
a5486e19aa [update] first working version of gap check, testing needed
[minor] soft-coded data channels
2023-01-03 18:11:53 +01:00
a89ea1b06d [refactor] PEP8 naming convention 2022-12-22 16:00:37 +01:00
24d15d9d55 [fix] mutable default argument 2022-12-22 16:00:07 +01:00
bc70dc0816 [minor] soft-coded unit factor for channel analysis 2022-12-22 15:56:32 +01:00
03616a2b7b [update] refined and enabled clock quality check 2022-12-22 15:36:55 +01:00
bf000fe042 [minor] visual tweaks 2022-12-21 16:03:10 +01:00
c90b430fa8 [bugfix] corrected error check (plot on new FAIL status)
[minor] message output
2022-12-21 15:48:18 +01:00
7d5f9cf516 [update] re-worked channel definition in parameters.yaml, each channel now has its own dictionary with optional plotting flags
[WIP] clock quality work in progress, currently disabled
2022-12-21 12:51:01 +01:00
b17ee1288c [minor] try re-read yaml in case it failed 2022-12-21 11:57:37 +01:00
bf82148449 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	parameters.yaml
#	survBot.py
2022-12-20 17:02:16 +01:00
174a8e0823 [new] mass channel surveillance added 2022-12-20 16:54:27 +01:00
11 changed files with 953 additions and 317 deletions

2
.gitignore vendored
View File

@@ -211,3 +211,5 @@ flycheck_*.el
/network-security.data
/__simulate_fail.json
/mailing_list.yaml

View File

@@ -1,6 +1,6 @@
# survBot
version: 0.1
version: 0.2
survBot is a small program used to track station quality channels of DSEBRA stations via PowBox output over SOH channels
by analysing contents of a Seiscomp3 datapath.
@@ -40,8 +40,16 @@ The GUI can be loaded via
python survBotGui.py
```
## Version Changes
- surveillance of mass, clock and gaps
- individual mailing lists for different stations
- html mail with recent status information
- updated web page design
- restructured parameter file
- recognize if PBox is disconnected
## Staff
Original author: M.Paffrath (marcel.paffrath@rub.de)
November 2022
June 2023

View File

@@ -0,0 +1,3 @@
# survBot is a small program used to track station quality channels of DSEBRA stations via PowBox output
# over SOH channels by analysing contents of a Seiscomp3 datapath.
__version__ = "0.2"

9
mailing_list.yaml Normal file
View File

@@ -0,0 +1,9 @@
# specify mail addresses and station network ids for which information mails shall be sent, e.g.:
# "mail.address@provider.com, mail.address2@provider2.com":
# - 1Y.GR01
# - 1Y.GR02
# "mail.address3@provder.com":
# - 1Y.GR03
#"kasper.fischer@rub.de":
# - 1Y.GR01

View File

@@ -1,18 +1,18 @@
# Parameters file for Surveillance Bot
datapath: "/data/SDS/" # SC3 Datapath
networks: ["1Y", "HA"] # select networks, list or str
networks: ["1Y", "HA", "MK"] # select networks, list or str
stations: "*" # select stations, list or str
locations: "*" # select locations, list or str
channels: ["EX1", "EX2", "EX3", "VEI", "LCQ"] # Specify SOH channels, currently supported EX[1-3], VEI and LCQ
stations_blacklist: ["TEST", "EREA"] # exclude these stations
stations_blacklist: ["TEST", "EREA", "DOMV", "LFKM", "GR19", "LAKA"] # exclude these stations
networks_blacklist: [] # exclude these networks
interval: 60 # Perform checks every x seconds
n_track: 300 # wait n_track * intervals before performing an action (i.e. send mail/end highlight status)
n_track: 360 # wait n_track * intervals before performing an action (i.e. send mail/end highlight status)
timespan: 7 # Check data of the recent x days
verbosity: 0 # verbosity flag
verbosity: 0 # verbosity flag for program console output (not logging)
logging_level: WARN # set logging level (info, warning, debug)
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
min_sample: 5 # minimum samples for raising Warn/FAIL
dt_thresh: [300, 1800] # threshold (s) for timing delay colourisation (yellow/red)
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)
@@ -40,11 +40,81 @@ POWBOX:
THRESHOLDS:
pb_thresh: 0.2 # Threshold for PowBox Voltage check +/- (V)
max_temp: 50 # max temperature for temperature warning
critical_temp: 65 # max temperature for critical warning (fail)
low_volt: 12 # min voltage for low voltage warning
high_volt: 14.8 # max voltage for over voltage 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
max_vm_warn: 1.5 # threshold for mass offset (warn), fail)
max_vm_fail: 2.5 # threshold for mass offset (warn), fail)
clockquality_warn: 90 # warn level - clock quality ranges from 0 % to 100 % with 100 % being the best level
clockquality_fail: 70 # fail level
min_gap: 0.1 # minimum for gap declaration, should be > 0 [s]
# ---------------------------------- Specification of input channels ---------------------------------------------------
# Currently supported: EX[1-3], VEI, VM[1-3], LCQ
#
# For each channel a factor 'unit' for unit conversion (e.g. to SI) can be provided, as well as a 'name'
# and 'ticks' [ymin, ymax, ystep] for plotting.
# 'warn' and 'fail' plot horizontal lines in corresponding colors (can be str in TRESHOLDS, int/float or iterable)
# keyword "pb_SOH2" or "pb_SOH3" can be used to extract warning values from above POWBOX parameter definition
#
# 'transform' can be provided for plotting to perform arithmetic operations in given order, e.g.:
# transform: - ["*", 20]
# - ["-", 20]
# --> PBox EX1 V to deg C: 20 * x -20
CHANNELS:
EX1:
unit: 1e-6
name: "PowBox Temperature (°C)"
ticks: [-10, 50, 10]
transform:
- ["*", 20]
- ["-", 20]
warn: "max_temp"
EX2:
unit: 1e-6
name: "PowBox 230V/12V (V)"
ticks: [0, 5, 1]
warn: "pb_SOH2"
EX3:
unit: 1e-6
name: "PowBox Router/Charger (V)"
ticks: [0, 5, 1]
warn: "pb_SOH3"
VEI:
unit: 1e-3
name: "Datalogger (V)"
ticks: [9, 15, 1]
warn: ["low_volt", "high_volt"]
fail: 10.5
VM1:
unit: 1e-6
name: "Mass position W (V)"
ticks: [-2.5, 2.5, 1]
warn: [-1.5, 1.5]
fail: [-2.5, 2.5]
VM2:
unit: 1e-6
name: "Mass position V (V)"
ticks: [-2.5, 2.5, 1]
warn: [-1.5, 1.5]
fail: [-2.5, 2.5]
VM3:
unit: 1e-6
name: "Mass position U (V)"
ticks: [-2.5, 2.5, 1]
warn: [-1.5, 1.5]
fail: [-2.5, 2.5]
LCQ:
name: "Clock quality (%)"
ticks: [0, 100, 20]
warn: "clockquality_warn"
fail: "clockquality_fail"
# specify data channels (can be additional to the above). From these channels only headers will be read
data_channels: ["HHZ", "HHN", "HHE"]
# ---------------------------------------- OPTIONAL PARAMETERS ---------------------------------------------------------
@@ -54,34 +124,23 @@ 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"}
ppsd: {"URL": "../ppsd/{nw}.{st}.html", "text": "show"}
# add station-independent links below html table (list items separated with -)
add_global_links:
# for example: - {"text": "our homepage", "URL": "https://www.rub.de"}
- {"text": "show recent events on map",
"URL": "https://fdsnws.geophysik.ruhr-uni-bochum.de/map/?lat=39.5&lon=21&zoom=7&baselayer=mapnik"}
# html logo at page bottom (path relative to html directory)
html_logo: "figures/Logo_RUB_BLAU_rgb.png"
# 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
stations_blacklist: [] # 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]
# specify recipients for single stations in a yaml: key = email-address, val = station list (e.g. [1Y.GR01, 1Y.GR02])
external_mail_list: "mailing_list.yaml"

View File

@@ -2,26 +2,36 @@ body {
background-color: #ffffff;
place-items: center;
text-align: center;
padding-bottom: 30px;
font-family: "Helvetica", "sans-serif";
}
table {
position: relative
}
td {
border-radius: 4px;
border-radius: 2px;
padding: 0px;
white-space: nowrap;
}
th {
background-color: #999;
border-radius: 4px;
color: #fff;
border-radius: 2px;
padding: 3px 1px;
position: sticky;
top: 0;
}
a:link, a:visited {
background-color: #ccc;
background-color: #e8e8e8;
color: #000;
text-decoration: none;
display: block;
border-radius: 4px;
border: 1px solid #bbb;
border: 1px solid #ccc;
}
a:hover {
@@ -37,3 +47,12 @@ a:hover {
50% { background-color: #ff3200;}
100% { background-color: #ffcc00;}
}
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
text-align: center;
}

View File

@@ -2,26 +2,36 @@ body {
background-color: #ffffff;
place-items: center;
text-align: center;
padding-bottom: 30px;
font-family: "Helvetica", "sans-serif";
}
table {
position: relative
}
td {
border-radius: 4px;
border-radius: 3px;
padding: 10px 2px;
white-space: nowrap;
}
th {
background-color: #999;
border-radius: 4px;
color: #fff;
border-radius: 3px;
padding: 10px, 2px;
position: sticky;
top: 0;
}
a:link {
background-color: #ccc;
a:link, a:visited {
background-color: #e8e8e8;
color: #000;
text-decoration: none;
display: block;
border-radius: 4px;
border: 1px solid #bbb;
border-radius: 6px;
border: 1px solid #ccc;
}
a:hover {
@@ -37,7 +47,16 @@ a:hover {
animation: blinkingBackground 2s infinite;
}
@keyframes blinkingBackground{
0% { background-color: #ffee00;}
0% { background-color: #ffcc00;}
50% { background-color: #ff3200;}
100% { background-color: #ffee00;}
100% { background-color: #ffcc00;}
}
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
text-align: center;
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ import os
import sys
import traceback
import logging
try:
from PySide2 import QtGui, QtCore, QtWidgets
except ImportError:
@@ -34,14 +36,14 @@ from obspy import UTCDateTime
from survBot import SurveillanceBot
from write_utils import *
from utils import get_bg_color, modify_stream_for_plot, trace_ylabels, trace_yticks
from utils import get_bg_color, modify_stream_for_plot, set_axis_yticks, set_axis_color, plot_axis_thresholds
try:
from rest_api.utils import get_station_iccid
from rest_api.rest_api_utils import get_last_messages, send_message, get_default_params
sms_funcs = True
except ImportError:
print('Could not load rest_api utils, SMS functionality disabled.')
logging.warning('Could not load rest_api utils, SMS functionality disabled.')
sms_funcs = False
deg_str = '\N{DEGREE SIGN}C'
@@ -54,10 +56,9 @@ class Thread(QtCore.QThread):
"""
update = QtCore.Signal()
def __init__(self, parent, runnable, verbosity=0):
def __init__(self, parent, runnable):
super(Thread, self).__init__(parent=parent)
self.setParent(parent)
self.verbosity = verbosity
self.runnable = runnable
self.is_active = True
@@ -69,11 +70,10 @@ class Thread(QtCore.QThread):
self.update.emit()
except Exception as e:
self.is_active = False
print(e)
print(traceback.format_exc())
logging.error(e)
logging.debug(traceback.format_exc())
finally:
if self.verbosity > 0:
print(f'Time for Thread execution: {UTCDateTime() - t0}')
logging.info(f'Time for Thread execution: {UTCDateTime() - t0}')
class MainWindow(QtWidgets.QMainWindow):
@@ -195,7 +195,7 @@ class MainWindow(QtWidgets.QMainWindow):
station = nwst_id.split('.')[1]
iccid = get_station_iccid(station)
if not iccid:
print('Could not find iccid for station', nwst_id)
logging.info(f'Could not find iccid for station: {nwst_id}')
return
sms_widget = ReadSMSWidget(parent=self, iccid=iccid)
sms_widget.setWindowTitle(f'Recent SMS of station: {nwst_id}')
@@ -316,8 +316,10 @@ class MainWindow(QtWidgets.QMainWindow):
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)
trace_ylabels(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
trace_yticks(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
# set_axis_ylabels(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
set_axis_yticks(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
set_axis_color(fig=self.plot_widget.canvas.fig)
plot_axis_thresholds(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
self.plot_widget.show()
def notification(self, text):

218
utils.py
View File

@@ -1,7 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import matplotlib
import numpy as np
from obspy import Stream
COLORS_DICT = {'FAIL': (195, 29, 14, 255),
'NO DATA': (255, 255, 125, 255),
'WARN': (250, 192, 63, 255),
'OK': (185, 245, 145, 255),
'undefined': (240, 240, 240, 255),
'disc': (126, 127, 131, 255), }
def get_bg_color(check_key, status, dt_thresh=None, hex=False):
@@ -10,10 +23,15 @@ def get_bg_color(check_key, status, dt_thresh=None, hex=False):
bg_color = get_time_delay_color(message, dt_thresh)
elif check_key == 'temp':
bg_color = get_temp_color(message)
elif check_key == 'mass':
bg_color = get_mass_color(message)
else:
if status.is_warn:
bg_color = get_color('WARNX')(status.count)
bg_color = get_warn_color(status.count)
elif status.is_error:
if status.connection_error:
bg_color = get_color('disc')
else:
bg_color = get_color('FAIL')
else:
bg_color = get_color(message)
@@ -26,18 +44,26 @@ def get_bg_color(check_key, status, dt_thresh=None, hex=False):
def get_color(key):
# some GUI default colors
colors_dict = {'FAIL': (255, 50, 0, 255),
'NO DATA': (255, 255, 125, 255),
'WARN': (255, 255, 80, 255),
'WARNX': lambda x: (min([255, 200 + x ** 2]), 255, 80, 255),
'OK': (125, 255, 125, 255),
'undefined': (230, 230, 230, 255)}
return colors_dict.get(key)
# some old GUI default colors
# colors_dict = {'FAIL': (255, 85, 50, 255),
# 'NO DATA': (255, 255, 125, 255),
# 'WARN': (255, 255, 80, 255),
# 'OK': (173, 255, 133, 255),
# 'undefined': (230, 230, 230, 255),
# 'disc': (255, 160, 40, 255),}
if not key in COLORS_DICT.keys():
key = 'undefined'
return COLORS_DICT.get(key)
def get_color_mpl(key):
color_tup = get_color(key)
return np.array([color/255. for color in color_tup])
def get_time_delay_color(dt, dt_thresh):
""" Set color of time delay after thresholds specified in self.dt_thresh """
if isinstance(dt, type(dt_thresh[0])):
if dt < dt_thresh[0]:
return get_color('OK')
elif dt_thresh[0] <= dt < dt_thresh[1]:
@@ -45,9 +71,25 @@ def get_time_delay_color(dt, dt_thresh):
return get_color('FAIL')
def get_warn_color(count, n_colors=20):
if count >= n_colors:
count = -1
gradient = np.linspace((240, 245, 110, 255), (250, 192, 63, 255), n_colors, dtype=int)
return tuple(gradient[count])
def get_mass_color(message):
# can change this to something else if wanted. This way it always returns get_color (without warn count)
if isinstance(message, (float, int)):
return get_color('OK')
return get_color(message)
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. """
if type(temp) in [str]:
if temp in COLORS_DICT.keys():
return get_color(temp)
return get_color('undefined')
cmap = matplotlib.cm.get_cmap(cmap)
val = (temp - vmin) / (vmax - vmin)
@@ -55,29 +97,61 @@ def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'):
return rgba
def modify_stream_for_plot(st, parameters):
def get_font_color(bg_color, hex=False):
if hex:
bg_color = matplotlib.colors.to_rgb(bg_color)
bg_color_hsv = matplotlib.colors.rgb_to_hsv(bg_color)
bg_color_hsl = hsv_to_hsl(bg_color_hsv)
font_color = (255, 255, 255, 255) if bg_color_hsl[2] < 0.6 else (0, 0, 0, 255)
if hex:
font_color = '#{:02x}{:02x}{:02x}'.format(*font_color[:3])
return font_color
def hsv_to_hsl(hsv):
hue, saturation, value = hsv
lightness = value * (1 - saturation / 2)
saturation = 0 if lightness in (0, 1) else (value - lightness) / min(lightness, 1 - lightness)
return hue, saturation, lightness
def modify_stream_for_plot(input_stream, 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()
# make a copy
st = Stream()
# 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)
channels_dict = parameters.get('CHANNELS')
# iterate over all channels and put them to new stream in order
for index, ch_tup in enumerate(channels_dict.items()):
# unpack tuple from items
channel, channel_dict = ch_tup
# get correct channel from stream
st_sel = input_stream.select(channel=channel)
# in case there are != 1 there is ambiguity
if not len(st_sel) == 1:
continue
# make a copy to not modify original stream!
tr = st_sel[0].copy()
# multiply with conversion factor for unit
unit_factor = channel_dict.get('unit')
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)
# apply transformations if provided
transform = channel_dict.get('transform')
if transform:
tr.data = transform_trace(tr.data, transform)
# modify trace id to maintain plotting order
name = channel_dict.get('name')
tr.id = f'{index + 1}: {name} - {tr.id}'
st.append(tr)
return st
@@ -104,42 +178,104 @@ def transform_trace(data, transf):
return data
def trace_ylabels(fig, parameters, verbosity=0):
def set_axis_ylabels(fig, parameters):
"""
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')
names = [channel.get('name') for channel in parameters.get('CHANNELS').values()]
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')
logging.info('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):
def set_axis_color(fig, color='0.8', shade_color='0.95'):
"""
Set all axes (frame) of figure to specific color. Shade every second axis.
"""
for i, ax in enumerate(fig.axes):
for key in ['bottom', 'top', 'right', 'left']:
ax.spines[key].set_color(color)
if i % 2:
ax.set_facecolor(shade_color)
def set_axis_yticks(fig, parameters):
"""
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')
ticks = [channel.get('ticks') for channel in parameters.get('CHANNELS').values()]
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.')
logging.info('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))
yticks = list(np.arange(ymin, ymax + step, step))
ax.set_yticks(yticks)
ax.set_ylim(ymin - step, ymax + step)
ax.set_ylim(ymin - 0.33 * step, ymax + 0.33 * step)
def plot_axis_thresholds(fig, parameters):
"""
Adds channel thresholds (warn, fail) to y-axis if defined in parameters.
"""
logging.info('Plotting trace thresholds')
keys_colors = {'warn': dict(color=0.8 * get_color_mpl('WARN'), linestyle=(0, (5, 10)), alpha=0.5, linewidth=0.7),
'fail': dict(color=0.8 * get_color_mpl('FAIL'), linestyle='solid', alpha=0.5, linewidth=0.7)}
for key, kwargs in keys_colors.items():
channel_threshold_list = [channel.get(key) for channel in parameters.get('CHANNELS').values()]
if not channel_threshold_list:
continue
plot_threshold_lines(fig, channel_threshold_list, parameters, **kwargs)
def plot_threshold_lines(fig, channel_threshold_list, parameters, **kwargs):
for channel_thresholds, ax in zip(channel_threshold_list, fig.axes):
if channel_thresholds in ['pb_SOH2', 'pb_SOH3']:
annotate_voltage_states(ax, parameters, channel_thresholds)
channel_thresholds = get_warn_states_pbox(channel_thresholds, parameters)
if not channel_thresholds:
continue
if not isinstance(channel_thresholds, (list, tuple)):
channel_thresholds = [channel_thresholds]
for warn_thresh in channel_thresholds:
if isinstance(warn_thresh, str):
warn_thresh = parameters.get('THRESHOLDS').get(warn_thresh)
if isinstance(warn_thresh, (float, int)):
ax.axhline(warn_thresh, **kwargs)
def get_warn_states_pbox(soh_key: str, parameters: dict) -> list:
pb_dict = parameters.get('POWBOX').get(soh_key)
if not pb_dict:
return []
return [key for key in pb_dict.keys() if key > 1]
def annotate_voltage_states(ax, parameters, pb_key, color='0.75'):
for voltage, voltage_dict in parameters.get('POWBOX').get(pb_key).items():
if float(voltage) < 1:
continue
out_string = ''
for key, val in voltage_dict.items():
if val != 'OK':
if out_string:
out_string += ' | '
out_string += f'{key}: {val}'
ax.annotate(out_string, (ax.get_xlim()[-1], voltage), color=color, fontsize='xx-small',
horizontalalignment='right')

View File

@@ -1,16 +1,21 @@
from base64 import b64encode
from datetime import timedelta
def write_html_table_title(fobj, parameters):
def _convert_to_textstring(lst):
return '\n'.join(lst)
def get_html_table_title(parameters):
title = get_print_title_str(parameters)
fobj.write(f'<h3>{title}</h3>\n')
return f'<h3>{title}</h3>\n'
def write_html_text(fobj, text):
fobj.write(f'<p>{text}</p>\n')
def get_html_text(text):
return f'<p>{text}</p>\n'
def write_html_header(fobj, refresh_rate=10):
def get_html_header(refresh_rate=10):
header = ['<!DOCTYPE html>',
'<html>',
'<head>',
@@ -21,28 +26,52 @@ def write_html_header(fobj, refresh_rate=10):
'<meta charset="utf-8">',
'<meta name="viewport" content="width=device-width, initial-scale=1">',
'<body>']
for item in header:
fobj.write(item + '\n')
header = _convert_to_textstring(header)
return header
def init_html_table(fobj):
fobj.write('<table style="width:100%">\n')
def get_mail_html_header():
header = ['<html>',
'<head>',
'</head>',
'<body>']
header = _convert_to_textstring(header)
return header
def finish_html_table(fobj):
fobj.write('</table>\n')
def init_html_table():
return '<table style="width:100%">\n'
def write_html_footer(fobj):
footer = ['</body>',
'</html>']
for item in footer:
fobj.write(item + '\n')
def finish_html_table():
return '</table>\n'
def write_html_row(fobj, items, html_key='td'):
def html_footer(footer_logo=None):
footer = ['</body>']
if footer_logo:
logo_items = [f'<div class="footer">',
f' <img style="float: right; padding: 10px;" src="{footer_logo}" height=30px>',
f'</div>']
footer += logo_items
footer.append('</html>\n')
footer = _convert_to_textstring(footer)
return footer
def add_html_image(img_data, img_format='png'):
return f"""<br>\n<img width="100%" src="data:image/{img_format};base64, {b64encode(img_data).decode('ascii')}">"""
def get_html_link(text, link):
return f'<a href="{link}"> {text} </a>'
def get_html_row(items, html_key='td'):
row_string = ''
default_space = ' '
fobj.write(default_space + '<tr>\n')
row_string += default_space + '<tr>\n'
for item in items:
text = item.get('text')
if item.get('bold'):
@@ -50,16 +79,16 @@ def write_html_row(fobj, items, html_key='td'):
if item.get('italic'):
text = '<i>' + text + '</i>'
tooltip = item.get('tooltip')
color = item.get('color')
# check for black background of headers (shouldnt happen anymore)
color = '#e6e6e6' if color == '#000000' else color
font_color = item.get('font_color')
hyperlink = item.get('hyperlink')
image_str = f'<a href="{hyperlink}">' if hyperlink else ''
color = 'transparent' if hyperlink else item.get('color')
text_str = get_html_link(text, hyperlink) if hyperlink else text
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')
fobj.write(default_space + '</tr>\n')
row_string += 2 * default_space + f'<{html_key}{class_str} bgcolor="{color}" title="{tooltip}"' \
+ f'style="color:{font_color}"> {text_str}</{html_key}>\n'
row_string += default_space + '</tr>\n'
return row_string
def get_print_title_str(parameters):