Aufgaben-Organisation mit einfachen GUIs

Autor: Gerd Raudenbusch
Stand: 28.06.2026

Für stupide, sich wiederholende Prozesse gibt es in der digitalen Welt zwei Möglichkeiten: man dokumentiert diese Aufgaben oder man automatisiert diese Aufgaben als Programm, als Skript oder als Shell-Erweiterung. Wenn sich solche Hilfsprogramme nach einiger Zeit zu einer Vielzahl angehäuft haben, und man ihre Namen zunehmend vergisst, entsteht schließlich der Wunsch, über einen organisierten Zugriff auf die Funktionen in Form einer Benutzer-Oberfläche zu verfügen, um sich die Arbeit zu erleichtern. Der folgende Artikel hierzu stellt drei Möglichkeiten vor.

Homeassistant

Home Assistant ist eigentlich eine Open-Source-Software für eine Smart-Home-Steuerung, mit der Geräte verschiedstener Hersteller zentral in einer Oberfläche verwaltet und automatisiert werden können. Heimautomation wird vorzugsweise im eigenen Netzwerk betrieben, sodass viele Funktionen von Home Assistant per Design ohne Cloud nutzbar sind.

Das beliebteste und meist verwendete Steuerprotokoll für den Home Assistant ist mit großem Abstand MQTT, weil es datensparsam und einfach zu verwenden ist. Beim Versuch, sich anspruchsvollere Wünsche der Datenintegration in die GUI des Homeassistant zu erfüllen, stößt man jedoch mitunter auch an Grenzen des mit YAML adaptierbaren Frameworks, die nur mit gewissem Aufwand zu überwinden sind. Nichtsdestotrotz ist Home Assistant sehr gut dafür geeignet, als eine solche Oberflächt zu fungieren und die meisten Entwicklungsaufgaben lassen sich relativ intuitiv mit ihm lösen.

streamlit

Streamlit ist ein Python-Framework zum schnellen Erstellen interaktiver Web-Apps, besonders für Datenanalyse, Dashboards und Machine-Learning-Tools. Mit Streamlit lässt sich mit sehr wenig Code eine Web-App bauen, ohne viel Frontend-Programmierung mit HTML, CSS oder JavaScript zu benötigen. Streamlit lässt sich ganz einfach dockern, z.B. mit dem folgenden Shell-Skript streamlit.sh :

#!/bin/bash
tag="latest"
if [ "${2}" != "" ]; then
    tag="${2}"
fi
name=$(basename ${0} |cut -d "." -f1)
img="3x3cut0r/streamlit"
options="-d \
    --privileged \
    -e TZ=\"Europe/Berlin\" \
        -v /opt/streamlit:/app \
        -p 8501:8501/tcp
    --restart=unless-stopped \
        -m 1g"

#########################
echo "Container: ${name}"
if [ "${1}" == "start" ]; then
    echo "Starting"
    docker run --name ${name} ${options} ${img}:${tag}
elif [ "${1}" == "stop" ]; then 
    echo "Stopping"
    docker rm -f ${name}
elif [ "${1}" == "restart" ]; then
    docker rm -f ${name} && docker run --name ${name} ${options} ${img}:${tag}
elif [ "${1}" == "login" ]; then
    docker exec -it ${name} /bin/bash
elif [ "${1}" == "commit" ]; then  
        echo "Committing"
    docker commit ${name} ${img}:${tag}
elif [ "${1}" == "pull" ]; then
        docker pull ${img}:${tag}
elif [ "${1}" == "load" ]; then
    echo "Loading"
    docker load < ${name}.tar.gz
elif [ "${1}" == "save" ]; then
    echo "Saving"
    docker save ${img}:${tag} | gzip > ${name}.tar.gz
elif [ "${1}" == "push" ]; then
    docker image push ${img}:${tag}
else
    echo "Unknown : ${1}"
    exit 1
fi

Mit ./streamlit.sh pull wird das Image vom Docker Hub gezogen, mit ./streamlist.sh restart wird der Container gestartet. Das Verzeichnis /app im Docker wird nach außen auf /opt/streamlit geleitet, wo das Skript streamlit_app.py zum Einsprung gesucht wird, sobald im Web-Browser http://<deine-ip>:8501 aufgerufen wird.

Dieses könnte z.B. so aussehen:

# https://github.com/streamlit/streamlit
# https://streamlit.io

import streamlit as st
import subprocess
import html
import re
st.set_page_config(page_title="Heimnetz", layout="wide")

# Hilfsfunktion zur Befehlsausführung auf der Shell
def run_shell_command(command):
    full_befehl = f"""{command}"""
    
    result = subprocess.run(
        full_befehl,
        shell=True,
        executable='/bin/bash',
        capture_output=True,
        text=True,
        timeout=60
    )
    return result.stdout, result.stderr, result.returncode

# Hilfsfunktion HTML-Konverter
def text_to_html_table(text):
    lines = text.strip().split('\n')
    html_table = '<table style="border: 1px solid #ccc; border-collapse: collapse;">'    
    for i, line in enumerate(lines):
        if not line.strip():
            continue
        columns = line.strip().split()
        if i == 0:
            html_table += '<thead><tr>'
            for col in columns:
                html_table += f'<th style="border: 1px solid #ccc; padding: 8px; background-color: #f0f0f0;">{html.escape(col)}</th>'
            html_table += '</tr></thead>'
        else:
            html_table += '<tr>'
            for col in columns:
                html_table += f'<td style="border: 1px solid #ccc; padding: 8px;">{html.escape(col)}</td>'
            html_table += '</tr>'    
    html_table += '</table>'
    return html_table

# Definition des Seitenleisten-Menüs
if "active_operation" not in st.session_state:
    st.session_state.active_operation = "Heimnetz"
st.sidebar.title("Operationen")
operation = st.sidebar.radio(
    "Wähle Operation:",
    ["Heimnetz","LAN-Geräteliste"],
    index=0 if st.session_state.active_operation == "Heimnetz" else 1 if st.session_state.active_operation == "LAN-Geräteliste" else 2
)
st.session_state.active_operation = operation
st.title(operation)

# ════════════════════════════════════════════════════════════
# ═══ Seite "Heimnetz" ═══
# Beispiel für eine statische Landing-Page im Heimnetz
# ════════════════════════════════════════════════════════════

if operation == "Heimnetz":
    st.markdown("<div style='border:1px solid #ddd;padding:20px;margin:20px 0;border-radius:10px;'>", unsafe_allow_html=True)
    st.markdown("<h2>Heimnetz</h2>", unsafe_allow_html=True)
    st.markdown("""<h3>Medienserver</h3>
<table>
<tr><td><a href="http://<Deine-Jelly-Url>:8096/web/#/home.html">Jellyfin Heimkino</a></td>
<td><a href="https://jellyfin.org/downloads/clients/">Mobile Apps</a></td>
</tr>
<tr><td><a href="http://<Deine-Navidrome-Url>:4533/app/#/login">Navidrome Musik Streaming Server</a></td>
<td><a href="https://www.navidrome.org/apps/">Mobile Apps</a></td></tr>
</table>

<!-- usw. -->

<h3>Heimnetz Infrastruktur</h3>

<table>
<tr><td><a href="http://<IoT-URL1>">IoT-Gerät1</a></td></tr>
<tr><td><a href="http://<IoT-URL2>">IoT-Gerät2</a></td></tr>
<tr><td><a href="http://<IoT-URL2>">IoT-Gerät3</a></td></tr>
</table>

<!-- usw. -->

<h3>Heimnetz Routing</h3>
<table>
<tr><td><a href="http://<Router-URL1>">Router 1</a></td></tr>
<tr><td><a href="http://<Router-URL2>">Router 2</a></td></tr>
<tr><td><a href="http://<Repeater-URL1>">Repeater 1</a></td></tr>
<tr><td><a href="http://<Repeater-URL2>">Repeater 2</a></td></tr>
<tr><td><a href="http://<Switch-URL1>">Switch 1</a></td></tr>
<tr><td><a href="http://<Switch-URL2>">Switch 2</a></td></tr>

<!-- usw. -->

</table>""", unsafe_allow_html=True)
    st.markdown("</div>", unsafe_allow_html=True)

# ════════════════════════════════════════════════════════════
# ═══ LAN-Geräteliste ═══
# Beispiel für eine dynamische Seite: 
#          Abfrage der Geräteliste der Fritzbox 
#          und Darstellung als Tabelle
# ════════════════════════════════════════════════════════════

if operation == "LAN-Geräteliste":
    st.markdown("<div style='border:1px solid #ddd;padding:20px;margin:20px 0;border-radius:10px;'>", unsafe_allow_html=True)
    st.markdown("<h3>LAN-Geräteliste</h3>", unsafe_allow_html=True)
    if st.button("Geräteliste abfragen", type="primary"):
        # Das Skript "routerscan" liegt auf dem Host 
        # unter /opt/streamlit und macht die Fritzbox-Abfrage
        raw_stdout, raw_stderr, errcode = run_shell_command("/app/routerscan") 
        if errcode == 0:
            output=text_to_html_table(raw_stdout)
        else:
            output="Error: "+raw_stderr
        st.markdown(output, unsafe_allow_html=True)
    st.markdown("</div>", unsafe_allow_html=True)

# ════════════════════════════════════════════════════════════
# ═══ Fusszeile ═══
# Beispiel für eine Fußzeile: 
#          Darstellung des Internet-Verbindungsstatus
# ════════════════════════════════════════════════════════════

raw_stdout, raw_stderr, errcode = run_shell_command("ping -c1 example.org 2>/dev/null | grep -c ' 0% packet loss' | tr -d '\n'")
if raw_stdout == "1":
    bottomline="<center><span style='display: block; color: #000; background-color: #aaffaa;'>Internet: verbunden</span></center>"
else:
    bottomline="<center><span style='display: block; color: #000; background-color: #ffaaaa;'>Internet: getrennt</span></center>"

st.markdown("""
<style>
.footer-caption {
    font-size: 24px !important;
    font-color: #000000;
    line-height: 1.4;

    position: fixed;
    left: 0rem;
    right: 0;
    bottom: 0;
    height: 32px;
    line-height: 15px;
    padding: 0 1rem;
    border-top: 1px solid #ddd;
    z-index: 9999;
}
</style>
<div class="footer-caption">""" + bottomline + """</div>""", unsafe_allow_html=True)

Fritzbox-Clientliste als Beispiel-Datenquelle für einen dynamischen Streamlit-Inhalt

#!/bin/bash
FRITZ_IP="http://<Deine-Fritzbox-IP>"
FRITZ_USERNAME="Dein-Username"
# Das Passwort hardzucoden ist nie 
# eine besonders gute Lösung!
#
# 1. Alternative: secret-tool
# Vorher einmalig
# secret-tool store --label='Meine-Fritzbox' service meine-fritzbox account <Fritbox-Benutzername>
#FRITZ_PASSWORT=$(secret-tool lookup service meine-fritzbox account ${FRITZ_USERNAME})
#
# 2. Alternative: KeepassXC:
#FRITZ_PASSWORT=$(keepassxc-cli show /mein-pfad/zum/keepass-safe.kdbx "Meine-Fritzbox" -s
FRITZ_PASSWORT="Dein-Passwort" # PoC

function routerlist() {
  local user="$1"
  local pass="$2"
  local address="${3:-192.168.178.1}"
  if [[ -z "$user" || -z "$pass" ]]; then
    >&2 echo "Usage: $0 USER PASS [ADDRESS]"
    return 1
  fi

  # mit python3 -m venv angelegt und vorbereitet
  /app/fritzbox_env/bin/python3 - <<'PY' "$address" "$user" "$pass" 
import sys
from fritzconnection import FritzConnection
from fritzconnection.lib.fritzhosts import FritzHosts

ADDRESS = sys.argv[1]
USER = sys.argv[2]
PASS = sys.argv[3]
fc = FritzConnection(address=ADDRESS, user=USER, password=PASS)
hosts = FritzHosts(fc)
for host in hosts.get_hosts_info():
    if host.get("status"):
        print(f'{host.get("name","")} {host.get("mac","")} {host.get("ip","")}')
PY
}

( echo "Name|MAC|IP"; routerlist "${FRITZ_USER}" "${FRITZ_PASSWORT}" "${FRITZ_IP}" ) | column -ts "|"

Fazit: Mit Streamlit lässt sich sehr leicht eine WebGUI bauen, um durch Skripte oder Programme automatisierte Aufgaben im Heimnetz übersichtlich und ansprechend zu organisieren.

dialog

In manchen Fällen ist eine WebGUI jedoch nicht "Lowlevel" genug, z.B. wenn:

Dann ist das Tool dialog die bessere Wahl. Es ist ein Terminal-UI-Tool für Shell-Skripte, welches Boxen für Meldungen, Eingaben, Menüs, Passwortfelder, Checkboxen oder Kalender anzeigen kann und sich mit apt install dialog leicht installieren lässt. Damit lassen sich auf einfache Weise interaktive Text-Oberflächen bauen, ohne gleich eine Web-App oder GUI zu benötigen.

Beispiel-Code:

#!/bin/bash

######################################
# Color support
######################################


if test -t 1; then
    ncolors=$(tput colors)
    if test -n "$ncolors" && test $ncolors -ge 8; then
        BLACK=$(tput setaf 0)
        RED=$(tput setaf 1)
        GREEN=$(tput setaf 2)
        YELLOW=$(tput setaf 3)
        BLUE=$(tput setaf 4)
        MAGENTA=$(tput setaf 5)
        CYAN=$(tput setaf 6)
        WHITE=$(tput setaf 7)

        BOLD=$(tput bold)
        RESET=$(tput sgr0)
    fi
fi

######################################
# Logging colorful to stderr
######################################


log() {
    >&2 echo "${1}"
}

printInfo() {
    log "${BOLD}${WHITE}${1}${RESET}"
}

printWarning() {
    log "${BOLD}${YELLOW}${1}${RESET}"
}

printSuccess() {
    log "${BOLD}${GREEN}${1}${RESET}"
}

printError() {
    log "${BOLD}${RED}${1}${RESET}"
}

######################################
# Dialog box menus and messages
######################################

assureparam() {
    local paramname="${3}"
    local param="${1}"
    local available="${2}"
    local rettype="${4}"
    if [[ ! -n "${param}" || " ${available} " != *" ${param} "* ]]; then
        IFS=' ' read -ra available_array <<<"${available}"
        items=()
        if [ "${rettype}" == "text" ]; then
            param=$(dialog --inputbox "${paramname}" 8 60 "${available}" 3>&1 1>&2 2>&3)
        elif [ "${rettype}" == "multi" ]; then
            for item in "${available_array[@]}"; do
                #items+=("${item:0:1}" "${item:1}" off)
                items+=("${item}" "" off)
            done
            param=$(dialog --separate-output --checklist "${paramname}" 30 90 80 "${items[@]}" 2>&1 >/dev/tty)
        else
            for item in "${available_array[@]}"; do
                if [ "${rettype}" == "char" ]; then
                    items+=("${item:0:1}" "${item:1}")
                else
                    items+=("${item}" "")
                fi
            done
            param=$(dialog --stdout --clear --backtitle "${paramname}" --title "${paramname}" --menu "PgUp/PgDn: Navigate, Return: Select" 30 90 80 "${items[@]}")
        fi
    fi
    echo "${param}" | tr '\n' ' ' | sed -E "s/ +$//g"
}

messagebox() {
    dialog --msgbox "${1}" 7 55
}

logbox() {
    dialog --hline "PgUp/PgDn: Scroll, ESC: Exit" --scrollbar --msgbox "${1}" 40 150
}

pause() { 
    if [ "${1}" == "" ]; then
        echo "Press any key to continue.";
    else
        echo "${1}";
    fi;
    read
}

######################################
# Some useful example functions
######################################

# Doing packet update
update() {
    sudo apt update && sudo DEBIAN_FRONTEND=noninteractive apt -yq --with-new-pkgs upgrade && sudo apt -yq autoremove
}

# Doing a WAN scan 192.168.<yournet>.xxx
wanscan() {
    sudo nmap -sn 192.168.${1}.1-254
}

perf-disks() {
    iostat -xz 1
}

perf-docker() {
    docker stats
}

perf-top() {
    top
}

######################################
# Example menu
######################################


example-menu() {
        while true; do
                cmd=(dialog --clear --backtitle "Example menu" --title "Example menu" --menu "Choice:" 22 120 12)
                options=(
                        1 "Show current network informations"
                        2 "Update APT packages"
                        3 "Make WAN network scan"
                        4 "Show disk performance"
                        5 "Show docker status"
                        6 "Show running proccesses"
                )
                choice=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
                case "$choice" in
                1) ifinfo && pause || pause "Error occured." ;;
                2) update && pause || pause "Error occured." ;;
                3) wanscan 1 && pause || pause "Error occured." ;;
                4) perf-disks && pause || pause "Error occured." ;;
                5) perf-docker && pause || pause "Error occured." ;;
                6) perf-top && pause || pause "Error occured." ;;
                *) clear; return ;;
                esac
        done
}

Das obige Bash-Skript lässt sich einfach inkludieren mit source <Skriptname>, oder als Shellerweiterung in .bash_aliases oder .bashrc einbinden, und mit example-menu in der Konsole aufrufen.

Zusammenfassung


Zurück zur Hauptseite