ioBroker-Skripte/AWTRIX Now Playing Sonos/ioBroker_awtrix_sonos_NowPlaying.js aktualisiert

This commit is contained in:
2026-01-03 13:18:17 +00:00
parent 1b5d0aa3f3
commit df81c528c8

View File

@@ -1,34 +1,73 @@
/****************************************************** /******************************************************
* AWTRIX NowPlaying Sonos → AWTRIX Custom App * AWTRIX NowPlaying Sonos → AWTRIX Custom App (ioBroker)
* Version 0.0.2 * Version: 0.2.0
* Autor: Mike * Autor: Mike (Repo-ready Version)
* Zweck: Zeigt "🎵 Künstler — Titel (Album)" auf der AWTRIX *
* Trigger: Änderungen an Sonos-Datenpunkten (Titel/Artist/Album/State) * 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 ***********/ /******************** USER CONFIG (ANPASSEN) ********************/
const MQTT_INSTANCE = "mqtt.0"; const CFG = {
const AWTRIX_PREFIX = "awtrix"; // Dein Prefix /* ioBroker MQTT Adapter-Instanz */
const APP_NAME = "NowPlaying"; MQTT_INSTANCE: "mqtt.0",
const LIFETIME_SEC = 600; // Eintrag läuft ab, wenn kein Refresh kommt
const KEEPALIVE_SEC = 10; // alle x Sekunden Refresh solange Titel vorhanden /* AWTRIX MQTT Prefix (Standard: "awtrix") */
const FORCE_SWITCH = false; // bei jedem Refresh auf App schalten (falls viele Apps) 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 */ /* Anzeige */
const ICON_MUSIC = 29944; ICON_MUSIC: 29944,
const COLOR_RGB = [255, 255, 255]; COLOR_RGB: [255, 255, 255],
const TEXT_CASE = 2; TEXT_CASE: 2, // 0=none, 1=upper, 2=lower (AWTRIX Setting, je nach Firmware)
/* Sonos-Datenpunkte (ggf. anpassen) */ /* Sonos-Datenpunkte (ANPASSEN!)
const DP = { * Tipp: In ioBroker Objekte -> sonos.0 -> root -> <dein Gerät> -> current_title / current_artist / current_album / state_simple
*/
DP: {
title: "sonos.0.root.192_168_178_75.current_title", title: "sonos.0.root.192_168_178_75.current_title",
artist: "sonos.0.root.192_168_178_75.current_artist", artist: "sonos.0.root.192_168_178_75.current_artist",
album: "sonos.0.root.192_168_178_75.current_album", 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 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) { function readVal(id) {
const st = getState(id); const st = getState(id);
return st ? (st.val ?? "") : ""; return st ? (st.val ?? "") : "";
@@ -40,22 +79,29 @@ function readBool(id) {
} }
function isPlaying() { function isPlaying() {
// state_simple ist bereits Boolean: true = Wiedergabe // state_simple ist im Sonos-Adapter i.d.R. Boolean: true = Wiedergabe
return readBool(DP.stateSimple); return readBool(CFG.DP.stateSimple);
} }
function buildText(artist, title, album) { function buildText(artist, title, album) {
let base = ""; let base = "";
if (artist && title) base = `${artist}${title}`;
else if (title) base = title; const a = (artist || "").trim();
else if (artist) base = artist; const t = (title || "").trim();
if (base && album) base += ` (${album})`; 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 = "—"; if (!base) base = "—";
return `🎵 ${base}`;
return `${CFG.PREFIX_EMOJI} ${base}`;
} }
function sendMQTT(topic, payloadObj) { function sendMQTT(topic, payloadObj) {
sendTo(MQTT_INSTANCE, "sendMessage2Client", { sendTo(CFG.MQTT_INSTANCE, "sendMessage2Client", {
topic, topic,
message: JSON.stringify(payloadObj), message: JSON.stringify(payloadObj),
retain: false, retain: false,
@@ -65,63 +111,81 @@ function sendMQTT(topic, payloadObj) {
function publishCustom(text) { function publishCustom(text) {
const payload = { const payload = {
name: APP_NAME, name: CFG.APP_NAME,
text, text,
icon: ICON_MUSIC, icon: CFG.ICON_MUSIC,
color: COLOR_RGB, color: CFG.COLOR_RGB,
textCase: TEXT_CASE, textCase: CFG.TEXT_CASE,
lifetime: LIFETIME_SEC 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() { 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)"); log("🛑 NowPlaying entfernt (kein Playback)");
} }
/*********** Kernlogik ***********/ /******************** Kernlogik ******************************/
let currentSig = ""; // artist|title|album let currentSig = ""; // artist|title|album
let keepAliveTmr = null; // setInterval-Handle let keepAliveTmr = null; // setInterval-Handle
let debounceTmr = null; let debounceTmr = null; // setTimeout-Handle
function stopKeepAlive() { function stopKeepAlive() {
if (keepAliveTmr) { if (keepAliveTmr) {
clearInterval(keepAliveTmr); clearInterval(keepAliveTmr);
keepAliveTmr = null; keepAliveTmr = null;
dbg("stopKeepAlive");
} }
} }
function startKeepAlive(text) { function startKeepAlive(text) {
stopKeepAlive(); stopKeepAlive();
// Erster Push sofort (sichtbar machen)
// 1) Sofort pushen (sichtbar machen)
publishCustom(text); publishCustom(text);
// … und dann regelmäßig solange Titel vorhanden UND Playback aktiv // 2) Dann regelmäßig refreshen solange Titel vorhanden & playing
keepAliveTmr = setInterval(() => { keepAliveTmr = setInterval(() => {
const t = String(readVal(DP.title)).trim(); const title = String(readVal(CFG.DP.title)).trim();
const playing = isPlaying(); const playing = isPlaying();
// Wenn kein Titel oder nicht mehr playing → sofort stoppen und App entfernen if (!title || !playing) {
if (!t || !playing) { dbg(`keepAlive stop: title="${title}" playing=${playing}`);
stopKeepAlive(); stopKeepAlive();
currentSig = ""; currentSig = "";
removeApp(); removeApp();
return; return;
} }
// Text bleibt bewusst gleich, solange der Track gleich ist.
publishCustom(text); publishCustom(text);
}, KEEPALIVE_SEC * 1000); }, CFG.KEEPALIVE_SEC * 1000);
dbg(`startKeepAlive every ${CFG.KEEPALIVE_SEC}s`);
} }
function updateAwtrix() { function updateAwtrix() {
const title = String(readVal(DP.title)).trim(); const title = String(readVal(CFG.DP.title)).trim();
const artist = String(readVal(DP.artist)).trim(); const artist = String(readVal(CFG.DP.artist)).trim();
const album = String(readVal(DP.album)).trim(); const album = String(readVal(CFG.DP.album)).trim();
const playing = isPlaying(); 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) { if ((!title && !artist && !album) || !playing) {
stopKeepAlive(); stopKeepAlive();
if (currentSig) removeApp(); if (currentSig) removeApp();
@@ -132,7 +196,7 @@ function updateAwtrix() {
const text = buildText(artist, title, album); 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) { if (sig !== currentSig) {
currentSig = sig; currentSig = sig;
log(`🎧 NowPlaying → ${text}`); log(`🎧 NowPlaying → ${text}`);
@@ -140,14 +204,17 @@ function updateAwtrix() {
} }
} }
/*********** Trigger & Initiallauf ***********/ /******************** Trigger & Initiallauf ******************************/
function scheduleUpdate() { function scheduleUpdate() {
if (debounceTmr) clearTimeout(debounceTmr); if (debounceTmr) clearTimeout(debounceTmr);
debounceTmr = setTimeout(updateAwtrix, 200); debounceTmr = setTimeout(updateAwtrix, CFG.DEBOUNCE_MS);
} }
// Triggert jetzt auch auf state_simple // Triggert auf Titel/Artist/Album/State
on({ id: [DP.title, DP.artist, DP.album, DP.stateSimple], change: "ne" }, scheduleUpdate); 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); setTimeout(updateAwtrix, 1500);