From df81c528c83b901803117d5949c8cd58e4cdeb90 Mon Sep 17 00:00:00 2001 From: mike Date: Sat, 3 Jan 2026 13:18:17 +0000 Subject: [PATCH] ioBroker-Skripte/AWTRIX Now Playing Sonos/ioBroker_awtrix_sonos_NowPlaying.js aktualisiert --- .../ioBroker_awtrix_sonos_NowPlaying.js | 193 ++++++++++++------ 1 file changed, 130 insertions(+), 63 deletions(-) diff --git a/ioBroker-Skripte/AWTRIX Now Playing Sonos/ioBroker_awtrix_sonos_NowPlaying.js b/ioBroker-Skripte/AWTRIX Now Playing Sonos/ioBroker_awtrix_sonos_NowPlaying.js index a61ee8b..669c1e7 100644 --- a/ioBroker-Skripte/AWTRIX Now Playing Sonos/ioBroker_awtrix_sonos_NowPlaying.js +++ b/ioBroker-Skripte/AWTRIX Now Playing Sonos/ioBroker_awtrix_sonos_NowPlaying.js @@ -1,34 +1,73 @@ /****************************************************** - * AWTRIX NowPlaying – Sonos → AWTRIX Custom App - * Version 0.0.2 - * Autor: Mike - * Zweck: Zeigt "🎵 Künstler — Titel (Album)" auf der AWTRIX - * Trigger: Änderungen an Sonos-Datenpunkten (Titel/Artist/Album/State) + * 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 ******************************************************/ -/*********** Einstellungen ***********/ -const MQTT_INSTANCE = "mqtt.0"; -const AWTRIX_PREFIX = "awtrix"; // Dein Prefix -const APP_NAME = "NowPlaying"; -const LIFETIME_SEC = 600; // Eintrag läuft ab, wenn kein Refresh kommt -const KEEPALIVE_SEC = 10; // alle x Sekunden Refresh solange Titel vorhanden -const FORCE_SWITCH = false; // bei jedem Refresh auf App schalten (falls viele Apps) +/******************** USER CONFIG (ANPASSEN) ********************/ +const CFG = { + /* ioBroker MQTT Adapter-Instanz */ + MQTT_INSTANCE: "mqtt.0", -/* Anzeige */ -const ICON_MUSIC = 29944; -const COLOR_RGB = [255, 255, 255]; -const TEXT_CASE = 2; + /* AWTRIX MQTT Prefix (Standard: "awtrix") */ + AWTRIX_PREFIX: "awtrix", -/* Sonos-Datenpunkte (ggf. anpassen) */ -const 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 + /* 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}`); +} -/*********** Helpers ***********/ function readVal(id) { const st = getState(id); return st ? (st.val ?? "") : ""; @@ -40,22 +79,29 @@ function readBool(id) { } function isPlaying() { - // state_simple ist bereits Boolean: true = Wiedergabe - return readBool(DP.stateSimple); + // state_simple ist im Sonos-Adapter i.d.R. Boolean: true = Wiedergabe + return readBool(CFG.DP.stateSimple); } function buildText(artist, title, album) { let base = ""; - if (artist && title) base = `${artist} — ${title}`; - else if (title) base = title; - else if (artist) base = artist; - if (base && album) base += ` (${album})`; - if (!base) base = "—"; - return `🎵 ${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(MQTT_INSTANCE, "sendMessage2Client", { + sendTo(CFG.MQTT_INSTANCE, "sendMessage2Client", { topic, message: JSON.stringify(payloadObj), retain: false, @@ -65,63 +111,81 @@ function sendMQTT(topic, payloadObj) { function publishCustom(text) { const payload = { - name: APP_NAME, + name: CFG.APP_NAME, text, - icon: ICON_MUSIC, - color: COLOR_RGB, - textCase: TEXT_CASE, - lifetime: LIFETIME_SEC + icon: CFG.ICON_MUSIC, + color: CFG.COLOR_RGB, + textCase: CFG.TEXT_CASE, + lifetime: CFG.LIFETIME_SEC }; - sendMQTT(`${AWTRIX_PREFIX}/custom/${APP_NAME}`, payload); - if (FORCE_SWITCH) sendMQTT(`${AWTRIX_PREFIX}/switch`, { name: APP_NAME }); + + 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() { - sendMQTT(`${AWTRIX_PREFIX}/custom/${APP_NAME}`, { name: APP_NAME, lifetime: 1 }); + // "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; +/******************** 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(); - // Erster Push sofort (sichtbar machen) + + // 1) Sofort pushen (sichtbar machen) publishCustom(text); - // … und dann regelmäßig solange Titel vorhanden UND Playback aktiv + // 2) Dann regelmäßig refreshen solange Titel vorhanden & playing keepAliveTmr = setInterval(() => { - const t = String(readVal(DP.title)).trim(); - const playing = isPlaying(); + const title = String(readVal(CFG.DP.title)).trim(); + const playing = isPlaying(); - // Wenn kein Titel oder nicht mehr playing → sofort stoppen und App entfernen - if (!t || !playing) { + 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); - }, KEEPALIVE_SEC * 1000); + }, CFG.KEEPALIVE_SEC * 1000); + + dbg(`startKeepAlive every ${CFG.KEEPALIVE_SEC}s`); } function updateAwtrix() { - const title = String(readVal(DP.title)).trim(); - const artist = String(readVal(DP.artist)).trim(); - const album = String(readVal(DP.album)).trim(); + 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(); - // Kein Lied ODER Player nicht im Play-Status → alles aus + 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(); @@ -130,9 +194,9 @@ function updateAwtrix() { } const text = buildText(artist, title, album); - const sig = `${artist}|${title}|${album}`; + const sig = `${artist}|${title}|${album}`; - // Neuer/anderer Track → Keep-Alive neu starten + // Neuer/anderer Track -> KeepAlive neu starten if (sig !== currentSig) { currentSig = sig; log(`🎧 NowPlaying → ${text}`); @@ -140,14 +204,17 @@ function updateAwtrix() { } } -/*********** Trigger & Initiallauf ***********/ +/******************** Trigger & Initiallauf ******************************/ function scheduleUpdate() { if (debounceTmr) clearTimeout(debounceTmr); - debounceTmr = setTimeout(updateAwtrix, 200); + debounceTmr = setTimeout(updateAwtrix, CFG.DEBOUNCE_MS); } -// Triggert jetzt auch auf state_simple -on({ id: [DP.title, DP.artist, DP.album, DP.stateSimple], change: "ne" }, scheduleUpdate); +// 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 +// Beim Start einmal versuchen (Adapter brauchen manchmal kurz) setTimeout(updateAwtrix, 1500);