Zum Inhalt springen

Ein Plugin schreiben ​

Ein Plugin ist eine Datei mit einer Klasse. Der Marktplatz, die Leiste, die Suche und das Aktivieren lesen alles aus den Klassenfeldern — die Oberfläche kennt kein Plugin beim Namen.

Diese Seite ist für Entwickler. Wie man ein Plugin einschaltet, einrichtet und benutzt, steht in der Anleitung und in der Plugin-Referenz — zu jedem eingebauten Plugin gehört dort eine Seite, siehe „Sichtbar machen“.

Wo es liegt ​

OrtWofür
backend/plugins/<name>.py + eine Zeile in backend/plugins/catalog.py (builtin())Eingebaut, wird mit der Software ausgeliefert
%APPDATA%/MultiverseStreamDeck/plugins/<name>.pyEigenes, ohne Eingriff in den Quelltext. Wird beim Start gelesen; eine kaputte Datei steht im Marktplatz mit ihrer Fehlermeldung

Ein eigenes Plugin darf kein eingebautes ersetzen (der Name ist dann vergeben).

Eigene Plugins (Datei im Plugin-Ordner) ​

In der Datei in %APPDATA%/MultiverseStreamDeck/plugins/ gibt es keinen relativen Import: statt from .base import ... gilt

python
from backend.plugins.base import Params, Plugin, PluginError

Die ausgelieferte Software bringt nur mit, was das Backend selbst benutzt; von der Standardbibliothek fehlt, was niemand importiert (z. B. sqlite3). Ein eigenes Plugin, das mehr braucht, läuft aus dem Quellstand (.venv), nicht aus dem Paket.

Das Gerüst ​

python
from .base import Params, Plugin, PluginError


class BeispielPlugin(Plugin):
    name = "beispiel"            # Schlüssel, klein, ohne Leerzeichen; steht in den Belegungen
    label = "Beispiel"           # Anzeigename

    # --- Marktplatz ---
    description = "Ein, zwei Sätze: was es kann und wofür man es braucht."
    category = "tools"           # basis | audio | stream | home | system | tools | info | games
    icon = "beispiel"            # Schlüssel in `Marks` (Oberfläche); leer = der Name
    keywords = ("anderes wort", "synonym")   # wonach man noch sucht
    starter = False              # True: bei frischer Installation schon an
    core = False                 # True: lässt sich nicht abschalten (nur `deck`)

    needs_connection = False     # nichts zu verbinden -> Zustand "ready"

    settings_fields = ()         # Einrichtungsformular, siehe unten
    commands = (
        {
            "name": "tu_etwas",
            "label": "Etwas tun",
            "params": [{"name": "was", "label": "Was", "required": True}],
        },
    )

    async def execute(self, command: str, params: Params) -> None:
        match command:
            case "tu_etwas":
                ...
            case _:
                raise PluginError(f"unknown beispiel command: {command!r}")

Befehle (commands) ​

FeldBedeutung
name, labelSchlüssel und Anzeigetext
paramsFormularfelder: {"name", "label", "required"?, "multiline"?}
absolute: TruePasst auf einen Schieberegler; braucht ein Parameter value (0–100). Der Wert kommt vom Regler
relative: TruePasst auf einen Drehregler; braucht ein Parameter step, der mit den Rastungen multipliziert wird
state: "schluessel"Die Kachel leuchtet, wenn states() für diesen Schlüssel True sagt. Fehlt das Feld, ist die Kachel kein Schalter

Ein Befehl ohne absolute/relative ist eine reine Tastenaktion.

Einrichtung (settings_fields) ​

python
settings_fields = (
    {"name": "host", "label": "Adresse", "placeholder": "192.168.1.20"},
    {"name": "token", "label": "Zugriffstoken", "secret": True},
)

Die Werte stehen in self._settings (ein dict, das der Konfiguration gehört — nicht kopieren). secret: True: der Wert verlässt das Backend nie, gemeldet wird nur, ob er gesetzt ist.

Nie nach etwas fragen, was der Anwendung gehört: keine Felder namens api_key, client_id, client_secret, redirect_uri (ein Test hält das für alle Plugins fest). Was dem Nutzer gehört — sein Token, seine Adresse, sein Passwort — bleibt einstellbar.

Verhalten ​

  • Ein Plugin, das etwas Unerwartetes wirft, darf die Verbindung nicht mitreißen. Fehler als PluginError("verständlicher Satz") melden; die Meldung sieht der Nutzer.
  • start() / stop() nur überschreiben, wenn es eine Verbindung gibt; immer await super().start() / stop() aufrufen.
  • Nichts blockieren. Netz, Prozesse und Windows-Aufrufe über asyncio.to_thread(...) oder asyncio.create_subprocess_exec. Kein time.sleep im Ereignisweg.
  • states() und widget_data() werden bei jedem Bildschirmaufbau gerufen: nur zurückgeben, was schon da ist — keine Netzabfrage, kein Warten.
  • Windows-spezifisches (ctypes.windll, os.startfile, tasklist) hinter sys.platform.startswith("win") und mit PluginError("only implemented on Windows") abfangen, damit die Tests auch anderswo laufen. Tastenkürzel senden: from .keys import send (send("ctrl+shift+m")).
  • Gefährliches (Herunterfahren, Beenden, Löschen) bekommt eine Frist oder einen Gegenbefehl: ein Tastendruck ist kein Programm.
  • Kommentare erklären das Warum; Docstrings und Texte für den Nutzer auf Deutsch, Namen im Code englisch oder deutsch wie in der Datei daneben.

Sichtbar machen ​

  1. Klassenfelder wie oben ausfüllen. Das Symbol (icon) muss in frontend/Desktop/Views.cs (Marks.Table) stehen; ohne eines zeigt die Oberfläche ein neutrales.

  2. Eingebaut: Klasse in catalog.builtin() eintragen. Die Reihenfolge dort ist die Reihenfolge im Marktplatz innerhalb einer Kategorie.

  3. Im Marktplatz aktivieren. Erst ein aktiviertes Plugin wird gestartet, erscheint in der Leiste, und seine Tasten laufen.

  4. Eingebaut: die Seite in der Dokumentation anlegen. Das Werkzeug legt sie als Gerüst an und trägt Kopf, Einstellungen, Befehle und Widgets selbst ein:

    .venv/Scripts/python.exe tools/docs_referenz.py

    Geschrieben wird der Text drumherum von Hand — wofür das Plugin da ist, wie man es einrichtet, Beispiele, Fallstricke —, und der Satz „Diese Seite ist noch nicht geschrieben.“ geht dabei weg. Zwischen den Marken (<!-- erzeugt:befehle --> … <!-- /erzeugt:befehle -->) ändert man nichts: das überschreibt das Werkzeug beim nächsten Lauf. Als Vorbild taugt jede Seite unter docs/plugin-referenz/, zum Beispiel ntfy.

Tests ​

backend/tests/test_plugin_<name>.py, ohne Hardware und ohne Netz: Netz gegen einen lokalen Fake-Server (http.server auf 127.0.0.1, Port 0), Windows-Aufrufe per monkeypatch. Mindestens:

  • jeder Befehl und das, was bei fehlenden/falschen Parametern passiert,
  • absolute braucht value, relative braucht step (Plugin-Vertrag),
  • keine verbotenen Felder in settings_fields,
  • die Klassenfelder des Marktplatzes (description, category in base.CATEGORIES, icon).
.venv/Scripts/python.exe -m pytest backend/tests/test_plugin_<name>.py

Ändert sich später ein Befehl, ein Feld oder die description, muss tools/docs_referenz.py noch einmal laufen. Wer es vergisst, bekommt es von backend/tests/test_docs_referenz.py gesagt — der Test nennt auch, welche Seite nicht mehr stimmt. Eine neue Seite, die noch ein Gerüst ist, lässt er nicht durch.