67 Commits

Author SHA1 Message Date
0ee3a27733 [bugfix] HTML faild validator.w3.org checks 2025-04-02 18:07:10 +02:00
e2df92e6b4 [update] adds mass activation checks to StationQC class
Implements checks for mass channel activity to ensure proper functionality.

Introduces methods to verify if mass channels are active and to set errors when they are not connected.

Enhances reliability of data logging by avoiding unnecessary processing when mass channels are inactive.
2025-04-02 17:28:25 +02:00
8fa96d03d5 Merge branch 'develop' into feature/docker 2025-03-25 12:03:47 +01:00
4e25190fbc Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	parameters.yaml
2025-03-25 11:42:18 +01:00
8ac501e8dc [update] add parameter to disable PowBox usage for selected stations also to parameters.yaml, updated style sheets for html 2025-03-25 11:39:17 +01:00
fcba73fcc5 [bugfix] fix bug in connect_to_mail_server function 2025-03-21 22:42:23 +01:00
16fbbde3d9 [bugfix] fixed call of function connect_to_mail_server 2025-03-21 17:48:49 +01:00
b986da5fef [minor] fixed some typos and missing import 2025-03-21 17:43:41 +01:00
a6c1570539 [update] merge branch 'feature/docker' into develop
- add mail server configuration options to use authentication with user/password combination wit SSL or TLS connection
- add support for running survBot in a Docker container
- update gridengine submit script submit_bot.sh
- update stylesheets to latest changes
- fixed some typos
2025-03-21 16:20:02 +01:00
aaadff6306 [update] README.md
- add section on running survBot in a Docker container
- add explaination of mal server settings in parameters.yaml
2025-03-21 16:14:21 +01:00
b46802a75e [bugfix] updated ownership setting in Dockerfile 2025-03-21 15:42:33 +01:00
080e73c1db [update] update Dockerfile
- mojor update of Dockerfile
- reverted location of simulate_fail.json file for simulating errors, e.g. to test sending e-mails
2025-03-21 14:22:59 +01:00
8a7e402ec5 reverted deletion of survBotGUI.py 2025-03-21 14:08:00 +01:00
3f07b7bcd0 [minor] update Dockerfile and parameters.yaml
- In the Dockerfile, added a new parameter "-parfile" with value "conf/parameters.yaml" to the CMD command in survBot.py.
- In parameters.yaml, made changes to the EMAIL section:
  - Added comments explaining how to specify mail server and credentials.
  - Added auth_type field with value "SSL".
  - Updated port field to 465 for SSL.
  - Updated user and password fields to read from environment variables or docker secrets.
  - Added comments explaining how to specify mail recipients, sender, and blacklists.
  - Moved location of simulate_fail.json to conf/simulate_fail.json for easier Docker integration
2025-03-21 12:41:50 +01:00
cf12500ec2 [minor] refactor mail server connection code
- Added `connect_to_mail_server` function to handle mail server connection
- Moved code for connecting to the mail server from `StationQC` class to `connect_to_mail_server`
- Updated references to use `connect_to_mail_server` in `StationQC` class
- Created new function `get_credential` to retrieve credentials from Docker secrets or environment variables
2025-03-21 12:36:52 +01:00
43912135e9 Update email parameters in parameters.yaml and modify SMTP connection in survBot.py
- Update mailserver, port, user, password, and sender in parameters.yaml
- Modify SMTP connection in survBot.py to support starttls connection if server is not "localhost"
- Read password from docker secret or environment variable if set to 'DOCKER' or 'ENV' respectively
2025-03-20 16:11:27 +01:00
3e47f4275b Add bind9-host and iputils-ping to Docker image requirements.
- Install bind9-host and iputils-ping packages in the Docker image.
- Create a new file "requirements.txt" with the specified package versions.
2025-03-20 15:58:22 +01:00
f4605b146b [feature] run survBot in Docker container
- version 0.2-docker
- add Dockerfile
- update paramters.yaml to use logo.png
- update stylesheet to reflect latest changes
- removed survBotGUI.py which is not needed in Docker container
2025-03-20 11:38:54 +01:00
0ce41e5654 Change resource limits and logging paths in submit_bot.sh
- Update h_vmem limit to 2.5G
- Add mem limit of 2.5G
- Set h_stack to INFINITY
- Change output log path to /data/www/~kasper/survBot/survBot_bg.log
- Change error log path to /data/www/~kasper/survBot/survBot_bg.err
- Enable email notifications for errors
- Update HTML output directory path
2025-03-20 11:30:44 +01:00
3dbba37fe9 [minor] update stations_blacklist in parameters.yaml
- Add "ATHR" and "HAVD" to the list of excluded stations
- No changes to other parameters
2025-03-20 10:33:37 +01:00
9626a4c88d [minor] add station GR27 to station blacklist in parameters.yaml
- Updated stations_blacklist to exclude stations DOMV, EREA, GR19, GR27, LAKA, LFKM and TEST
- Changed the description of datapath to clarify it as the path to SDS data archive.
- Fixed some typos.
2025-03-20 10:00:25 +01:00
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
14 changed files with 1188 additions and 355 deletions

6
.gitignore vendored
View File

@@ -211,3 +211,9 @@ flycheck_*.el
/network-security.data /network-security.data
/__simulate_fail.json
/mailing_list.yaml
.vscode/
*.code-workspace

40
Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
FROM python:3
# metadata
LABEL maintainer="Kasper D. Fischer <kasper.fischer@rub.de>"
LABEL version="0.2-docker"
LABEL description="Docker image for the survBot application"
# install required system packages
RUN apt update && apt install -y bind9-host iputils-ping
# create user and group and home directory
RUN groupadd -r survBot && useradd -r -g survBot survBot
RUN mkdir -p /home/survBot && chown -R survBot:survBot /home/survBot
# change working directory
WORKDIR /usr/src/app
# install required python packages
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
pip install --no-cache-dir --requirement /tmp/requirements.txt
# copy application files
COPY survBot.py utils.py write_utils.py LICENSE README.md ./
# copy configuration files
VOLUME /usr/src/app/conf
COPY parameters.yaml mailing_list.yam[l] simulate_fail.jso[n] conf/
RUN ln -s conf/simulate_fail.json simulate_fail.json
# copy www files
VOLUME /usr/src/app/www
COPY logo.pn[g] stylesheets/desktop.css stylesheets/mobile.css www/
RUN ln -s www/survBot_out.html www/index.html
# change ownership of working directory
RUN chown -R survBot:survBot /usr/src/app
# run the application as user survBot
USER survBot
CMD [ "python", "./survBot.py", "-html", "www", "-parfile", "conf/parameters.yaml" ]

View File

@@ -1,9 +1,9 @@
# survBot # 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 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. by analyzing contents of a Seiscomp data archive.
## Requirements ## Requirements
@@ -40,8 +40,53 @@ The GUI can be loaded via
python survBotGui.py python survBotGui.py
``` ```
### Docker
To run the program in a Docker container, first build the image:
```shell script
docker build -t survbot .
```
Then run the container:
```shell script
docker run -v /path/to/conf-dir:/usr/src/app/conf -v /path/to/output:/usr/src/app/www survbot
```
The directory `/path/to/conf-dir` should contain the `parameters.yaml` file, and the directory `/path/to/output` will contain the output HTML files.
### Configuration of the e-mail server settings
The e-mail server settings can be configured in the `parameters.yaml` file. The following settings are available:
* `mailserver`: the address of the mail server
* `auth_type`: the authentication type for the mail server (`None`, `SSL`, `TLS`)
* `port`: the port of the mail server
* `user`: the username for the mail server (if required)
* `password`: the password for the mail server (if required)
The `user` and `password` fields are optional, and can be left empty if the mail server does not require authentication. The `auth_type` field can be set to `None` if no authentication is required, `SSL` if the mail server requires SSL authentication, or `TLS` if the mail server requires TLS authentication. If the `user` or `password` fields are set to `Docker` ore `ENV` the program will try to read the values from the docker secrets `mail_user` and `mail_password` or environment variables `MAIL_USER` and `MAIL_PASSWORD` respectively. Docker secrets are only available in Docker Swarm mode, i.e. if the program is run as a service.
## Version Changes
### 0.2
* 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
### 0.2-docker
* added Dockerfile for easy deployment
* added more settings for connection to a mail server
## Staff ## Staff
Original author: M.Paffrath (marcel.paffrath@rub.de) Original author: M.Paffrath (<marcel.paffrath@rub.de>)
Contributions: Kasper D. Fischer (<kasper.fischer@rub.de>)
November 2022 Jan 2025

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-docker"

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@provider.com":
# - 1Y.GR03
#"kasper.fischer@rub.de":
# - 1Y.GR01

View File

@@ -1,18 +1,18 @@
# Parameters file for Surveillance Bot # Parameters file for Surveillance Bot
datapath: "/data/SDS/" # SC3 Datapath datapath: "/data/SDS/" # path to SDS data archive
networks: ["1Y", "HA"] # select networks, list or str networks: ["1Y", "HA", "MK"] # select networks, list or str
stations: "*" # select stations, list or str stations: "*" # select stations, list or str
locations: "*" # select locations, 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: ["ATHR", "DOMV", "EREA", "GR19", "GR27", "HAVD", "LAKA", "LFKM", "TEST"] # exclude these stations
stations_blacklist: ["TEST", "EREA"] # exclude these stations
networks_blacklist: [] # exclude these networks networks_blacklist: [] # exclude these networks
interval: 60 # Perform checks every x seconds 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 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) 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 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) 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) reread_parameters: True # reread parameters file (change parameters on runtime, not for itself/GUI refresh/datapath)
@@ -40,13 +40,88 @@ POWBOX:
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
critical_temp: 65 # max temperature for critical warning (fail)
low_volt: 12 # min voltage for low voltage warning low_volt: 12 # min voltage for low voltage warning
high_volt: 14.8 # max voltage for over voltage warning high_volt: 14.8 # max voltage for over voltage warning
unclassified: 5 # min voltage samples not classified for warning unclassified: 5 # min voltage samples not classified for warning
clockquality_warn: 90 # clock quality ranges from 0 % to 100 % with 100 % being the best level max_vm_warn: 1.5 # threshold for mass offset (warn), fail)
clockquality_fail: 70 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 THRESHOLDS, 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 --------------------------------------------------------- # ---------------------------------------- OPTIONAL PARAMETERS ---------------------------------------------------------
# deactivate powbox surveillance for stations (e.g. for solar powered), key: station, value: status message (abbr.)
no_pbox_stations: {'GR33': 'SOL', 'GR27B': 'DCN', 'RP01': 'noPBox', 'RP02': 'noPBox', 'RP03': 'noPBox', 'RP04': 'noPBox', 'RP05B': 'noPBox', 'RP06': 'noPBox', 'RP07': 'noPBox', 'RP08': 'noPBox'}
# deactivate mass position surveillance for stations without connected mass channels (e.g. STS-2 instruments), key: station, value: status message (abbr.)
no_mass_stations: {'BIA': 'DCN', 'KNEZ': 'DCN', 'KRUS': 'DCN', 'OHR': 'DCN', 'OTOV': 'DCN', 'SKO': 'DCN', 'STIP': 'DCN', 'VAY': 'DCN', 'ZUPA': 'DCN'}
# add links to html table with specified key as column and value as relative link, interpretable string 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) # nw (e.g. 1Y), st (e.g. GR01A), nwst_id (e.g. 1Y.GR01A)
@@ -54,34 +129,31 @@ add_links:
# for example: slmon: {"URL": "path/{nw}_{st}.html", "text": "link"} # for example: slmon: {"URL": "path/{nw}_{st}.html", "text": "link"}
slmon: {"URL": "../slmon/{nw}_{st}.html", "text": "show"} slmon: {"URL": "../slmon/{nw}_{st}.html", "text": "show"}
24h-plot: {"URL": "../scheli/{nw}/{st}.png", "text": "plot"} 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=osm"}
# html logo at page bottom (path relative to html directory)
html_logo: "logo.png"
# E-mail notifications # E-mail notifications
EMAIL: EMAIL:
mailserver: "localhost" # specify mail server and credentials
addresses: ["marcel.paffrath@rub.de", "kasper.fischer@rub.de"] # list of mail addresses for info mails # port, auth_type, user and password are only required if mailserver is not set to "localhost"
sender: "webmaster@geophysik.ruhr-uni-bochum.de" # mail sender # user and password can be set to "ENV" or "DOCKER" to read from environment variables or docker secrets
stations_blacklist: ['GR33'] # do not send emails for specific stations mailserver: "smtp.example.org" # mail server
auth_type: "SSL" # mail authentication type, can be "SSL", "TLS" or "None"
port: 465 # mail port, default 465 for SSL, 587 for TLS
user: "DOCKER" # mail user, read from environment variable if set to "ENV" or from docker secret if set to "DOCKER"
password: "DOCKER" # mail password, read from environment variable if set to "ENV" or from docker secret if set to "DOCKER"
# specify mail recipients, sender and blacklists
addresses: ["test@example.org"] # list of mail addresses for info mails
sender: "<test@example.org>" # mail sender
stations_blacklist: [] # do not send emails for specific stations
networks_blacklist: [] # do not send emails for specific network 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])
# names for plotting of the above defined parameter "channels" in the same order external_mail_list: "conf/mailing_list.yaml"
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]

36
requirements.txt Normal file
View File

@@ -0,0 +1,36 @@
Brotli==1.1.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
contourpy==1.3.1
cycler==0.12.1
decorator==5.2.1
fonttools==4.56.0
greenlet==3.1.1
h2==4.2.0
hpack==4.1.0
hyperframe==6.1.0
idna==3.10
kiwisolver==1.4.8
lxml==5.3.1
matplotlib==3.8.4
munkres==1.1.4
numpy==1.26.4
obspy==1.4.1
packaging==24.2
pillow==11.1.0
pip==25.0.1
pycparser==2.22
pyparsing==3.2.1
PySocks==1.7.1
python-dateutil==2.9.0.post0
PyYAML==6.0.2
requests==2.32.3
scipy==1.15.2
setuptools==75.8.2
six==1.17.0
SQLAlchemy==1.4.54
unicodedata2==16.0.0
urllib3==2.3.0
wheel==0.45.1
zstandard==0.23.0

View File

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

View File

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

View File

@@ -1,19 +1,24 @@
#!/bin/bash #!/bin/bash
ulimit -s 8192
#$ -l low #$ -l low
#$ -l h_vmem=5G #$ -l h_vmem=2.5G
#$ -l mem=2.5G
#$ -l h_stack=INFINITY
#$ -cwd #$ -cwd
#$ -pe smp 1 #$ -pe smp 1
#$ -N survBot_bg #$ -binding linear:1
#$ -l os=*stretch #$ -N survBot
#$ -o /data/www/~kasper/survBot/survBot_bg.log
#$ -e /data/www/~kasper/survBot/survBot_bg.err
#$ -m e
#$ -M kasper.fischer@rub.de
source /opt/anaconda3/etc/profile.d/conda.sh source /opt/anaconda3/etc/profile.d/conda.sh
conda activate py37 conda activate survBot
# environment variables for numpy to prevent multi threading # environment variables for numpy to prevent multi threading
export MKL_NUM_THREADS=1 export MKL_NUM_THREADS=1
export NUMEXPR_NUM_THREADS=1 export NUMEXPR_NUM_THREADS=1
export OMP_NUM_THREADS=1 export OMP_NUM_THREADS=1
python survBot.py -html '/data/www/~marcel/' python survBot.py -html '/data/www/~kasper/survBot'

File diff suppressed because it is too large Load Diff

View File

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

269
utils.py
View File

@@ -1,7 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging
import matplotlib import matplotlib
import numpy as np
import smtplib
import os
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): def get_bg_color(check_key, status, dt_thresh=None, hex=False):
@@ -10,10 +25,15 @@ def get_bg_color(check_key, status, dt_thresh=None, hex=False):
bg_color = get_time_delay_color(message, dt_thresh) bg_color = get_time_delay_color(message, dt_thresh)
elif check_key == 'temp': elif check_key == 'temp':
bg_color = get_temp_color(message) bg_color = get_temp_color(message)
elif check_key == 'mass':
bg_color = get_mass_color(message)
else: else:
if status.is_warn: if status.is_warn:
bg_color = get_color('WARNX')(status.count) bg_color = get_warn_color(status.count)
elif status.is_error: elif status.is_error:
if status.connection_error:
bg_color = get_color('disc')
else:
bg_color = get_color('FAIL') bg_color = get_color('FAIL')
else: else:
bg_color = get_color(message) bg_color = get_color(message)
@@ -26,18 +46,26 @@ def get_bg_color(check_key, status, dt_thresh=None, hex=False):
def get_color(key): def get_color(key):
# some GUI default colors # some old GUI default colors
colors_dict = {'FAIL': (255, 50, 0, 255), # colors_dict = {'FAIL': (255, 85, 50, 255),
'NO DATA': (255, 255, 125, 255), # 'NO DATA': (255, 255, 125, 255),
'WARN': (255, 255, 80, 255), # 'WARN': (255, 255, 80, 255),
'WARNX': lambda x: (min([255, 200 + x ** 2]), 255, 80, 255), # 'OK': (173, 255, 133, 255),
'OK': (125, 255, 125, 255), # 'undefined': (230, 230, 230, 255),
'undefined': (230, 230, 230, 255)} # 'disc': (255, 160, 40, 255),}
return colors_dict.get(key) 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): 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 isinstance(dt, type(dt_thresh[0])):
if dt < dt_thresh[0]: if dt < dt_thresh[0]:
return get_color('OK') return get_color('OK')
elif dt_thresh[0] <= dt < dt_thresh[1]: elif dt_thresh[0] <= dt < dt_thresh[1]:
@@ -45,9 +73,25 @@ def get_time_delay_color(dt, dt_thresh):
return get_color('FAIL') 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'): 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]:
if temp in COLORS_DICT.keys():
return get_color(temp)
return get_color('undefined') return get_color('undefined')
cmap = matplotlib.cm.get_cmap(cmap) cmap = matplotlib.cm.get_cmap(cmap)
val = (temp - vmin) / (vmax - vmin) val = (temp - vmin) / (vmax - vmin)
@@ -55,29 +99,61 @@ def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'):
return rgba 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 """ """ 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 # make a copy
if ch_units or ch_transf: st = Stream()
st = st.copy()
# modify trace for plotting by multiplying unit factor (e.g. 1e-3 mV to V) channels_dict = parameters.get('CHANNELS')
if ch_units:
for tr in st: # iterate over all channels and put them to new stream in order
channel = tr.stats.channel for index, ch_tup in enumerate(channels_dict.items()):
unit_factor = ch_units.get(channel) # 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: if unit_factor:
tr.data = tr.data * float(unit_factor) tr.data = tr.data * float(unit_factor)
# modify trace for plotting by other arithmetic expressions
if ch_transf: # apply transformations if provided
for tr in st: transform = channel_dict.get('transform')
channel = tr.stats.channel if transform:
transf = ch_transf.get(channel) tr.data = transform_trace(tr.data, transform)
if transf:
tr.data = transform_trace(tr.data, transf) # 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 return st
@@ -99,47 +175,156 @@ def transform_trace(data, transf):
elif operator_str == '/': elif operator_str == '/':
data = data / val data = data / val
else: else:
raise IOError(f'Unknown arithmethic operator string: {operator_str}') raise IOError(f'Unknown arithmetic operator string: {operator_str}')
return data 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. 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): if not names: # or not len(st.traces):
return return
if not len(names) == len(fig.axes): if not len(names) == len(fig.axes):
if verbosity: logging.info('Mismatch in axis and label lengths. Not adding plot labels')
print('Mismatch in axis and label lengths. Not adding plot labels')
return return
for channel_name, ax in zip(names, fig.axes): for channel_name, ax in zip(names, fig.axes):
if channel_name: if channel_name:
ax.set_ylabel(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. 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: if not ticks:
return return
if not len(ticks) == len(fig.axes): if not len(ticks) == len(fig.axes):
if verbosity: logging.info('Mismatch in axis tick and label lengths. Not changing plot ticks.')
print('Mismatch in axis tick and label lengths. Not changing plot ticks.')
return return
for ytick_tripple, ax in zip(ticks, fig.axes): for ytick_tripple, ax in zip(ticks, fig.axes):
if not ytick_tripple: if not ytick_tripple:
continue continue
ymin, ymax, step = ytick_tripple 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_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')
def get_credential(source, param):
"""
Retrieve a credential from a Docker secret or environment variable.
"""
if source == 'DOCKER':
try:
with open('/run/secrets/'+param.lower(), 'r') as f:
return f.read().strip()
except FileNotFoundError as e:
logging.error(f'Could not read from Docker secret at /run/secrets/{param.lower()}')
logging.error(e)
elif source == 'ENV':
try:
return os.environ.get(param.upper())
except Exception as e:
logging.error(f'Could not read from environment variable {param.upper()}')
logging.error(e)
# return source if no credential was found
return source
def connect_to_mail_server(mail_params):
"""
Connect to mail server and return server object.
"""
# get server from parameters
server = mail_params.get('mailserver')
# get auth_type from parameters
auth_type = mail_params.get('auth_type')
# set up connection to mail server
if auth_type == 'None':
s = smtplib.SMTP(server)
else:
# user and password from parameters, docker secret or environment variable
user = get_credential(mail_params.get('user'), 'mail_user')
password = get_credential(mail_params.get('password'), 'mail_password')
# create secure connection to server
if auth_type == 'SSL':
s = smtplib.SMTP_SSL(server, mail_params.get('port'))
elif auth_type == 'TLS':
s = smtplib.SMTP(server, mail_params.get('port'))
s.starttls()
else:
logging.error('Unknown authentication type. Mails can not be sent')
return
s.login(user, password)
return s

View File

@@ -1,48 +1,78 @@
from base64 import b64encode
from datetime import timedelta 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) 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): def get_html_text(text):
fobj.write(f'<p>{text}</p>\n') return f'<p>{text}</p>\n'
def write_html_header(fobj, refresh_rate=10): def get_html_header(refresh_rate=10):
header = ['<!DOCTYPE html>', header = ['<!DOCTYPE html>',
'<html>', '<html>',
'<head>', '<head>',
' <link rel="stylesheet" media="only screen and (max-width: 400px)" href="mobile.css" />', ' <title>SurvBot status</title>',
' <link rel="stylesheet" media="only screen and (min-width: 401px)" href="desktop.css" />', ' <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">',
f' <meta http-equiv="refresh" content="{refresh_rate}">',
' <meta charset="utf-8">',
' <meta name="viewport" content="width=device-width, initial-scale=1">',
'</head>', '</head>',
f'<meta http-equiv="refresh" content="{refresh_rate}" >',
'<meta charset="utf-8">',
'<meta name="viewport" content="width=device-width, initial-scale=1">',
'<body>'] '<body>']
for item in header: header = _convert_to_textstring(header)
fobj.write(item + '\n') return header
def init_html_table(fobj): def get_mail_html_header():
fobj.write('<table style="width:100%">\n') header = ['<html>',
'<head>',
'</head>',
'<body>']
header = _convert_to_textstring(header)
return header
def finish_html_table(fobj): def init_html_table():
fobj.write('</table>\n') return '<table style="width:100%">\n'
def write_html_footer(fobj): def finish_html_table():
footer = ['</body>', return '</table>\n'
'</html>']
for item in footer:
fobj.write(item + '\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 = ' ' default_space = ' '
fobj.write(default_space + '<tr>\n') row_string += default_space + '<tr>\n'
for item in items: for item in items:
text = item.get('text') text = item.get('text')
if item.get('bold'): if item.get('bold'):
@@ -50,16 +80,16 @@ def write_html_row(fobj, items, html_key='td'):
if item.get('italic'): if item.get('italic'):
text = '<i>' + text + '</i>' text = '<i>' + text + '</i>'
tooltip = item.get('tooltip') tooltip = item.get('tooltip')
color = item.get('color') font_color = item.get('font_color')
# check for black background of headers (shouldnt happen anymore)
color = '#e6e6e6' if color == '#000000' else color
hyperlink = item.get('hyperlink') 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') html_class = item.get('html_class')
class_str = f' class="{html_class}"' if html_class else '' 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}' row_string += 2 * default_space + f'<{html_key}{class_str} bgcolor="{color}" title="{tooltip}"' \
+ text + f'</{html_key}>\n') + f'style="color:{font_color}">{text_str}</{html_key}>\n'
fobj.write(default_space + '</tr>\n') row_string += default_space + '</tr>\n'
return row_string
def get_print_title_str(parameters): def get_print_title_str(parameters):