This commit is contained in:
2026-01-05 11:19:29 +01:00
7 changed files with 335 additions and 340 deletions

View File

@@ -0,0 +1,5 @@
# (Optional) lokale Notizen / IDE
.DS_Store
.idea/
.vscode/
*.log

View File

@@ -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

View File

@@ -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.

View File

@@ -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. 510)
- `LIFETIME_SEC` größer setzen (z.B. 6001200)
- 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`.

View File

@@ -1,201 +0,0 @@
# 🎵 AWTRIX NowPlaying Sonos → AWTRIX Custom App (ioBroker)
Zeigt den aktuell laufenden Song von **Sonos** als eigene **AWTRIX Custom-App** an:
`🎵 Künstler — Titel (Album)` hält sich per Keep-Alive in der Rotation, solange ein Titel läuft.
> Getestet mit ioBroker (Javascript Adapter) + MQTT Adapter und einer AWTRIX mit MQTT-Support für Custom-Apps.
---
## 🚀 Features
- Liest **Titel / Artist / Album** aus Sonos-Datenpunkten
- Baut automatisch die Anzeige: `🎵 Künstler — Titel (Album)`
- **Keep-Alive**: hält die App aktiv, solange ein Titel vorhanden ist
- Automatisches **Entfernen** der App, sobald keine Titelinfos mehr da sind
- Optionales **FORCE_SWITCH**: bei jedem Refresh auf die App umschalten
- **Debounce-Logik** (200 ms) gegen Flattern bei Trackwechseln
---
## 🧩 Voraussetzungen
- **ioBroker** mit:
- Javascript Adapter (zum Ausführen des Skripts)
- MQTT Adapter (als Client zum MQTT-Broker; verwendet `sendMessage2Client`)
- Sonos Adapter (liefert `current_title`, `current_artist`, `current_album`)
- **AWTRIX** (mit MQTT-Support für Custom-Apps)
- Ein erreichbarer **MQTT-Broker** (falls nicht in ioBroker integriert)
---
## ⚙️ Installation
1. In ioBroker den **Javascript-Adapter** öffnen → neues Script `NowPlaying Sonos → AWTRIX` anlegen
2. Den Code aus `nowplaying_sonos_awtrix.js` einfügen
3. Folgende Werte anpassen:
- `MQTT_INSTANCE`, `AWTRIX_PREFIX`
- Sonos-Datenpunkte unter `DP.title / artist / album`
4. Skript starten
→ Beim ersten laufenden Titel sollte auf der AWTRIX die App **NowPlaying** erscheinen.
---
## 🔧 Konfiguration
| Variable | Typ | Standardwert | Beschreibung |
|--------------------|---------|---------------|--------------|
| `MQTT_INSTANCE` | String | `"mqtt.0"` | ioBroker-Instanzname des MQTT-Adapters |
| `AWTRIX_PREFIX` | String | `"awtrix"` | MQTT-Prefix der AWTRIX (z. B. `awtrix/custom/NowPlaying`) |
| `APP_NAME` | String | `"NowPlaying"`| Name der Custom-App |
| `LIFETIME_SEC` | Number | `600` | Lebensdauer der App in Sekunden (wenn kein Refresh kommt) |
| `KEEPALIVE_SEC` | Number | `10` | Intervall für den Refresh in Sekunden |
| `FORCE_SWITCH` | Boolean | `false` | Bei jedem Refresh auf die App umschalten |
| `ICON_MUSIC` | Number | `29944` | AWTRIX-Icon-ID |
| `COLOR_RGB` | Array | `[255,255,255]` | RGB-Farbe für Text |
| `TEXT_CASE` | Number | `2` | Textdarstellung (0 = normal, 1 = upper, 2 = smart) |
| `DP.title` | String | Beispiel | Sonos-Datenpunkt `current_title` |
| `DP.artist` | String | Beispiel | Sonos-Datenpunkt `current_artist` |
| `DP.album` | String | Beispiel | Sonos-Datenpunkt `current_album` |
> ⚠️ **Hinweis:** Passe die Sonos-Datenpunkte an dein Gerät an IP und Struktur können variieren.
---
## 🧠 Funktionsweise
1. **Trigger:**
```js
on({ id: [DP.title, DP.artist, DP.album], change: "ne" }, scheduleUpdate);
```
Bei Änderung wird `updateAwtrix()` mit Debounce (200 ms) aufgerufen.
2. **Logik:**
- Kein Titel → App entfernen, Keep-Alive stoppen
- Neuer Track → Text bauen, sofort publishen, Keep-Alive starten
- Gleicher Track → Keep-Alive aktualisiert regelmäßig selbst
3. **Keep-Alive:**
Alle `KEEPALIVE_SEC` Sekunden wird `publishCustom()` aufgerufen.
Sobald kein Titel mehr vorhanden ist → App wird automatisch entfernt.
---
## 📡 MQTT-Themen & Payloads
### Custom-App veröffentlichen
**Topic:**
```
<AWTRIX_PREFIX>/custom/<APP_NAME>
```
**Payload:**
```json
{
"name": "NowPlaying",
"text": "🎵 Artist — Title (Album)",
"icon": 29944,
"color": [255, 255, 255],
"textCase": 2,
"lifetime": 600
}
```
---
### Auf App umschalten (optional)
**Topic:**
```
<AWTRIX_PREFIX>/switch
```
**Payload:**
```json
{ "name": "NowPlaying" }
```
---
### App entfernen
**Topic:**
```
<AWTRIX_PREFIX>/custom/<APP_NAME>
```
**Payload:**
```json
{ "name": "NowPlaying", "lifetime": 1 }
```
---
> Das Skript sendet über:
> ```js
> sendTo("mqtt.X", "sendMessage2Client", { topic, message, retain: false, qos: 0 });
> ```
---
## 🛠️ Anpassungen & Tipps
### Mehrere Sonos-Player
- Skript **duplizieren**
- `APP_NAME` (z. B. `NowPlaying_Kueche`) und `DP.*` anpassen
### Anzeige-Dauer / Refresh
- Anzeige-Frequenz → `KEEPALIVE_SEC`
- Ablaufzeit ohne Refresh → `LIFETIME_SEC`
### Direkter Wechsel auf NowPlaying
- `FORCE_SWITCH = true` setzen, um bei jedem Refresh direkt zu wechseln
---
## 🧰 Troubleshooting
| Problem | Mögliche Ursache / Lösung |
|----------|----------------------------|
| Keine Anzeige | MQTT-Broker nicht erreichbar, falsches Prefix oder kein Song aktiv |
| App verschwindet zu schnell | `LIFETIME_SEC` erhöhen |
| Zu häufige Refreshs | `KEEPALIVE_SEC` erhöhen |
| Falsches Icon oder Textformat | `ICON_MUSIC` oder `TEXT_CASE` anpassen |
| App spammt Rotation | `FORCE_SWITCH = false` lassen |
| Keine Daten von Sonos | Sonos-Adapter prüfen (korrekte Player-IDs?) |
---
## 💾 Code
Die vollständige Version liegt in:
`nowplaying_sonos_awtrix.js`
---
## 🔒 Sicherheit
- MQTT-Zugangsdaten werden im ioBroker-MQTT-Adapter verwaltet
- Kein Retain (`retain:false`), QoS = 0 → kein Risiko veralteter Nachrichten
---
## 📜 Lizenz
**MIT License** siehe `LICENSE`
---
## 👏 Credits
- **Autor:** Mike
- **AWTRIX-Projekt:** Community & Entwickler
- **ioBroker Adapter:** Sonos, MQTT & Javascript
---
## 🧾 Changelog
| Version | Datum | Änderungen |
|----------|--------|------------|
| v0.0.1 | 2025-10-27 | Erste Veröffentlichung |
---

View File

@@ -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);

View File

@@ -1,139 +0,0 @@
/******************************************************
* AWTRIX NowPlaying Sonos → AWTRIX Custom App
* Version 0.0.1
* Autor: Mike
* Zweck: Zeigt "🎵 Künstler — Titel (Album)" auf der AWTRIX
* Trigger: Änderungen an Sonos-Datenpunkten (Titel/Artist/Album)
******************************************************/
/*********** 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)
/* Anzeige */
const ICON_MUSIC = 29944;
const COLOR_RGB = [255, 255, 255];
const TEXT_CASE = 2;
/* 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"
};
/*************************************/
/*********** Helpers ***********/
function readVal(id) {
const st = getState(id);
return st ? (st.val ?? "") : "";
}
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}`;
}
function sendMQTT(topic, payloadObj) {
// ioBroker als Broker → sendMessage2Client
sendTo(MQTT_INSTANCE, "sendMessage2Client", {
topic,
message: JSON.stringify(payloadObj),
retain: false,
qos: 0
});
}
function publishCustom(text) {
const payload = {
name: APP_NAME,
text,
icon: ICON_MUSIC,
color: COLOR_RGB,
textCase: TEXT_CASE,
lifetime: LIFETIME_SEC
};
sendMQTT(`${AWTRIX_PREFIX}/custom/${APP_NAME}`, payload);
if (FORCE_SWITCH) sendMQTT(`${AWTRIX_PREFIX}/switch`, { name: APP_NAME });
}
function removeApp() {
sendMQTT(`${AWTRIX_PREFIX}/custom/${APP_NAME}`, { name: APP_NAME, lifetime: 1 });
log("🛑 NowPlaying entfernt (keine Titelinfos)");
}
/*********** Kernlogik ***********/
let currentSig = ""; // artist|title|album
let keepAliveTmr = null; // setInterval-Handle
let debounceTmr = null;
function stopKeepAlive() {
if (keepAliveTmr) {
clearInterval(keepAliveTmr);
keepAliveTmr = null;
}
}
function startKeepAlive(text) {
stopKeepAlive();
// Erster Push sofort (sichtbar machen) …
publishCustom(text);
// … und dann regelmäßig solange Titel vorhanden ist
keepAliveTmr = setInterval(() => {
// Wenn Titel leer geworden ist, sofort stoppen und App entfernen
const t = String(readVal(DP.title)).trim();
if (!t) {
stopKeepAlive();
currentSig = "";
removeApp();
return;
}
publishCustom(text);
}, KEEPALIVE_SEC * 1000);
}
function updateAwtrix() {
const title = String(readVal(DP.title)).trim();
const artist = String(readVal(DP.artist)).trim();
const album = String(readVal(DP.album)).trim();
// Kein Lied → alles aus
if (!title && !artist && !album) {
stopKeepAlive();
if (currentSig) removeApp();
currentSig = "";
return;
}
const text = buildText(artist, title, album);
const sig = `${artist}|${title}|${album}`;
// Neuer/anderer Track → Keep-Alive neu starten
if (sig !== currentSig) {
currentSig = sig;
log(`🎧 NowPlaying → ${text}`);
startKeepAlive(text);
} else {
// Gleicher Track: nichts weiter Keep-Alive tickt von selbst
}
}
/*********** Trigger & Initiallauf ***********/
function scheduleUpdate() {
if (debounceTmr) clearTimeout(debounceTmr);
debounceTmr = setTimeout(updateAwtrix, 200);
}
on({ id: [DP.title, DP.artist, DP.album], change: "ne" }, scheduleUpdate);
// Beim Start einmal versuchen
setTimeout(updateAwtrix, 1500);