Design
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
| Ort | Wofür |
|---|---|
backend/plugins/<name>.py + eine Zeile in backend/plugins/catalog.py (builtin()) | Eingebaut, wird mit der Software ausgeliefert |
%APPDATA%/MultiverseStreamDeck/plugins/<name>.py | Eigenes, 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, PluginErrorDie 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)
| Feld | Bedeutung |
|---|---|
name, label | Schlüssel und Anzeigetext |
params | Formularfelder: {"name", "label", "required"?, "multiline"?} |
absolute: True | Passt auf einen Schieberegler; braucht ein Parameter value (0–100). Der Wert kommt vom Regler |
relative: True | Passt 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; immerawait super().start()/stop()aufrufen.- Nichts blockieren. Netz, Prozesse und Windows-Aufrufe über
asyncio.to_thread(...)oderasyncio.create_subprocess_exec. Keintime.sleepim Ereignisweg. states()undwidget_data()werden bei jedem Bildschirmaufbau gerufen: nur zurückgeben, was schon da ist — keine Netzabfrage, kein Warten.- Windows-spezifisches (
ctypes.windll,os.startfile,tasklist) hintersys.platform.startswith("win")und mitPluginError("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
Klassenfelder wie oben ausfüllen. Das Symbol (
icon) muss infrontend/Desktop/Views.cs(Marks.Table) stehen; ohne eines zeigt die Oberfläche ein neutrales.Eingebaut: Klasse in
catalog.builtin()eintragen. Die Reihenfolge dort ist die Reihenfolge im Marktplatz innerhalb einer Kategorie.Im Marktplatz aktivieren. Erst ein aktiviertes Plugin wird gestartet, erscheint in der Leiste, und seine Tasten laufen.
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.pyGeschrieben 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 unterdocs/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,
absolutebrauchtvalue,relativebrauchtstep(Plugin-Vertrag),- keine verbotenen Felder in
settings_fields, - die Klassenfelder des Marktplatzes (
description,categoryinbase.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.