/****************************************************** * 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);