From 875640d8f3b2939707b6ade7c9c4bd1a31b05382 Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 15 May 2026 12:54:22 +0200 Subject: [PATCH] Initial commit --- .gitignore | 5 + CHANGELOG.md | 7 ++ LICENSE | 21 ++++ README.md | 82 ++++++++++++++ awtrix-sonos-nowplaying.js | 220 +++++++++++++++++++++++++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 awtrix-sonos-nowplaying.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cf29ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# (Optional) lokale Notizen / IDE +.DS_Store +.idea/ +.vscode/ +*.log diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fe3955c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 0.2.0 +- Erste GitHub/Gitea-ready Version (Single-File ioBroker Script) +- Keep-Alive Refresh + Auto-Remove bei Stop/Pause +- Debounce für Sonos Events +- README + MIT License diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3cef8a0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d77e768 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# AWTRIX 3 – Sonos NowPlaying (ioBroker Script) + +Zeigt den aktuell abgespielten Sonos-Titel als **AWTRIX 3 Custom App** via **MQTT** an: + +> 🎵 Künstler — Titel (Album) + +Das Script nutzt einen **Keep-Alive Refresh**, damit die App in der Rotation bleibt, solange Sonos spielt. Sobald Playback stoppt/pausiert oder kein Titel mehr vorhanden ist, wird die App automatisch entfernt. + +## Features + +- ✅ Anzeige: `🎵 Künstler — Titel (Album)` +- ✅ Keep-Alive Refresh (damit die App nicht „rausfällt“) +- ✅ Entfernt die App automatisch bei Stop/Pause/kein Titel +- ✅ Debounce gegen Event-Spam vom Sonos-Adapter +- ✅ Alles in **einer Datei** (ioBroker-friendly) + +## Voraussetzungen + +- ioBroker **JavaScript-Adapter** +- ioBroker **Sonos-Adapter** +- ioBroker **MQTT-Adapter** (als Client/Publisher nutzbar über `sendMessage2Client`) +- AWTRIX 3 mit aktivierter MQTT-Anbindung (Prefix bekannt, i.d.R. `awtrix`) + +## Installation (Quick Start) + +1. Datei `nowplaying.js` öffnen und den Block **USER CONFIG** anpassen: + - `AWTRIX_PREFIX` (meist `awtrix`) + - `DP.*` (deine Sonos-Datenpunkte) +2. In ioBroker → **JavaScript** → neues Script anlegen → Inhalt von `nowplaying.js` einfügen +3. Script starten +4. Sonos abspielen → nach spätestens wenigen Sekunden sollte es auf der AWTRIX erscheinen + +## Sonos-Datenpunkte finden + +In ioBroker unter **Objekte**: + +`sonos.0` → `root` → `` → +- `current_title` +- `current_artist` +- `current_album` +- `state_simple` + +Kopiere die Objekt-IDs in `CFG.DP`. + +## AWTRIX MQTT Topics (was das Script sendet) + +- Custom App: + - `/custom/` + - Beispiel: `awtrix/custom/NowPlaying` +- Optionaler Switch: + - `/switch` + - Wird nur genutzt, wenn `FORCE_SWITCH=true` + +## Konfiguration (wichtigste Optionen) + +Im `CFG` Block: + +- `LIFETIME_SEC`: Wie lange ein Eintrag ohne Refresh überlebt +- `KEEPALIVE_SEC`: Alle wieviel Sekunden refreshed wird +- `FORCE_SWITCH`: Wenn `true`, schaltet AWTRIX bei jedem Refresh aktiv auf die App (meist **false** lassen) +- `ICON_MUSIC`, `COLOR_RGB`, `TEXT_CASE`: Darstellung +- `DEBUG`: Zusätzliche Logs + +## Troubleshooting + +### Es wird nichts angezeigt +- Stimmt `AWTRIX_PREFIX`? +- Ist MQTT auf der AWTRIX aktiv? +- Funktioniert dein MQTT Adapter (und kann publishen)? +- Stimmen die Sonos-Datenpunkte? + +### App verschwindet nach kurzer Zeit +- `KEEPALIVE_SEC` ggf. kleiner setzen (z.B. 5–10) +- `LIFETIME_SEC` größer setzen (z.B. 600–1200) +- Prüfen ob `state_simple` wirklich `true` während Playback ist + +### Die Uhr springt ständig auf die App +- `FORCE_SWITCH` auf `false` setzen + +## Lizenz + +MIT – siehe `LICENSE`. diff --git a/awtrix-sonos-nowplaying.js b/awtrix-sonos-nowplaying.js new file mode 100644 index 0000000..669c1e7 --- /dev/null +++ b/awtrix-sonos-nowplaying.js @@ -0,0 +1,220 @@ +/****************************************************** + * AWTRIX NowPlaying – Sonos → AWTRIX Custom App (ioBroker) + * Version: 0.2.0 + * Autor: Mike (Repo-ready Version) + * + * Zweck: + * - Zeigt "🎵 Künstler — Titel (Album)" auf der AWTRIX 3 (via MQTT Custom App) + * - Keep-Alive Refresh hält die App in der Rotation, solange Sonos spielt & Titel vorhanden ist + * - Entfernt die App sofort, wenn Playback stoppt/pausiert oder kein Titel vorhanden ist + * + * Voraussetzungen: + * - ioBroker JavaScript-Adapter + * - ioBroker MQTT-Adapter (als Client/Publisher) -> sendTo(..., "sendMessage2Client", ...) + * - ioBroker Sonos-Adapter + * - AWTRIX 3 mit MQTT aktiviert + ******************************************************/ + +/******************** USER CONFIG (ANPASSEN) ********************/ +const CFG = { + /* ioBroker MQTT Adapter-Instanz */ + MQTT_INSTANCE: "mqtt.0", + + /* AWTRIX MQTT Prefix (Standard: "awtrix") */ + AWTRIX_PREFIX: "awtrix", + + /* Name der Custom App auf der AWTRIX */ + APP_NAME: "NowPlaying", + + /* Wie lange darf der AWTRIX-Eintrag ohne Refresh leben (Sekunden) */ + LIFETIME_SEC: 600, + + /* Alle x Sekunden ein Refresh, solange Titel vorhanden UND Sonos spielt */ + KEEPALIVE_SEC: 10, + + /* Wenn true: bei jedem Refresh wird aktiv auf die App gewechselt (kann nerven) */ + FORCE_SWITCH: false, + + /* Anzeige */ + ICON_MUSIC: 29944, + COLOR_RGB: [255, 255, 255], + TEXT_CASE: 2, // 0=none, 1=upper, 2=lower (AWTRIX Setting, je nach Firmware) + + /* Sonos-Datenpunkte (ANPASSEN!) + * Tipp: In ioBroker Objekte -> sonos.0 -> root -> -> current_title / current_artist / current_album / state_simple + */ + DP: { + title: "sonos.0.root.192_168_178_75.current_title", + artist: "sonos.0.root.192_168_178_75.current_artist", + album: "sonos.0.root.192_168_178_75.current_album", + stateSimple: "sonos.0.root.192_168_178_75.state_simple" // true=spielt, false=kein Playback + }, + + /* Textformat */ + PREFIX_EMOJI: "🎵", + SEP_ARTIST_TITLE: " — ", + SHOW_ALBUM_IN_PARENS: true, + + /* Debounce für schnelle Sonos-Updates (ms) */ + DEBOUNCE_MS: 200, + + /* Optional: Debug-Logs */ + DEBUG: false +}; +/******************** /USER CONFIG ******************************/ + +/******************** Helpers ******************************/ +function dbg(msg) { + if (CFG.DEBUG) log(`🐞 ${msg}`); +} + +function readVal(id) { + const st = getState(id); + return st ? (st.val ?? "") : ""; +} + +function readBool(id) { + const st = getState(id); + return st ? !!st.val : false; +} + +function isPlaying() { + // state_simple ist im Sonos-Adapter i.d.R. Boolean: true = Wiedergabe + return readBool(CFG.DP.stateSimple); +} + +function buildText(artist, title, album) { + let base = ""; + + const a = (artist || "").trim(); + const t = (title || "").trim(); + const al = (album || "").trim(); + + if (a && t) base = `${a}${CFG.SEP_ARTIST_TITLE}${t}`; + else if (t) base = t; + else if (a) base = a; + + if (base && CFG.SHOW_ALBUM_IN_PARENS && al) base += ` (${al})`; + if (!base) base = "—"; + + return `${CFG.PREFIX_EMOJI} ${base}`; +} + +function sendMQTT(topic, payloadObj) { + sendTo(CFG.MQTT_INSTANCE, "sendMessage2Client", { + topic, + message: JSON.stringify(payloadObj), + retain: false, + qos: 0 + }); +} + +function publishCustom(text) { + const payload = { + name: CFG.APP_NAME, + text, + icon: CFG.ICON_MUSIC, + color: CFG.COLOR_RGB, + textCase: CFG.TEXT_CASE, + lifetime: CFG.LIFETIME_SEC + }; + + sendMQTT(`${CFG.AWTRIX_PREFIX}/custom/${CFG.APP_NAME}`, payload); + dbg(`publishCustom -> ${text}`); + + if (CFG.FORCE_SWITCH) { + sendMQTT(`${CFG.AWTRIX_PREFIX}/switch`, { name: CFG.APP_NAME }); + dbg("FORCE_SWITCH -> switch"); + } +} + +function removeApp() { + // "lifetime: 1" sorgt dafür, dass die App praktisch sofort aus der Rotation fällt + sendMQTT(`${CFG.AWTRIX_PREFIX}/custom/${CFG.APP_NAME}`, { + name: CFG.APP_NAME, + lifetime: 1 + }); + dbg("removeApp"); + log("🛑 NowPlaying entfernt (kein Playback)"); +} + +/******************** Kernlogik ******************************/ +let currentSig = ""; // artist|title|album +let keepAliveTmr = null; // setInterval-Handle +let debounceTmr = null; // setTimeout-Handle + +function stopKeepAlive() { + if (keepAliveTmr) { + clearInterval(keepAliveTmr); + keepAliveTmr = null; + dbg("stopKeepAlive"); + } +} + +function startKeepAlive(text) { + stopKeepAlive(); + + // 1) Sofort pushen (sichtbar machen) + publishCustom(text); + + // 2) Dann regelmäßig refreshen solange Titel vorhanden & playing + keepAliveTmr = setInterval(() => { + const title = String(readVal(CFG.DP.title)).trim(); + const playing = isPlaying(); + + if (!title || !playing) { + dbg(`keepAlive stop: title="${title}" playing=${playing}`); + stopKeepAlive(); + currentSig = ""; + removeApp(); + return; + } + + // Text bleibt bewusst gleich, solange der Track gleich ist. + publishCustom(text); + }, CFG.KEEPALIVE_SEC * 1000); + + dbg(`startKeepAlive every ${CFG.KEEPALIVE_SEC}s`); +} + +function updateAwtrix() { + const title = String(readVal(CFG.DP.title)).trim(); + const artist = String(readVal(CFG.DP.artist)).trim(); + const album = String(readVal(CFG.DP.album)).trim(); + const playing = isPlaying(); + + dbg(`updateAwtrix: playing=${playing} title="${title}" artist="${artist}" album="${album}"`); + + // Kein Lied ODER Player nicht playing -> alles aus + if ((!title && !artist && !album) || !playing) { + stopKeepAlive(); + if (currentSig) removeApp(); + currentSig = ""; + return; + } + + const text = buildText(artist, title, album); + const sig = `${artist}|${title}|${album}`; + + // Neuer/anderer Track -> KeepAlive neu starten + if (sig !== currentSig) { + currentSig = sig; + log(`🎧 NowPlaying → ${text}`); + startKeepAlive(text); + } +} + +/******************** Trigger & Initiallauf ******************************/ +function scheduleUpdate() { + if (debounceTmr) clearTimeout(debounceTmr); + debounceTmr = setTimeout(updateAwtrix, CFG.DEBOUNCE_MS); +} + +// Triggert auf Titel/Artist/Album/State +on( + { id: [CFG.DP.title, CFG.DP.artist, CFG.DP.album, CFG.DP.stateSimple], change: "ne" }, + scheduleUpdate +); + +// Beim Start einmal versuchen (Adapter brauchen manchmal kurz) +setTimeout(updateAwtrix, 1500);