commit 3c8bd230f39361a648c106b271ed69367b42c964 Author: drg Date: Mon Aug 18 20:27:09 2025 +0200 init SpaceStatus diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd373c7 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +Dieses Projekt ist ein Scriptable-Widget für iOS, das den Status der Clubräume und des Mumble-Servers anzeigt. + +**Credits** + +Der Code wurde ursprünglich von Kai entwickelt. Vielen Dank für die Arbeit und Inspiration. diff --git a/SpaceStatus.js b/SpaceStatus.js new file mode 100644 index 0000000..0d1e9e4 --- /dev/null +++ b/SpaceStatus.js @@ -0,0 +1,273 @@ +// Variables used by Scriptable. +// These must be at the very top of the FileManager. Do not edit. +// icon-color: deep-green; icon-glyph: power-off; + +// API-URLs für Mumble- und Space-Status +const mumbleApiUrl = "https://status.chaospott.de/chaospott_mumble.json"; +const spaceApiUrl = "https://status.chaospott.de/status.json"; + +// URLs für das Logo und lokale Dateinamen für Caching +const logoUrl = "https://chaospott.de/images/logo.png"; +const logoLocalFilename = "chaospott_logo.png"; +const mumbleLocalFilename = "chaospott_mumble.json"; +const spaceLocalFilename = "chaospott_space.json"; + +// Titel und Untertitel des Widgets +const title = "Chaospott"; +const subTitle = "Essen"; + +// Variablen für die Farbzustände der Räume +var colorSpaceClosed; +var colorSpaceOpen; + +// Widget-Erstellung und Anzeige +var widget = await createWidget(); + +// Wenn das Skript nicht im Widget-Kontext läuft, präsentiere das Widget als kleines Fenster +if (!config.runsInWidget) { + await widget.presentSmall(); +} + +// Setze das Widget +Script.setWidget(widget); +Script.complete(); + +// Funktion zur Erstellung des Widgets +async function createWidget() { + // Definiere Farben für verschiedene Status + const colorOpenFresh = Color.green(); + const colorOpenStale = new Color("#00ff00", 0.3); + const colorClosedFresh = new Color("#ff0000", 1.0); + const colorClosedStale = new Color("#ff0000", 0.3); + const colorLonelyFresh = new Color("#ff8800", 1.0); + const colorLonelyStale = new Color("#ff8800", 0.3); + + // Setze Standardfarben für den Widget-Hintergrund + var colorBorderOpen = colorOpenFresh; + var colorBorderClosed = colorClosedFresh; + var colorBorderLonely = colorLonelyFresh; + var colorMumbleOpen; + var colorMumbleClosed; + var colorMumbleLonely; + + // Erstelle ein neues Widget + const widget = new ListWidget(); + + // Versuche, die JSON-Daten von den APIs abzurufen und im Cache zu speichern + try { + var [mumbleStatus, mumbleFresh] = await getJSONandCache(mumbleLocalFilename, mumbleApiUrl); + var [spaceStatus, spaceFresh] = await getJSONandCache(spaceLocalFilename, spaceApiUrl); + } catch (err) { + // Bei Fehlern zeige eine Fehlermeldung an + const errorList = new ListWidget(); + errorList.addText("Please enable internet for initial execution."); + return errorList; + } + + // Setze die Farben basierend auf den aktuellen Mumble-Daten + if (mumbleFresh) { + colorMumbleOpen = colorOpenFresh; + colorMumbleClosed = colorClosedFresh; + colorMumbleLonely = colorLonelyFresh; + } else { + colorMumbleOpen = colorOpenStale; + colorMumbleClosed = colorClosedStale; + colorMumbleLonely = colorLonelyStale; + colorBorderOpen = colorOpenStale; + colorBorderClosed = colorClosedStale; + colorBorderLonely = colorLonelyStale; + } + + // Setze die Farben basierend auf den aktuellen Space-Daten + if (spaceFresh) { + colorSpaceOpen = colorOpenFresh; + colorSpaceClosed = colorClosedFresh; + } else { + colorSpaceOpen = colorOpenStale; + colorSpaceClosed = colorClosedStale; + colorBorderOpen = colorOpenStale; + colorBorderClosed = colorClosedStale; + colorBorderLonely = colorLonelyStale; + } + + // Bestimme die Hintergrundfarbe des Widgets basierend auf dem Status des Raums und der Benutzeranzahl + if (spaceStatus.state.open) { + widget.backgroundColor = colorBorderOpen; + } else { + switch (mumbleStatus.connected_users) { + case 0: + widget.backgroundColor = colorBorderClosed; + break; + case 1: + widget.backgroundColor = colorBorderLonely; + break; + default: + widget.backgroundColor = colorBorderOpen; + } + } + + // Setze Padding für das Widget + widget.setPadding(0, 5, 0, 5); + canvasStack = widget.addStack(); + canvasStack.setPadding(8, 15, 8, 15); + canvasStack.cornerRadius = 15; // Runde die Ecken des Widgets + canvasStack.layoutVertically(); // Vertikale Anordnung der Elemente + canvasStack.backgroundColor = Color.dynamic(Color.white(), Color.black()); + + // Header-Stack für Titel und Logo + const headerStack = canvasStack.addStack(); + const titleStack = headerStack.addStack(); + + titleStack.layoutVertically(); // Vertikale Anordnung für Titel und Untertitel + const titleText = titleStack.addText(title); // Titel hinzufügen + titleText.font = Font.regularSystemFont(16); // Schriftart für Titel + + const subTitleText = titleStack.addText(subTitle); // Untertitel hinzufügen + subTitleText.font = Font.mediumSystemFont(10); // Schriftart für Untertitel + + headerStack.addSpacer(5); // Abstand hinzufügen + + // Logo hinzufügen + let logo = await getCachedImage(logoLocalFilename, logoUrl); + const logoImage = headerStack.addImage(logo); + logoImage.imageSize = new Size(30, 30); // Größe des Logos + + canvasStack.addSpacer(5); // Abstand hinzufügen + + // Mittelreihe für den Raumstatus + const middleRow = canvasStack.addStack(); + spaceStatus.sensors.door_locked.forEach((obj, i, arr) => { + spaceView(middleRow, obj.location, obj.value); // Raumansicht für jeden Sensor + if (i !== arr.length - 1) { middleRow.addSpacer(); } // Abstand zwischen den Sensoren + }); + + // Letzte Aktualisierung des Raums anzeigen + const spaceLastUpdate = new Date(spaceStatus.state.lastchange * 1000); + const spaceLastUpdateStack = canvasStack.addStack(); + let spaceLastUpdateLabel = spaceLastUpdateStack.addDate(spaceLastUpdate); + spaceLastUpdateLabel.font = Font.mediumSystemFont(6); // Schriftart für das Datum + spaceLastUpdateLabel.applyRelativeStyle(); // Relatives Datum anwenden + + // Untere Reihe für Mumble-Status + const bottomRow = canvasStack.addStack(); + bottomRow.useDefaultPadding(); // Standard-Padding verwenden + bottomRow.centerAlignContent(); // Inhalte zentrieren + const mumbleLabelStack = bottomRow.addStack(); + mumbleLabelStack.layoutVertically(); // Vertikale Anordnung für Mumble-Label + + const labelMumble = mumbleLabelStack.addText("M"); // Mumble-Label hinzufügen + labelMumble.font = Font.regularSystemFont(12); // Schriftart für Mumble-Label + + // Letzte Aktualisierung des Mumble-Servers anzeigen + let mumbleLastUpdate = new Date(mumbleStatus.last_update * 1000); + const labelMumbleUpdated = mumbleLabelStack.addDate(mumbleLastUpdate); + labelMumbleUpdated.font = Font.mediumSystemFont(6); // Schriftart für das Datum + labelMumbleUpdated.applyTimeStyle(); // Zeitstil anwenden + + bottomRow.addSpacer(3); // Abstand hinzufügen + + // Anzahl der verbundenen Benutzer anzeigen + const mumbleValueStack = bottomRow.addStack(); + const labelMumbleUser = mumbleValueStack.addText(mumbleStatus.connected_users.toString(10)); // Benutzeranzahl hinzufügen + labelMumbleUser.font = Font.boldSystemFont(23); // Schriftart für Benutzeranzahl + + // Benutzeranzahl-Farbe basierend auf dem Status festlegen + switch (mumbleStatus.connected_users) { + case 0: + labelMumbleUser.textColor = colorMumbleClosed; // Rot für geschlossen + break; + case 1: + labelMumbleUser.textColor = colorMumbleLonely; // Orange für einsam + break; + default: + labelMumbleUser.textColor = colorMumbleOpen; // Grün für offen + } + + bottomRow.addSpacer(10); // Abstand hinzufügen + + // Abstand zwischen den Elementen + canvasStack.addSpacer(4); + + // Datum und Uhrzeit hinzufügen + dateStack = canvasStack.addStack(); + dateStack.layoutHorizontally(); // Horizontale Anordnung für Datum + dateStack.bottomAlignContent(); // Inhalte am unteren Rand ausrichten + dateStack.addSpacer(41); // Abstand hinzufügen + const now = new Date(); // Aktuelle Zeit abrufen + const timeLabel = dateStack.addDate(now); // Uhrzeit hinzufügen + timeLabel.font = Font.mediumSystemFont(10); // Schriftart für die Uhrzeit + timeLabel.centerAlignText(); // Uhrzeit zentrieren + timeLabel.applyTimeStyle(); // Zeitstil anwenden + timeLabel.textColor = Color.darkGray(); // Schriftfarbe für die Uhrzeit + + return widget; // Das erstellte Widget zurückgeben +} + +// Funktion zur Anzeige des Raumstatus +function spaceView(widget, space, lockStatus) { + const viewStack = widget.addStack(); // Neuen Stack für den Raumstatus hinzufügen + viewStack.layoutVertically(); // Vertikale Anordnung + viewStack.centerAlignContent(); // Inhalte zentrieren + + // Raumname formatieren + const spaceName = space.charAt(0).toUpperCase() + space.slice(1); + const label = viewStack.addText(spaceName); // Raumname hinzufügen + label.font = Font.regularSystemFont(14); // Schriftart für Raumname + + // Schloss-Symbol hinzufügen, abhängig vom Lock-Status + const lock = SFSymbol.named("lock." + (lockStatus ? "" : "open.") + "fill"); + lock.applyFont(Font.systemFont(20)); // Schriftart für das Symbol + const lockImage = viewStack.addImage(lock.image); // Schlossbild hinzufügen + lockImage.resizable = false; // Größe des Bildes festlegen + lockImage.imageSize = new Size(25, 25); // Größe des Schlosses + + // Färbe das Schloss basierend auf dem Lock-Status + if (lockStatus) { + lockImage.tintColor = colorSpaceClosed; // Rot für geschlossen + } else { + lockImage.tintColor = colorSpaceOpen; // Grün für offen + } +} + +// Funktion zum Abrufen und Cachen von Bildern +async function getCachedImage(localFilename, url) { + let fm = FileManager.local(); // Lokalen FileManager abrufen + let dir = fm.cacheDirectory(); // Cache-Verzeichnis abrufen + let path = fm.joinPath(dir, localFilename); // Pfad zur lokalen Datei erstellen + if (fm.fileExists(path)) { + return fm.readImage(path); // Bild aus dem Cache lesen, wenn vorhanden + } else { + let r = new Request(url); // Anfrage an die URL erstellen + try { + let returnImage = await r.loadImage(); // Bild von der URL laden + fm.writeImage(path, returnImage); // Bild im Cache speichern + return returnImage; // Das geladene Bild zurückgeben + } catch (err) { + // Bei Fehlern ein Platzhalterbild zurückgeben + return SFSymbol.named("photo").image; + } + } +} + +// Funktion zum Abrufen und Cachen von JSON-Daten +async function getJSONandCache(localFilename, url) { + let fm = FileManager.local(); // Lokalen FileManager abrufen + let dir = fm.cacheDirectory(); // Cache-Verzeichnis abrufen + let path = fm.joinPath(dir, localFilename); // Pfad zur lokalen Datei erstellen + let r = new Request(url); // Anfrage an die URL erstellen + try { + var data = await r.loadJSON(); // JSON-Daten von der URL laden + fm.writeString(path, JSON.stringify(data, null, 2)); // Daten im Cache speichern + var fresh = true; // Daten sind aktuell + } catch (err) { + // Wenn der Abruf fehlschlägt, versuche, die zwischengespeicherte Version zu lesen + if (fm.fileExists(path)) { + data = JSON.parse(fm.readString(path), null); // Daten aus dem Cache lesen + fresh = false; // Daten sind nicht aktuell + } else { + throw "no data"; // Keine Daten vorhanden + } + } + return [data, fresh]; // Die Daten und die Aktualität der Daten zurück +} +