83 Commits

Author SHA1 Message Date
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
c8c3aff2fb Merge remote-tracking branch 'origin/issue/1' into issue/1 2022-12-20 12:23:23 +01:00
f2e322230e fixed paramter file 2022-12-20 12:19:32 +01:00
735abac249 use function calc_occurences 2022-12-20 12:19:05 +01:00
541815d81f Add quality check for clock quality (LCQ) 2022-12-20 12:19:05 +01:00
47c3fbabf0 Merge pull request 'release/1.0' (#3) from release/1.0 into main
Reviewed-on: #3
2022-12-20 12:05:45 +01:00
91fa60851d Merge branch 'develop' into issue/1 2022-12-20 11:39:53 +01:00
2a62fe406f Merge branch 'issue/1' into develop 2022-12-20 11:37:58 +01:00
d397ce377e [update] add network/station blacklists for mail functionality 2022-12-20 10:23:25 +01:00
8690a50899 Merge remote-tracking branch 'origin/develop' into issue/1 2022-12-08 16:02:36 +01:00
d764c5c256 Merge remote-tracking branch 'origin/develop' into develop 2022-12-08 13:23:45 +01:00
a56781dca3 [minor] renamed and added stylesheets to git repository 2022-12-08 13:23:21 +01:00
ac9f83d856 fixed paramter file 2022-12-06 16:46:19 +01:00
124c4413e1 Merge branch 'develop' into issue/1 2022-12-06 16:34:36 +01:00
fc64239c88 don't analysis values of data gaps when cheching for thresholds.
use stream.merge(fill_value=np.nan)
2022-12-06 16:34:08 +01:00
69412dc5fe use function calc_occurences 2022-12-06 15:51:24 +01:00
ac1ce5f6fa Merge branch 'develop' into issue/1 2022-12-06 15:47:56 +01:00
cb3623e4a9 Add quality check for clock quality (LCQ) 2022-12-06 15:43:13 +01:00
f0ae7da2be [update] add min_sample parameter, which controls ne number of samples that have to be of a specific voltage state before counting them as fail-state 2022-12-06 15:31:09 +01:00
a30cd8c0d4 [update] add trace axis ticking/min-max 2022-12-02 11:15:37 +01:00
a3378874fa [minor] parameter change 2022-12-02 11:15:01 +01:00
d21fb0ca3b [minor] parameter change 2022-11-29 10:42:29 +01:00
19b8df8f7d [minor] add html class for mobile (WIP) 2022-11-29 10:42:15 +01:00
6fc1e073c0 [bugfix] compared timedelta with int 2022-11-24 10:13:21 +01:00
56351ee700 [update] send message (mail) on station timeout 2022-11-23 11:52:26 +01:00
f45c5b20c5 [bugfix] set error state to active until mail is sent 2022-11-23 11:29:05 +01:00
7a2b7add04 [minor] parameter changes 2022-11-23 11:28:36 +01:00
ae0c2ef4e9 [minor] track activity status, modify html output for stylesheet 2022-11-22 18:06:25 +01:00
d35c176aab [update] add channel naming for plots 2022-11-22 15:51:15 +01:00
9444405453 [minor] reformat yaml file 2022-11-22 13:34:58 +01:00
a6d59c8c71 [update] added possibility to modify voltage level in plots from settings in parameters.yaml 2022-11-22 12:07:16 +01:00
3fe5fc48d1 [minor] re-ordered parameters 2022-11-21 10:36:46 +01:00
7da3db260a [update] error tracking + send email functionality 2022-11-17 09:52:04 +01:00
2c1e923920 [minor] add stylesheet to html header 2022-11-16 11:24:48 +01:00
8e42ac11c7 [update] complete rework of status handling (added Warn/Error classes etc.) 2022-11-15 17:19:39 +01:00
4d4324a1e9 [refactor] st_id -> nwst_id 2022-11-15 13:48:56 +01:00
4ba9c20d0f [update] add possibility to add columns with links to other web pages (e.g. seedlink monitor) 2022-11-15 13:44:19 +01:00
cd6b40688b [update] re-read data daily, add daily overlap, add value -1 for voltage lower 1V (e.g. pbox not connected) 2022-11-14 22:31:22 +01:00
04371f92c5 [minor] update README, add check for output dir existence 2022-11-09 16:53:43 +01:00
c723a32274 [update] html writer creates hourly figures of all stations 2022-11-09 16:29:01 +01:00
18dac062ef [bugfix] too many under 1V warnings appearing, moved them to "other" 2022-11-09 09:49:27 +01:00
6791a729ed [bugfix] corrected warning number for under/overvoltage 2022-11-08 17:11:37 +01:00
c3f9ad0fd9 [update] moved html writing to survBot.py so that no GUI is needed 2022-11-08 16:45:21 +01:00
abc201c673 [update] append warnings to existing ones 2022-11-08 14:23:56 +01:00
68c6df72c9 [update] html output from GUI in background possible but maybe not optimal (needs display to work in bg) 2022-11-08 13:35:46 +01:00
37b73d4393 Merge remote-tracking branch 'origin/develop' into develop 2022-11-07 17:56:58 +01:00
7a6072f8dd [update] first version write html output, todo: html parameters argparse? 2022-11-07 17:56:41 +01:00
13 changed files with 1699 additions and 273 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.
@@ -26,20 +26,30 @@ to use the GUI:
Configurations of *datapath*, *networks*, *stations* etc. can be done in the **parameters.yaml** input file.
The main program is executed by entering
The main program with html output is executed by entering
```shell script
python survBot.py
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
```shell script
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,26 +1,33 @@
# Parameters file for Surveillance Bot
datapath: '/data/SDS/' # SC3 Datapath
networks: ['1Y', 'HA']
stations: '*'
locations: '*'
channels: ['EX1', 'EX2', 'EX3', 'VEI'] # Specify SOH channels, currently supported EX[1-3] and VEI
stations_blacklist: ['TEST', 'EREA']
networks_blacklist: []
interval: 20 # Perform checks every x seconds
timespan: 7 # Check data of the recent x days
verbosity: 0
datapath: "/data/SDS/" # SC3 Datapath
networks: ["1Y", "HA", "MK"] # select networks, list or str
stations: "*" # select stations, list or str
locations: "*" # select locations, list or str
stations_blacklist: ["TEST", "EREA", "DOMV"] # exclude these stations
networks_blacklist: [] # exclude these networks
interval: 60 # Perform checks every x seconds
n_track: 360 # wait n_track * intervals before performing an action (i.e. send mail/end highlight status)
timespan: 3 # Check data of the recent x days
verbosity: 0 # verbosity flag
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: 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)
POWBOX:
pb_ok: 1 # Voltage for PowBox OK
pb_SOH2: # PowBox channel 2 voltage translations
1: {"230V": 'OK', "12V": "OK"}
-1: {"230V": "PBox under 1V", "12V": "PBox under 1V"}
1: {"230V": "OK", "12V": "OK"}
2: {"230V": "OFF", "12V": "OK"}
3: {"230V": "OK", "12V": "overvoltage"}
4: {"230V": "OK", "12V": "undervoltage"}
4.5: {"230V": "OFF", "12V": "overvoltage"}
5: {"230V": "OFF", "12V": "undervoltage"}
pb_SOH3: # PowBox channel 3 voltage translations
-1: {"router": "PBox under 1V", "charger": "PBox under 1V"}
1: {"router": "OK", "charger": "OK"}
2: {"router": "OK", "charger": "0 < resets < 3"}
2.5: {"router": "OK", "charger": "locked"}
@@ -28,10 +35,108 @@ POWBOX:
4: {"router": "FAIL", "charger": "0 < resets < 3"}
5: {"router": "FAIL", "charger": "locked"}
# Thresholds for program warnings/voltage classifications
THRESHOLDS:
pb_thresh: 0.2 # Threshold for PowBox Voltage check +/- (V)
max_temp: 50 # max temperature for temperature warning
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
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)
#
# '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: [2, 3, 4, 4.5, 5]
EX3:
unit: 1e-6
name: "PowBox Router/Charger (V)"
ticks: [0, 5, 1]
warn: [2, 2.5, 3, 4, 5]
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 ---------------------------------------------------------
# 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"}
# 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
networks_blacklist: [] # do not send emails for specific network
# 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"

58
stylesheets/desktop.css Normal file
View File

@@ -0,0 +1,58 @@
body {
background-color: #ffffff;
place-items: center;
text-align: center;
padding-bottom: 30px;
font-family: "Helvetica", "sans-serif";
}
table {
position: relative
}
td {
border-radius: 2px;
padding: 0px;
white-space: nowrap;
}
th {
background-color: #999;
color: #fff;
border-radius: 2px;
padding: 3px 1px;
position: sticky;
top: 0;
}
a:link, a:visited {
background-color: #e8e8e8;
color: #000;
text-decoration: none;
display: block;
border-radius: 4px;
border: 1px solid #ccc;
}
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;}
}
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
text-align: center;
}

62
stylesheets/mobile.css Normal file
View File

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

14
submit_bot.sh Normal file → Executable file
View File

@@ -2,14 +2,18 @@
ulimit -s 8192
#$ -l low
#$ -l os=*stretch
#$ -l h_vmem=5G
#$ -cwd
#$ -pe smp 1
##$ -q "*@minos15"
export PYTHONPATH="$PYTHONPATH:/home/marcel/git/"
#$ -N survBot_bg
#$ -l os=*stretch
source /opt/anaconda3/etc/profile.d/conda.sh
conda activate py37
python survBot.py
# environment variables for numpy to prevent multi threading
export MKL_NUM_THREADS=1
export NUMEXPR_NUM_THREADS=1
export OMP_NUM_THREADS=1
python survBot.py -html '/data/www/~marcel/'

1173
survBot.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
#! /usr/bin/env python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
GUI overlay for the main survBot to show quality control of different stations specified in parameters.yaml file.
"""
@@ -9,7 +10,6 @@ __author__ = 'Marcel Paffrath'
import os
import sys
import traceback
from datetime import timedelta
try:
from PySide2 import QtGui, QtCore, QtWidgets
@@ -22,7 +22,6 @@ except ImportError:
except ImportError:
raise ImportError('Could import neither of PySide2, PySide6 or PyQt5')
import matplotlib
from matplotlib.figure import Figure
if QtGui.__package__ in ['PySide2', 'PyQt5', 'PySide6']:
@@ -34,6 +33,8 @@ else:
from obspy import UTCDateTime
from survBot import SurveillanceBot
from write_utils import *
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
@@ -76,25 +77,16 @@ class Thread(QtCore.QThread):
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parameters='parameters.yaml', dt_thresh=(300, 1800)):
def __init__(self, parameters='parameters.yaml'):
"""
Main window of survBot GUI.
:param parameters: Parameters dictionary file (yaml format)
:param dt_thresh: threshold for timing delay colourisation (yellow/red)
"""
super(MainWindow, self).__init__()
# some GUI default colors
self.colors_dict = {'FAIL': (255, 50, 0, 255),
'NO DATA': (255, 255, 125, 255),
'WARN': (255, 255, 125, 255),
'WARNX': lambda x: (min([255, 200 + x**2]), 255, 125, 255),
'OK': (125, 255, 125, 255),
'undefined': (230, 230, 230, 255)}
# init some attributes
self.dt_thresh = dt_thresh
self.last_mouse_loc = None
self.status_message = ''
self.starttime = UTCDateTime()
# setup main layout of the GUI
@@ -107,6 +99,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.survBot = SurveillanceBot(parameter_path=parameters)
self.parameters = self.survBot.parameters
self.refresh_period = self.parameters.get('interval')
self.dt_thresh = [int(val) for val in self.parameters.get('dt_thresh')]
# create thread that is used to update
self.thread = Thread(parent=self, runnable=self.survBot.execute_qc)
@@ -136,10 +129,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.table.setRowCount(len(station_list))
self.table.setHorizontalHeaderLabels(keys)
for index, st_id in enumerate(station_list):
for index, nwst_id in enumerate(station_list):
item = QtWidgets.QTableWidgetItem()
item.setText(str(st_id.rstrip('.')))
item.setData(QtCore.Qt.UserRole, st_id)
item.setText(str(nwst_id.rstrip('.')))
item.setData(QtCore.Qt.UserRole, nwst_id)
self.table.setVerticalHeaderItem(index, item)
self.main_layout.addWidget(self.table)
@@ -186,38 +179,38 @@ class MainWindow(QtWidgets.QMainWindow):
header_item = self.table.verticalHeaderItem(row_ind)
if not header_item:
return
st_id = header_item.data(QtCore.Qt.UserRole)
nwst_id = header_item.data(QtCore.Qt.UserRole)
context_menu = QtWidgets.QMenu()
read_sms = context_menu.addAction('Get last SMS')
send_sms = context_menu.addAction('Send SMS')
action = context_menu.exec_(self.mapToGlobal(self.last_mouse_loc))
if action == read_sms:
self.read_sms(st_id)
self.read_sms(nwst_id)
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 """
station = st_id.split('.')[1]
station = nwst_id.split('.')[1]
iccid = get_station_iccid(station)
if not iccid:
print('Could not find iccid for station', st_id)
print('Could not find iccid for station', nwst_id)
return
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:
sms_widget.show()
else:
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 """
station = st_id.split('.')[1]
station = nwst_id.split('.')[1]
iccid = get_station_iccid(station)
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()
def set_clear_on_refresh(self):
@@ -225,37 +218,30 @@ class MainWindow(QtWidgets.QMainWindow):
def fill_status_bar(self):
""" Set status bar text """
self.status_message = self.survBot.status_message
status_bar = self.statusBar()
timespan = timedelta(seconds=int(self.parameters.get('timespan') * 24 * 3600))
status_bar.showMessage(f'Program starttime (UTC) {self.starttime.strftime("%Y-%m-%d %H:%M:%S")} | '
f'Refresh period: {self.refresh_period}s | '
f'Showing data of last {timespan}')
status_bar.showMessage(self.status_message)
def fill_table(self):
""" Fills the table with most recent information. Executed after execute_qc thread is done or on refresh. """
# fill status bar first with new time
self.fill_status_bar()
for col_ind, check_key in enumerate(self.survBot.keys):
for row_ind, st_id in enumerate(self.survBot.station_list):
status_dict, detailed_dict = self.survBot.analysis_results.get(st_id)
for row_ind, nwst_id in enumerate(self.survBot.station_list):
status_dict = self.survBot.analysis_results.get(nwst_id)
status = status_dict.get(check_key)
detailed_message = detailed_dict.get(check_key)
if check_key == 'last active':
bg_color = self.get_time_delay_color(status)
elif check_key == 'temp':
bg_color = self.get_temp_color(status)
if not type(status) in [str]:
status = str(status) + deg_str
else:
statussplit = status.split(' ')
if len(statussplit) > 1 and statussplit[0] == 'WARN':
x = int(status.split(' ')[-1].lstrip('(').rstrip(')'))
bg_color = self.colors_dict.get('WARNX')(x)
else:
bg_color = self.colors_dict.get(status)
if not bg_color:
bg_color = self.colors_dict.get('undefined')
message, detailed_message = status.get_status_str()
dt_thresh = [timedelta(seconds=sec) for sec in self.dt_thresh]
bg_color = get_bg_color(check_key, status, dt_thresh)
if check_key == 'temp':
if not type(message) in [str]:
message = str(message) + deg_str
# Continue if nothing changed
text = str(status)
text = str(message)
cur_item = self.table.item(row_ind, col_ind)
if cur_item and text == cur_item.text():
if not self.parameters.get('track_changes') or self.clear_on_refresh:
@@ -266,9 +252,9 @@ class MainWindow(QtWidgets.QMainWindow):
# Create new data item
item = QtWidgets.QTableWidgetItem()
item.setText(str(status))
item.setText(str(message))
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
cur_item = self.table.item(row_ind, col_ind)
@@ -292,24 +278,6 @@ class MainWindow(QtWidgets.QMainWindow):
# table filling/refreshing done, set clear_on_refresh to False
self.clear_on_refresh = False
def get_time_delay_color(self, dt):
""" Set color of time delay after thresholds specified in self.dt_thresh """
dt_thresh = [timedelta(seconds=sec) for sec in self.dt_thresh]
if dt < dt_thresh[0]:
return self.colors_dict.get('OK')
elif dt_thresh[0] <= dt < dt_thresh[1]:
return self.colors_dict.get('WARN')
return self.colors_dict.get('FAIL')
def get_temp_color(self, 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]:
return self.colors_dict.get('undefined')
cmap = matplotlib.cm.get_cmap(cmap)
val = (temp - vmin) / (vmax - vmin)
rgba = [int(255 * c) for c in cmap(val)]
return rgba
def set_font_bold(self, item):
""" Set item font bold """
f = item.font()
@@ -341,12 +309,17 @@ class MainWindow(QtWidgets.QMainWindow):
vheader.setSectionResizeMode(index, QtWidgets.QHeaderView.Stretch)
def plot_stream(self, item):
st_id, check = item.data(QtCore.Qt.UserRole)
st = self.survBot.data.get(st_id)
nwst_id, check = item.data(QtCore.Qt.UserRole)
st = self.survBot.data.get(nwst_id)
if st:
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)
# 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):

View File

@@ -1,14 +0,0 @@
#!/bin/bash
ulimit -s 8192
#$ -l os=*stretch
##$ -cwd
#$ -pe smp 1
##$ -q "*@minos15"
export PYTHONPATH="$PYTHONPATH:/home/marcel/git/"
source /opt/anaconda3/etc/profile.d/conda.sh
conda activate py37
python /home/marcel/git/survBot/survBotGUI.py

248
utils.py Normal file
View File

@@ -0,0 +1,248 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import matplotlib
import numpy as np
from obspy import Stream
def get_bg_color(check_key, status, dt_thresh=None, hex=False):
message = status.message
if check_key == 'last active':
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_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)
if not bg_color:
bg_color = get_color('undefined')
if hex:
bg_color = '#{:02x}{:02x}{:02x}'.format(*bg_color[:3])
return bg_color
def get_color(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),}
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), }
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]:
return get_color('WARN')
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]:
return get_color('undefined')
cmap = matplotlib.cm.get_cmap(cmap)
val = (temp - vmin) / (vmax - vmin)
rgba = [int(255 * c) for c in cmap(val)]
return rgba
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 """
# make a copy
st = Stream()
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)
# 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
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 set_axis_ylabels(fig, parameters, verbosity=0):
"""
Adds channel names to y-axis if defined in parameters.
"""
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')
return
for channel_name, ax in zip(names, fig.axes):
if channel_name:
ax.set_ylabel(channel_name)
def set_axis_color(fig, color='0.8'):
"""
Set all axes of figure to specific color
"""
for ax in fig.axes:
for key in ['bottom', 'top', 'right', 'left']:
ax.spines[key].set_color(color)
def set_axis_yticks(fig, parameters, verbosity=0):
"""
Adds channel names to y-axis if defined in parameters.
"""
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.')
return
for ytick_tripple, ax in zip(ticks, fig.axes):
if not ytick_tripple:
continue
ymin, ymax, step = ytick_tripple
yticks = list(np.arange(ymin, ymax + step, step))
ax.set_yticks(yticks)
ax.set_ylim(ymin - 0.33 * step, ymax + 0.33 * step)
def plot_axis_thresholds(fig, parameters, verbosity=0):
"""
Adds channel thresholds (warn, fail) to y-axis if defined in parameters.
"""
if verbosity > 0:
print('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 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 type(warn_thresh in (float, int)):
ax.axhline(warn_thresh, **kwargs)

97
write_utils.py Normal file
View File

@@ -0,0 +1,97 @@
from base64 import b64encode
from datetime import timedelta
def _convert_to_textstring(lst):
return '\n'.join(lst)
def get_html_table_title(parameters):
title = get_print_title_str(parameters)
return f'<h3>{title}</h3>\n'
def get_html_text(text):
return f'<p>{text}</p>\n'
def get_html_header(refresh_rate=10):
header = ['<!DOCTYPE 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}" >',
'<meta charset="utf-8">',
'<meta name="viewport" content="width=device-width, initial-scale=1">',
'<body>']
header = _convert_to_textstring(header)
return header
def get_mail_html_header():
header = ['<html>',
'<head>',
'</head>',
'<body>']
header = _convert_to_textstring(header)
return header
def init_html_table():
return '<table style="width:100%">\n'
def finish_html_table():
return '</table>\n'
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 = ' '
row_string += default_space + '<tr>\n'
for item in items:
text = item.get('text')
if item.get('bold'):
text = '<b>' + text + '</b>'
if item.get('italic'):
text = '<i>' + text + '</i>'
tooltip = item.get('tooltip')
font_color = item.get('font_color')
hyperlink = item.get('hyperlink')
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 ''
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):
timespan = parameters.get('timespan') * 24 * 3600
tdelta_str = str(timedelta(seconds=int(timespan))).replace(', 0:00:00', '')
return f'Analysis table of router quality within the last {tdelta_str}'