Initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# (Optional) lokale Notizen / IDE
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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` → `<dein Gerät>` →
|
||||
- `current_title`
|
||||
- `current_artist`
|
||||
- `current_album`
|
||||
- `state_simple`
|
||||
|
||||
Kopiere die Objekt-IDs in `CFG.DP`.
|
||||
|
||||
## AWTRIX MQTT Topics (was das Script sendet)
|
||||
|
||||
- Custom App:
|
||||
- `<prefix>/custom/<APP_NAME>`
|
||||
- Beispiel: `awtrix/custom/NowPlaying`
|
||||
- Optionaler Switch:
|
||||
- `<prefix>/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`.
|
||||
@@ -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 -> <dein Gerät> -> 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);
|
||||
Reference in New Issue
Block a user