Als technikaffiner Elternteil steht man häufig vor einer besonderen Herausforderung: Wie kann man moderne Technik kindgerecht und sinnvoll in den Alltag integrieren? Wir haben auch die kommerzielle Lösung Toniebox(*), welche sehr beliebt ist aber teuer und in ihren Funktionen begrenzt. Vor allem gibt es manchmal auch nicht den Tonie des gewünschten Hörspiels (bspw. “Die Schule der magischen Tiere”).
In diesem Beitrag stelle ich eine selbstgebaute Alternative vor, die einem Kind ermöglicht, über das einfache Auflegen von NFC-Tags die eigenen Spotify-Playlists abzuspielen – und das vollständig unabhängig von Smartphones oder komplexen Bedienelementen.
Diese DIY-Lösung verbindet einen ESP8266-Mikrocontroller(*) mit einem RFID-RC522-Lesemodul(*) und nutzt ioBroker als Smart-Home-Zentrale. Darüber wird ein Google Home Mini als Ausgabegerät angesteuert. Im Gegensatz zu kommerziellen Lösungen bietet dieses Projekt zahlreiche Vorteile: Es ist kostengünstiger, flexibel erweiterbar und nutzt vorhandene Geräte wie den Google Home Mini neu. Zudem lassen sich beliebige Spotify-Playlists hinterlegen – sei es für Hörbücher, Musik oder Einschlaflieder.
Vorteile dieser DIY-Lösung
- Kosteneffizient: Die Hardwarekosten liegen bei etwa 15-20€ (plus Google Home Mini oder alternativ Amazon Echo, falls nicht vorhanden)
- Nutzung vorhandener Infrastruktur: Integriert sich in bestehende Smart-Home-Umgebungen
- Flexibilität: Jederzeit erweiterbar und anpassbar, z.B. um neue Playlists hinzuzufügen
- Pädagogisch wertvoll: Kinder lernen den Umgang mit Technik auf spielerische Weise
- Personalisierbar: Nutzt individuelle Spotify-Konten und persönliche Playlists
- Keine Abhängigkeit von externen Diensten: Volle Kontrolle über alle Komponenten
Materialien und Voraussetzungen
Hardware
- ESP8266 NodeMCU(*) (mit USB-C-Schnittstelle)
- RFID-RC522-Modul(*) (13,56 MHz, SPI-Schnittstelle)
- Jumper-Kabel(*) für die Verbindungen
- NFC-Tags(*)
- Google Home Mini oder Amazon Echo
- USB C Kabel mit Netzteil(*)zur Stromversorgung des ESP8266
Software und Dienste
- Arduino IDE zur Programmierung des ESP8266
- ioBroker als Smart-Home-Zentrale, vorzugsweise auf einer Synology NAS oder einem Raspberry Pi
- MQTT-Broker (kann über den ioBroker MQTT-Adapter bereitgestellt werden)
- Spotify Premium-Konto
- Spotify Developer-Account (kostenlos)
Voraussetzungen
- Anleitung lesen können 🙂
- Ein funktionierendes ioBroker-System
- Basiskenntnisse im Umgang mit JavaScript
- WLAN-Netzwerk, in dem sich alle Geräte befinden
Schritt 1: Hardware-Aufbau
Der Hardware-Aufbau ist unkompliziert und erfordert nur Lötarbeiten, wenn man keine Jumperkabel verwendet. Der ESP8266 NodeMCU wird über Jumper-Kabel mit dem RFID-RC522-Lesemodul verbunden. Hier die Verkabelung im Detail:
RFID-RC522 Pin(*) | ESP8266 NodeMCU Pin(*) |
SDA (SS) | D8 (GPIO15) |
SCK | D5 (GPIO14) |
MOSI | D7 (GPIO13) |
MISO | D6 (GPIO12) |
IRQ | Nicht verbunden |
GND | GND |
RST | D4 (GPIO2) |
3.3V | 3.3V |
Diese Verbindungen stellen die SPI-Kommunikation zwischen dem ESP8266 und dem RFID-Lesemodul sicher. Achte auf die korrekte Verbindung, da falsch verbundene Pins zu Fehlfunktionen führen können.

Schritt 2: ESP8266-Programmierung
Der ESP8266 hat zwei Hauptaufgaben: Er liest die RFID-Tags und sendet die erkannten IDs per MQTT an ioBroker. Der folgende Arduino-Code übernimmt diese Funktionen:
#include <SPI.h>
#include <MFRC522.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
// WLAN-Konfiguration
const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";
// MQTT-Broker-Konfiguration
const char* mqtt_server = "192.168.x.x"; // IP-Adresse deines ioBroker-Servers
const int mqtt_port = 1883;
const char* mqtt_topic = "home/spotify/nfccontrol";
// RFID-Konfiguration
#define RST_PIN D4
#define SS_PIN D8
// Instanzen für RFID-Reader und MQTT-Client
MFRC522 mfrc522(SS_PIN, RST_PIN);
WiFiClient espClient;
PubSubClient client(espClient);
// Zeitvariablen für Verbindungswiederherstellung
unsigned long lastReconnectAttempt = 0;
const long reconnectInterval = 5000;
void setup() {
// Initialisiere serielle Kommunikation für Debugging
Serial.begin(115200);
Serial.println("\n\nRFID zu Spotify-System startet...");
// Initialisiere SPI-Kommunikation für den RFID-Reader
SPI.begin();
mfrc522.PCD_Init();
Serial.println("RFID-Reader initialisiert");
// Zeige Details zum RFID-Reader an
mfrc522.PCD_DumpVersionToSerial();
// Verbindung zum WLAN herstellen
setupWiFi();
// MQTT-Client konfigurieren
client.setServer(mqtt_server, mqtt_port);
// Ersten MQTT-Verbindungsversuch starten
connectToMQTT();
Serial.println("Setup abgeschlossen - bereit für RFID-Tags");
}
void loop() {
// MQTT-Verbindung überprüfen und bei Bedarf wiederherstellen
if (!client.connected()) {
unsigned long now = millis();
if (now - lastReconnectAttempt > reconnectInterval) {
lastReconnectAttempt = now;
if (connectToMQTT()) {
lastReconnectAttempt = 0;
}
}
} else {
// Client-Loop ausführen, wenn verbunden
client.loop();
}
// Prüfe, ob ein neues RFID-Tag vorhanden ist
if (!mfrc522.PICC_IsNewCardPresent()) {
delay(50); // Kurze Pause zur CPU-Entlastung
return;
}
// Versuche, die Daten des Tags zu lesen
if (!mfrc522.PICC_ReadCardSerial()) {
delay(50);
return;
}
// Lese die UID (eindeutige ID) des RFID-Tags
String cardUID = getCardUID();
Serial.println("RFID-Tag erkannt mit UID: " + cardUID);
// Sende die Daten per MQTT
sendCardDataMQTT(cardUID);
// Warte, bis das Tag entfernt wurde
Serial.println("Warte auf Entfernung des Tags...");
delay(1000);
// Stoppe die Kommunikation mit dem aktuellen Tag
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
}
// WLAN-Verbindung herstellen
void setupWiFi() {
delay(10);
Serial.println();
Serial.print("Verbinde mit WLAN: ");
Serial.println(ssid);
// Setze WiFi-Modus und starte Verbindung
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// Warte auf Verbindung mit Status-Ausgabe
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// Verbindung hergestellt - zeige Netzwerkdetails an
Serial.println("");
Serial.println("WiFi verbunden");
Serial.print("IP-Adresse: ");
Serial.println(WiFi.localIP());
}
// Verbindung zum MQTT-Broker herstellen
boolean connectToMQTT() {
Serial.print("Verbinde mit MQTT-Broker (");
Serial.print(mqtt_server);
Serial.print(":");
Serial.print(mqtt_port);
Serial.print(")... ");
// Erstelle eine eindeutige Client-ID basierend auf der Chip-ID
String clientId = "ESP8266_RFID_";
clientId += String(ESP.getChipId(), HEX);
// Versuche die Verbindung herzustellen
if (client.connect(clientId.c_str())) {
Serial.println("verbunden");
// Sende eine Verbindungsbestätigung
client.publish(mqtt_topic, "{\"status\":\"connected\",\"device\":\"ESP8266_RFID_Reader\"}");
return true;
} else {
Serial.print("fehlgeschlagen, rc=");
Serial.println(client.state());
return false;
}
}
// RFID-Tag-UID als Hex-String lesen
String getCardUID() {
String cardUID = "";
for (byte i = 0; i < mfrc522.uid.size; i++) {
// Füge führende Nullen für Werte unter 16 (0x10) hinzu
cardUID += (mfrc522.uid.uidByte[i] < 0x10 ? "0" : "");
// Konvertiere Byte zu Hex und füge zum String hinzu
cardUID += String(mfrc522.uid.uidByte[i], HEX);
}
// Konvertiere zu Großbuchstaben für konsistente Formatierung
cardUID.toUpperCase();
return cardUID;
}
// Sende die Daten des RFID-Tags per MQTT
void sendCardDataMQTT(String cardUID) {
if (!client.connected()) {
Serial.println("MQTT nicht verbunden - überspringe Senden");
return;
}
// Erstelle ein JSON-Dokument mit den Tag-Daten
DynamicJsonDocument doc(256);
doc["uid"] = cardUID;
doc["device"] = "ESP8266_RFID_Reader";
doc["rssi"] = WiFi.RSSI();
doc["timestamp"] = millis();
// Konvertiere das JSON-Dokument in einen String
char jsonBuffer[256];
serializeJson(doc, jsonBuffer);
// Veröffentliche die Daten per MQTT
Serial.print("Sende MQTT-Nachricht: ");
Serial.println(jsonBuffer);
if (client.publish(mqtt_topic, jsonBuffer)) {
Serial.println("MQTT-Nachricht erfolgreich gesendet");
} else {
Serial.println("MQTT-Nachricht konnte nicht gesendet werden");
}
}
Dieser Code ist robust gestaltet und beinhaltet Fehlerbehandlung, automatische Wiederverbindung bei WLAN- oder MQTT-Ausfällen sowie eine klare Strukturierung der Funktionen. Passe vor dem Hochladen die WLAN- und MQTT-Parameter an deine Umgebung an.
Schritt 3: ioBroker-Konfiguration
MQTT-Adapter einrichten
Zuerst installieren und konfigurieren wir den MQTT-Adapter in ioBroker:
- Gehe in ioBroker zum Reiter “Adapter”
- Suche nach “MQTT” und installiere den “MQTT Client/Broker”-Adapter
- Konfiguriere den Adapter als Server/Broker mit folgenden Einstellungen:
- Verbindungstyp: Server/Broker
- Port: 1883 (Standard)
- Authentifizierung: Nach Bedarf aktivieren
- Speichere die Einstellungen und starte den Adapter
JavaScript-Adapter für Spotify-Integration
Der JavaScript-Adapter bildet das Herzstück des Systems. Er empfängt die MQTT-Nachrichten vom ESP8266, verarbeitet die RFID-IDs und steuert Spotify an. Hier ist der vollständige Code:
// Spotify Direct API Controller
// Dieses Skript verbindet RFID-Tags direkt mit Spotify über die Spotify API
// Optimiert für die Nutzung mit einem festen Google Home-Gerät
// Konfiguration
const config = {
clientId: 'DEINE_CLIENT_ID', // Von der Spotify Developer-App
clientSecret: 'DEIN_CLIENT_SECRET', // Von der Spotify Developer-App
redirectUri: 'http://[deine-iobroker-ip]:8082/userfiles/spotify-auth.html',
deviceName: 'DEIN_GOOGLE_HOME', // Name des Google Home in Spotify
scope: 'user-read-playback-state user-modify-playback-state user-read-currently-playing',
wakeUpPlaylistUri: 'spotify:playlist:37i9dQZF1DXcBWIGoYBM5M' // Eine Standard-Playlist zum "Aufwecken" des Google Home
};
// Speicherung der Tokens (im State, damit sie Neustarts überleben)
const tokenStatePath = 'javascript.0.SpotifyDirectAPI.tokens';
// Status-Flag um wiederholte Wake-Up-Versuche zu verhindern
let wakeUpAttempted = false;
// Erstelle States für Token-Speicherung
createState(tokenStatePath + '.accessToken', '', {
name: 'Spotify Access Token',
type: 'string',
role: 'text'
});
createState(tokenStatePath + '.refreshToken', '', {
name: 'Spotify Refresh Token',
type: 'string',
role: 'text'
});
createState(tokenStatePath + '.expiresAt', 0, {
name: 'Spotify Token Expiry',
type: 'number',
role: 'value'
});
// RFID zu Playlist-Zuordnung
const nfcToPlaylist = {
"048D4B91C22A81": "spotify:playlist:0FExWK304ULhIT9UrgcBSV",
// Weitere Zuordnungen hier hinzufügen
};
// Verbesserte Hilfsfunktion für HTTP-Requests mit direktem HTTP-Client
function httpRequest(options) {
return new Promise((resolve, reject) => {
// URL ohne die URL-Klasse parsen
let urlString = options.url;
let hostname, path;
// Entferne das Protokoll (http:// oder https://)
urlString = urlString.replace(/^https?:\/\//, '');
// Finde den ersten Schrägstrich, um Hostname und Pfad zu trennen
const slashIndex = urlString.indexOf('/');
if (slashIndex !== -1) {
hostname = urlString.substring(0, slashIndex);
path = urlString.substring(slashIndex);
} else {
hostname = urlString;
path = '/';
}
const requestOptions = {
hostname: hostname,
path: path,
method: options.method || 'GET',
headers: options.headers || {}
};
log('HTTP-Anfrage: ' + options.method + ' ' + hostname + path);
const req = require('https').request(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
if (data && data.length > 0) {
try {
const parsedData = JSON.parse(data);
log('HTTP-Antwort: Status ' + res.statusCode);
// Wenn HTTP-Status 401, wirf einen speziellen Fehler
if (res.statusCode === 401) {
reject(new Error('401 Unauthorized: Token ungültig oder abgelaufen'));
} else {
resolve(parsedData);
}
} catch (e) {
// Verbesserte Fehlerbehandlung für ungültige JSON-Antworten
log('Konnte Antwort nicht als JSON parsen', 'debug');
// Bei Status 204 (No Content) oder wenn die Antwort leer ist, gib leeres Objekt zurück
if (res.statusCode === 204 || data.trim() === '') {
log('HTTP-Antwort: No-Content oder leere Antwort mit Status ' + res.statusCode);
resolve({});
} else {
// Bei anderen ungültigen JSON-Antworten, resolven und eine Warnung loggen
log('Ungültige JSON-Antwort, aber Anfrage war erfolgreich (Status: ' + res.statusCode + ')', 'warn');
resolve({ statusCode: res.statusCode });
}
}
} else {
log('HTTP-Antwort: Leere Antwort mit Status ' + res.statusCode);
resolve({});
}
} catch (e) {
log('Fehler beim Verarbeiten der Antwort: ' + e.message, 'error');
reject(new Error('Fehler beim Verarbeiten der Antwort: ' + e.message));
}
});
});
req.on('error', (error) => {
log('HTTP-Fehler: ' + error.message, 'error');
reject(error);
});
// Timeout nach 30 Sekunden
req.setTimeout(30000, () => {
log('HTTP-Timeout nach 30 Sekunden', 'warn');
req.abort();
reject(new Error('Timeout nach 30 Sekunden'));
});
// Sende Daten falls vorhanden
if (options.data) {
req.write(typeof options.data === 'string' ? options.data : JSON.stringify(options.data));
}
req.end();
});
}
// Verbesserte Token-Abruffunktion mit Refresh-Versuch
async function getValidToken() {
const now = Date.now();
const expiresAt = getState(tokenStatePath + '.expiresAt').val;
const accessToken = getState(tokenStatePath + '.accessToken').val;
const refreshToken = getState(tokenStatePath + '.refreshToken').val;
// Wenn das Token noch gültig ist, verwende es
if (expiresAt > now) {
return accessToken;
}
// Token ist abgelaufen - versuche es zu aktualisieren wenn möglich
if (refreshToken) {
try {
log('Token abgelaufen, versuche Refresh...');
const response = await refreshAccessToken(refreshToken);
log('Token erfolgreich aktualisiert, gültig bis: ' + new Date(getState(tokenStatePath + '.expiresAt').val).toLocaleString());
return response.access_token;
} catch (e) {
log('Fehler beim Token-Refresh: ' + e.message, 'warn');
// Verwende trotzdem das alte Token als Fallback
log('Verwende abgelaufenes Token als Fallback');
return accessToken;
}
} else {
log('Warnung: Spotify-Token ist abgelaufen und kein Refresh-Token vorhanden', 'warn');
return accessToken;
}
}
// Access-Token über Refresh-Token aktualisieren
async function refreshAccessToken(refreshToken) {
const data = [
'grant_type=refresh_token',
'refresh_token=' + encodeURIComponent(refreshToken),
'client_id=' + encodeURIComponent(config.clientId),
'client_secret=' + encodeURIComponent(config.clientSecret)
].join('&');
const options = {
url: 'https://accounts.spotify.com/api/token',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(data)
},
data: data
};
const response = await httpRequest(options);
if (response.access_token) {
// Speichere das neue Token
setState(tokenStatePath + '.accessToken', response.access_token);
// Speichere die Ablaufzeit (aktuell + Ablaufzeit in Sekunden - 60s Puffer)
const expiresAt = Date.now() + (response.expires_in - 60) * 1000;
setState(tokenStatePath + '.expiresAt', expiresAt);
// Speichere das neue Refresh-Token, falls vorhanden
if (response.refresh_token) {
setState(tokenStatePath + '.refreshToken', response.refresh_token);
}
return response;
} else {
throw new Error('Keine Token in der Antwort');
}
}
// Manuelle Einstellung von Tokens (für den Fall, dass das Refresh nicht funktioniert)
function setManualTokens(accessToken, refreshToken, expiresInSeconds) {
setState(tokenStatePath + '.accessToken', accessToken);
setState(tokenStatePath + '.refreshToken', refreshToken);
// Berechne Ablaufzeit
const expiresAt = Date.now() + (expiresInSeconds * 1000);
setState(tokenStatePath + '.expiresAt', expiresAt);
log('Tokens manuell aktualisiert. Gültig bis: ' + new Date(expiresAt).toLocaleString());
}
// Authentifizierungs-URL generieren
function getAuthUrl() {
const params = [
'client_id=' + encodeURIComponent(config.clientId),
'response_type=code',
'redirect_uri=' + encodeURIComponent(config.redirectUri),
'scope=' + encodeURIComponent(config.scope)
].join('&');
const authUrl = 'https://accounts.spotify.com/authorize?' + params;
log('Öffne diese URL in deinem Browser, um mit der Spotify-Authentifizierung zu beginnen: ' + authUrl);
return authUrl;
}
// Initialen Access-Token abrufen
async function getInitialToken(code) {
const data = [
'grant_type=authorization_code',
'code=' + encodeURIComponent(code),
'redirect_uri=' + encodeURIComponent(config.redirectUri),
'client_id=' + encodeURIComponent(config.clientId),
'client_secret=' + encodeURIComponent(config.clientSecret)
].join('&');
const options = {
url: 'https://accounts.spotify.com/api/token',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(data)
},
data: data
};
const response = await httpRequest(options);
if (response.access_token) {
setState(tokenStatePath + '.accessToken', response.access_token);
setState(tokenStatePath + '.refreshToken', response.refresh_token);
const expiresAt = Date.now() + (response.expires_in - 60) * 1000;
setState(tokenStatePath + '.expiresAt', expiresAt);
return response;
} else {
throw new Error('Keine Token in der Antwort');
}
}
// Spotify API-Anfrage mit verbesserter Fehlerbehandlung für 401-Fehler
async function spotifyApiRequest(endpoint, method = 'GET', data = null, retryOnUnauthorized = true) {
const token = await getValidToken();
if (!token) {
throw new Error('Kein gültiges Token verfügbar');
}
const options = {
url: 'https://api.spotify.com/v1/' + endpoint,
method: method,
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
};
if (data) {
options.data = JSON.stringify(data);
options.headers['Content-Length'] = Buffer.byteLength(options.data);
}
try {
return await httpRequest(options);
} catch (e) {
// Wenn der Fehler ein 401 Unauthorized ist und wir Retries erlauben
if (e.message.includes('401') && retryOnUnauthorized) {
log('401 Unauthorized - Versuche Token-Refresh', 'warn');
const refreshToken = getState(tokenStatePath + '.refreshToken').val;
if (refreshToken) {
try {
// Versuche, das Token zu aktualisieren
const tokenResponse = await refreshAccessToken(refreshToken);
log('Token erfolgreich aktualisiert, versuche Anfrage erneut');
// Wiederhole die Anfrage mit dem neuen Token, aber ohne weiteren Retry
return await spotifyApiRequest(endpoint, method, data, false);
} catch (refreshError) {
log('Token-Refresh fehlgeschlagen: ' + refreshError.message, 'error');
throw new Error('Spotify API-Fehler nach fehlgeschlagenem Token-Refresh: ' + e.message);
}
} else {
throw new Error('401 Unauthorized und kein Refresh-Token verfügbar');
}
} else {
throw e;
}
}
}
// Verfügbare Geräte abrufen
async function getDevices() {
return await spotifyApiRequest('me/player/devices');
}
// Playlist auf bestimmtem Gerät abspielen
async function playPlaylist(playlistUri, deviceId) {
return await spotifyApiRequest('me/player/play?device_id=' + deviceId, 'PUT', {
context_uri: playlistUri
});
}
// "Aufwecken" des Google Home durch kurzes Abspielen und Pausieren
async function wakeUpGoogleHome() {
log('Versuche, Google Home zu aktivieren...');
wakeUpAttempted = true; // Verhindere wiederholte Wake-Up-Versuche in derselben Sitzung
try {
// Versuche, alle verfügbaren Geräte zu finden
const devices = await getDevices();
if (!devices || !devices.devices || devices.devices.length === 0) {
log('Keine Geräte für Wake-Up gefunden', 'warn');
return false;
}
// Suche nach jedem Gerät, das den konfigurierten Namen enthält
const targetDevice = devices.devices.find(d => d.name === config.deviceName);
if (!targetDevice) {
log('Konfiguriertes Google Home-Gerät für Wake-Up nicht gefunden', 'warn');
return false;
}
log('Versuche Wake-Up auf Gerät: ' + targetDevice.name);
try {
// Spiele kurz etwas und pausiere dann wieder
await spotifyApiRequest('me/player/play?device_id=' + targetDevice.id, 'PUT', {
context_uri: config.wakeUpPlaylistUri
});
// Kurz warten und dann pausieren
await new Promise(resolve => setTimeout(resolve, 3000));
try {
await spotifyApiRequest('me/player/pause?device_id=' + targetDevice.id, 'PUT');
} catch (pauseError) {
// Ignoriere Fehler beim Pausieren, das ist nicht kritisch
log('Hinweis: Konnte Wiedergabe nach Wake-Up nicht pausieren: ' + pauseError.message, 'debug');
}
log('Wake-Up erfolgreich auf ' + targetDevice.name);
return true;
} catch (playError) {
log('Fehler beim Abspielen während Wake-Up: ' + playError.message, 'error');
return false;
}
} catch (e) {
log('Fehler beim Wake-Up: ' + e.message, 'error');
return false;
}
}
// Gerät anhand des Namens finden
async function findDeviceByName(name) {
const devices = await getDevices();
if (!devices || !devices.devices || !devices.devices.length) {
throw new Error('Keine Geräte gefunden');
}
log('Verfügbare Geräte: ' + JSON.stringify(devices.devices.map(d => d.name)));
// Exaktes Match
const device = devices.devices.find(d => d.name === name);
if (!device) {
throw new Error('Konfiguriertes Google Home-Gerät nicht gefunden');
}
log('Google Home gefunden: ' + device.name + ' (ID: ' + device.id + ')');
return device;
}
// Playlist auf Google Home abspielen mit verbesserter Fehlerbehandlung
async function playPlaylistOnGoogleHome(playlistUri) {
try {
log('Versuche Playlist ' + playlistUri + ' auf Google Home "' + config.deviceName + '" abzuspielen');
// Prüfe Token-Status
const token = await getValidToken();
if (!token) {
throw new Error('Kein Access-Token verfügbar');
}
log('Rufe verfügbare Geräte ab...');
try {
let devices = await getDevices();
if (!devices || !devices.devices) {
log('Keine Geräteinformationen von Spotify erhalten. Versuche Wake-Up...', 'warn');
// Versuche, Google Home zu wecken, falls noch nicht versucht
if (!wakeUpAttempted) {
const wakeUpSuccess = await wakeUpGoogleHome();
if (wakeUpSuccess) {
// Versuche erneut, Geräte abzurufen
devices = await getDevices();
}
}
if (!devices || !devices.devices) {
throw new Error('Keine Geräteinformationen von Spotify erhalten');
}
}
log('Gefundene Geräte: ' + devices.devices.map(d => d.name).join(', '));
// Versuche, das Gerät zu finden
const device = await findDeviceByName(config.deviceName);
log('Google Home gefunden: ' + device.name + ' (ID: ' + device.id + ')');
log('Starte Playlist auf Gerät...');
await playPlaylist(playlistUri, device.id);
log('Playlist ' + playlistUri + ' wird jetzt auf ' + device.name + ' abgespielt');
return true;
} catch (deviceError) {
// Wenn das Gerät nicht gefunden wurde und wir noch keinen Wake-Up versucht haben
if (deviceError.message.includes('nicht gefunden') && !wakeUpAttempted) {
log('Gerät nicht gefunden. Versuche Wake-Up...', 'warn');
const wakeUpSuccess = await wakeUpGoogleHome();
if (wakeUpSuccess) {
// Versuche erneut, die Playlist abzuspielen
return await playPlaylistOnGoogleHome(playlistUri);
} else {
throw new Error('Google Home nicht gefunden und Wake-Up fehlgeschlagen');
}
} else {
throw deviceError;
}
}
} catch (e) {
log('Fehler beim Abspielen: ' + e.message, 'error');
return false;
} finally {
// Setze das Wake-Up-Flag zurück (für den nächsten Versuch)
setTimeout(() => {
wakeUpAttempted = false;
}, 10000); // 10 Sekunden Wartezeit
}
}
// MQTT-Nachricht verarbeiten
function processMqttMessage(message) {
try {
// JSON-Nachricht parsen
const data = JSON.parse(message);
// Status-Nachricht ignorieren
if (data.status === "connected") {
log("ESP8266 RFID-Reader verbunden");
return;
}
// UID aus der Nachricht extrahieren
const cardUID = data.uid;
if (!cardUID) {
log("Keine UID in der MQTT-Nachricht gefunden", 'warn');
return;
}
log('RFID-Tag erkannt mit UID: ' + cardUID);
// Prüfen, ob eine Zuordnung für diese UID existiert
if (nfcToPlaylist[cardUID]) {
const playlistUri = nfcToPlaylist[cardUID];
log('Playlist für UID ' + cardUID + ' gefunden: ' + playlistUri);
// Playlist auf Google Home abspielen
playPlaylistOnGoogleHome(playlistUri).then(success => {
if (success) {
log('Wiedergabe erfolgreich gestartet');
} else {
log('Wiedergabe konnte nicht gestartet werden', 'warn');
}
});
} else {
log('Keine Playlist für UID ' + cardUID + ' konfiguriert', 'warn');
}
} catch (e) {
log('Fehler bei der Verarbeitung der MQTT-Nachricht: ' + e.message, 'error');
}
}
// Test der Spotify-Verbindung
function testSpotifyConnection() {
log('Teste Spotify-Verbindung...');
getValidToken().then(token => {
if (!token) {
log('Kein gültiges Token vorhanden', 'error');
return;
}
log('Token vorhanden: ' + token.substring(0, 10) + '...');
getDevices().then(devices => {
if (!devices || !devices.devices) {
log('Keine Geräteliste erhalten: ' + JSON.stringify(devices), 'error');
// Versuche Wake-Up, wenn keine Geräte gefunden wurden
wakeUpGoogleHome().then(success => {
if (success) {
log('Wake-Up erfolgreich, versuche erneut, Geräte abzurufen');
setTimeout(testSpotifyConnection, 5000); // Verzögerter Neuversuch
}
});
return;
}
log('Verfügbare Geräte: ' + devices.devices.map(d => d.name + ' (' + d.type + ')').join(', '));
// Versuche, das konfigurierte Gerät zu finden
const device = devices.devices.find(d =>
d.name === config.deviceName || d.name.includes(config.deviceName)
);
if (device) {
log('Konfiguriertes Gerät gefunden: ' + device.name + ' (ID: ' + device.id + ')');
} else {
log('Konfiguriertes Gerät "' + config.deviceName + '" nicht gefunden!', 'warn');
}
}).catch(e => {
log('Fehler beim Abrufen der Geräte: ' + e.message, 'error');
});
}).catch(e => {
log('Fehler beim Prüfen des Tokens: ' + e.message, 'error');
});
}
// Zeitplan für regelmäßige Geräteabfragen einrichten
schedule("0 * * * *", function() { // Jede Stunde zur vollen Stunde
log('Führe geplante Spotify-Geräteabfrage durch...');
getValidToken().then(token => {
if (!token) {
log('Kein gültiges Token für geplante Abfrage', 'warn');
return;
}
getDevices().then(devices => {
if (devices && devices.devices) {
log('Geplante Abfrage: Gefundene Geräte: ' + devices.devices.map(d => d.name).join(', '));
// Prüfe, ob das konfigurierte Google Home in der Liste ist
const googleHome = devices.devices.find(d =>
d.name === config.deviceName || d.name.includes(config.deviceName)
);
if (!googleHome) {
log('Geplante Abfrage: Google Home nicht gefunden. Versuche, es zu aktivieren...', 'warn');
// Versuche, das Google Home zu aktivieren
wakeUpGoogleHome().then(success => {
if (success) {
log('Geplante Abfrage: Wake-Up erfolgreich');
} else {
log('Geplante Abfrage: Wake-Up fehlgeschlagen', 'warn');
}
});
} else {
log('Geplante Abfrage: Google Home gefunden (' + googleHome.name + ')');
}
} else {
log('Geplante Abfrage: Keine Geräte gefunden', 'warn');
// Versuche Wake-Up, wenn keine Geräte gefunden wurden
wakeUpGoogleHome();
}
}).catch(e => {
log('Fehler bei geplanter Geräteabfrage: ' + e.message, 'error');
});
});
});
// Zeitplan für regelmäßige Token-Aktualisierung (alle 3 Stunden)
schedule("10 */3 * * *", function() {
log('Führe geplante Token-Aktualisierung durch...');
const refreshToken = getState(tokenStatePath + '.refreshToken').val;
if (refreshToken) {
refreshAccessToken(refreshToken).then(result => {
log('Token erfolgreich aktualisiert. Gültig bis: ' + new Date(getState(tokenStatePath + '.expiresAt').val).toLocaleString());
}).catch(e => {
log('Fehler bei geplanter Token-Aktualisierung: ' + e.message, 'error');
});
} else {
log('Kein Refresh-Token für geplante Aktualisierung vorhanden', 'warn');
}
});
// MQTT-Topic abonnieren
on({id: 'mqtt.0.home.spotify.nfccontrol', change: 'any'}, function (obj) {
if (!obj.state || !obj.state.val) return;
log('MQTT-Nachricht erhalten: ' + obj.state.val);
processMqttMessage(obj.state.val);
});
// Authentifikationsprozess starten
function startAuth() {
const authUrl = getAuthUrl();
log('Authentifizierungs-URL: ' + authUrl);
// Speichere die URL auch im State, damit sie leichter zugänglich ist
setState('javascript.0.SpotifyDirectAPI.authUrl', authUrl, true);
}
// Authentifikationscode verarbeiten
function processAuthCode(code) {
log('Verarbeite Auth-Code: ' + code);
getInitialToken(code).then(result => {
log('Authentifizierung erfolgreich! Token gültig bis: ' + new Date(getState(tokenStatePath + '.expiresAt').val).toLocaleString());
// Teste die Verbindung
getDevices().then(devices => {
log('Verfügbare Geräte: ' + JSON.stringify(devices.devices.map(d => d.name)));
}).catch(e => {
log('Fehler beim Abrufen der Geräte: ' + e.message, 'error');
});
}).catch(e => {
log('Fehler bei der Authentifizierung: ' + e.message, 'error');
});
}
// Erstelle States für die Steuerung
createState('javascript.0.SpotifyDirectAPI.startAuth', false, {
name: 'Starte Spotify-Authentifizierung',
type: 'boolean',
role: 'button'
});
createState('javascript.0.SpotifyDirectAPI.authCode', '', {
name: 'Spotify Auth Code eingeben',
type: 'string',
role: 'text'
});
createState('javascript.0.SpotifyDirectAPI.authUrl', '', {
name: 'Spotify Auth URL',
type: 'string',
role: 'text',
read: true,
write: false
});
// Erstelle einen Test-Button
createState('javascript.0.SpotifyDirectAPI.testConnection', false, {
name: 'Spotify-Verbindung testen',
type: 'boolean',
role: 'button'
});
// Erstelle einen Wake-Up-Button
createState('javascript.0.SpotifyDirectAPI.wakeUpGoogleHome', false, {
name: 'Google Home aufwecken',
type: 'boolean',
role: 'button'
});
// Erstelle State für manuelle Token-Aktualisierung
createState('javascript.0.SpotifyDirectAPI.manualTokenUpdate', JSON.stringify({
accessToken: '',
refreshToken: '',
expiresIn: 3600
}), {
name: 'Manuelle Token-Aktualisierung',
type: 'string',
role: 'json'
});
// Überwache Aktionen
on({id: 'javascript.0.SpotifyDirectAPI.startAuth', change: 'ne'}, function (obj) {
if (obj.state.val) {
log('Starte Authentifizierungsprozess...');
startAuth();
// Button zurücksetzen
setTimeout(function() {
setState('javascript.0.SpotifyDirectAPI.startAuth', false);
}, 1000);
}
});
on({id: 'javascript.0.SpotifyDirectAPI.authCode', change: 'ne'}, function (obj) {
if (obj.state.val) {
processAuthCode(obj.state.val);
// Code zurücksetzen nach Verarbeitung
setTimeout(function() {
setState('javascript.0.SpotifyDirectAPI.authCode', '');
}, 5000);
}
});
on({id: 'javascript.0.SpotifyDirectAPI.testConnection', change: 'ne'}, function(obj) {
if (obj.state.val) {
testSpotifyConnection();
// Button zurücksetzen
setTimeout(function() {
setState('javascript.0.SpotifyDirectAPI.testConnection', false);
}, 1000);
}
});
on({id: 'javascript.0.SpotifyDirectAPI.wakeUpGoogleHome', change: 'ne'}, function(obj) {
if (obj.state.val) {
wakeUpGoogleHome().then(success => {
if (success) {
log('Wake-Up über Button erfolgreich');
} else {
log('Wake-Up über Button fehlgeschlagen', 'warn');
}
});
// Button zurücksetzen
setTimeout(function() {
setState('javascript.0.SpotifyDirectAPI.wakeUpGoogleHome', false);
}, 1000);
}
});
on({id: 'javascript.0.SpotifyDirectAPI.manualTokenUpdate', change: 'ne'}, function(obj) {
if (obj.state && obj.state.val) {
try {
const data = JSON.parse(obj.state.val);
if (data.accessToken && data.refreshToken && data.expiresIn) {
setManualTokens(data.accessToken, data.refreshToken, data.expiresIn);
}
} catch (e) {
log('Fehler beim Parsen der manuellen Token-Daten: ' + e.message, 'error');
}
}
});
// Startup
log('Spotify Direct API Controller gestartet');
log('Überprüfe Token-Status...');
// Prüfe beim Start, ob wir ein gültiges Token haben
getValidToken().then(token => {
if (token) {
log('Token gefunden. Teste Verbindung...');
testSpotifyConnection();
} else {
log('Kein gültiges Token gefunden. Bitte starte die Authentifizierung durch Setzen von javascript.0.SpotifyDirectAPI.startAuth auf true.');
}
}).catch(e => {
log('Fehler beim Überprüfen des Tokens: ' + e.message, 'error');
});
// Nach dem Start die Geräte abfragen, um Google Home zu aktivieren
setTimeout(function() {
log('Führe initiale Geräteabfrage zur Aktivierung des Google Home durch...');
getDevices().then(devices => {
if (devices && devices.devices) {
log('Initiale Abfrage: Gefundene Geräte: ' + devices.devices.map(d => d.name).join(', '));
// Prüfe, ob das Google Home gefunden wurde
const googleHome = devices.devices.find(d =>
d.name === config.deviceName || d.name.includes(config.deviceName)
);
if (!googleHome) {
log('Initiale Abfrage: Google Home nicht gefunden. Versuche, es zu aktivieren...', 'warn');
wakeUpGoogleHome();
} else {
log('Initiale Abfrage: Google Home gefunden (' + googleHome.name + ')');
}
}
}).catch(e => {
log('Fehler bei initialer Geräteabfrage: ' + e.message, 'error');
});
}, 10000); // 10 Sekunden nach dem Start
Dieser JavaScript-Code enthält eine robuste Fehlerbehandlung, automatische Token-Aktualisierung und eine Wake-Up-Funktion, um das Google Home bei Bedarf zu aktivieren. Die Regelmäßigkeit der Geräteabfragen und Token-Aktualisierungen sorgt für einen stabilen Betrieb.
Alternativ kannst du statt dem Java Script Code auch NodeRed nutzen und den Workflow dort abbilden.
Schritt 4: Spotify-Authentifizierung einrichten
Für die Authentifizierung mit der Spotify API benötigst du eine App im Spotify Developer Dashboard:
- Besuche developer.spotify.com und melde dich mit dem Spotify-Konto deines Kindes an
- Klicke auf “Create an App” und gib einen Namen und eine Beschreibung ein
- Füge als Redirect URI die in deinem JavaScript-Code angegebene URL hinzu
http://[deine-iobroker-ip]:8082/userfiles/spotify-auth.html - Notiere dir die Client-ID und das Client-Secret
- Trage diese in den JavaScript-Code ein:
clientId: ‘DEINE_CLIENT_ID’,
clientSecret: ‘DEIN_CLIENT_SECRET’,
Anschließend musst du noch eine kleine HTML-Datei für die Authentifizierung erstellen:
- Gehe in ioBroker zu “Dateien” > “userfiles”
- Erstelle eine neue Datei “spotify-auth.html” mit folgendem Inhalt:
<!DOCTYPE html>
<html>
<head>
<title>Spotify Authentifizierung</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #f5f5f5;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #1DB954; /* Spotify-Grün */
}
#codeContainer {
margin-top: 20px;
}
#code {
background: #eee;
padding: 15px;
border-radius: 4px;
border: 1px solid #ddd;
font-family: monospace;
word-break: break-all;
}
.success {
background-color: #dff0d8;
color: #3c763d;
padding: 15px;
border-radius: 4px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Spotify Authentifizierung</h1>
<div id="initialMessage">
<p>Diese Seite hilft dir, deinen ioBroker mit Spotify zu verbinden.</p>
<p>Wenn Spotify dich weitergeleitet hat, siehst du unten den Authentifizierungscode.</p>
</div>
<div id="codeContainer" style="display:none">
<p>Die Authentifizierung war erfolgreich! Bitte kopiere diesen Code und füge ihn in ioBroker im State <strong>javascript.0.SpotifyDirectAPI.authCode</strong> ein:</p>
<pre id="code"></pre>
<div class="success">
<p>Nach dem Einfügen des Codes in ioBroker kannst du diese Seite schließen.</p>
</div>
</div>
<div id="errorContainer" style="display:none">
<p>Bei der Authentifizierung ist ein Fehler aufgetreten:</p>
<pre id="error" style="background:#f8d7da; color:#721c24; padding:15px; border-radius:4px;"></pre>
</div>
</div>
<script>
window.onload = function() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (code) {
document.getElementById('code').innerText = code;
document.getElementById('codeContainer').style.display = 'block';
document.getElementById('initialMessage').style.display = 'none';
} else if (error) {
document.getElementById('error').innerText = error;
document.getElementById('errorContainer').style.display = 'block';
document.getElementById('initialMessage').style.display = 'none';
}
};
</script>
</body>
</html>
Authentifizierung bei Spotify abschließen
Nachdem du den Code erfolgreich implementiert hast, musst du noch die Authentifizierung mit Spotify durchführen:
- Öffne deine ioBroker-Oberfläche und setze das Objekt
javascript.0.SpotifyDirectAPI.startAuth
auftrue
(einfach anklicken). - Das System generiert eine Authentifizierungs-URL, die du im Objekt
javascript.0.SpotifyDirectAPI.authUrl
findest. Kopiere diese URL und öffne sie in deinem Browser. - In deinem Browser wirst du aufgefordert, dich bei Spotify anzumelden und der Anwendung die Berechtigungen zu erteilen. Nach erfolgreicher Anmeldung leitet Spotify dich zur vorher konfigurierten Redirect-URI weiter.
- Auf dieser Seite wird ein Authentifizierungscode angezeigt. Kopiere diesen Code.
- Kehre zu ioBroker zurück und füge den kopierten Code in das Objekt
javascript.0.SpotifyDirectAPI.authCode
ein. - Das System verarbeitet den Code automatisch und erhält die nötigen Access- und Refresh-Tokens von Spotify. Diese werden in den entsprechenden Objekten gespeichert und für zukünftige API-Anfragen verwendet.
- Du kannst jetzt die Verbindung testen, indem du das Objekt
javascript.0.SpotifyDirectAPI.testConnection
auftrue
setzt.
Nach erfolgreicher Authentifizierung ist dein System bereit, RFID-Tags zu erkennen und die entsprechenden Spotify-Playlists auf deinem Google Home abzuspielen.
Schritt 5: NFC-Tags programmieren
Die NFC-Tags müssen nicht wirklich “programmiert” werden – wir nutzen einfach die eindeutigen UIDs der Tags, die der ESP8266 ausliest. Um die UIDs zu erfassen und Playlists zuzuordnen:
- Starte das ESP8266-System und den ioBroker-JavaScript-Code
- Halte ein NFC-Tag an den RFID-Leser
- Im ioBroker-Log wird die UID des Tags angezeigt, z.B. “RFID-Tag erkannt mit UID: 048D4B91C22A81”
- Notiere diese UID für das spätere Zuordnen zu einer Playlist
Die NFC-Tags können später mit kindgerechten Bildern oder Symbolen beklebt werden, um den Inhalt visuell zu repräsentieren (z.B. ein Bild aus einem Hörbuch oder das Cover eines Albums).
Schritt 6: Spotify-Playlists zuordnen
Jetzt musst du die Spotify-Playlist-URIs für die NFC-Tags sammeln:
- Öffne Spotify und navigiere zu der Playlist, die du dem NFC-Tag zuordnen möchtest
- Klicke mit der rechten Maustaste auf die Playlist und wähle “Teilen” > “URI kopieren”
- Die kopierte URI sollte etwa so aussehen: “spotify:playlist:0FExWK304ULhIT9UrgcBSV”
Trage die UIDs und URIs im JavaScript-Code ein:
// RFID zu Playlist-Zuordnung
const nfcToPlaylist = {
"048D4B91C22A81": "spotify:playlist:0FExWK304ULhIT9UrgcBSV",
"A1B2C3D4E5F6": "spotify:playlist:1a2b3c4d5e6f7g8h9i0j",
// Weitere Zuordnungen hier hinzufügen
};
Schritt 7: System testen und debuggen
Nach Abschluss aller Einrichtungsschritte solltest du das System gründlich testen:
- Stelle sicher, dass der Google Home Mini eingeschaltet und mit dem Internet verbunden ist
- Halte ein konfiguriertes NFC-Tag an den RFID-Leser
- Beobachte die Logs in ioBroker, um den Fortschritt zu verfolgen
- Der Google Home Mini sollte die entsprechende Playlist abspielen
Bei Problemen bietet der JavaScript-Code verschiedene Debugging-Möglichkeiten:
- Der State
javascript.0.SpotifyDirectAPI.testConnection
kann auftrue
gesetzt werden, um die Verbindung zu testen - Der State
javascript.0.SpotifyDirectAPI.wakeUpGoogleHome
kann auftrue
gesetzt werden, um das Google Home zu “wecken” - Die Logs in ioBroker zeigen detaillierte Informationen über jeden Schritt des Prozesses
Wartung und Erweiterung
Regelmäßige Wartung
Das System wurde so konzipiert, dass es weitgehend wartungsfrei läuft. Es gibt jedoch einige Punkte, die du im Auge behalten solltest:
- Die Spotify-Tokens werden automatisch alle drei Stunden aktualisiert
- Der Google Home wird stündlich überprüft, um sicherzustellen, dass er verfügbar ist
- Bei längeren Stromausfällen muss sich der ESP8266 möglicherweise neu mit dem WLAN verbinden
Mögliche Erweiterungen
Das System bietet zahlreiche Erweiterungsmöglichkeiten:
- Mehrere Google Home-Geräte: Der Code kann erweitert werden, um verschiedene NFC-Tags mit unterschiedlichen Google Home-Geräten zu verknüpfen.
- Physisches Gehäuse: Ein 3D-gedrucktes Gehäuse für den ESP8266 und den RFID-Leser kann das System kindgerechter gestalten.
- Statusanzeige: LEDs können hinzugefügt werden, um den Status des Systems anzuzeigen (z.B. bereit, lesend, Fehler).
- Weitere Aktionen: Neben dem Abspielen von Playlists könnten andere Aktionen implementiert werden, wie das Pausieren der Wiedergabe oder das Überspringen von Titeln.
- Integration mit anderen Smart-Home-Systemen: Das System könnte mit anderen ioBroker-Adaptern interagieren, um z.B. beim Abspielen bestimmter Playlists das Licht zu dimmen.
Fazit
Dieses DIY-Projekt bietet eine kostengünstige und flexible Alternative zu kommerziellen Kindermusikboxen wie der Toniebox. Durch die Integration von ESP8266, RFID-Technologie, ioBroker und Spotify entsteht ein System, das nicht nur kinderleicht zu bedienen ist, sondern auch vollständig anpassbar und erweiterbar bleibt.
Die Kombination aus Hardware und Software demonstriert eindrucksvoll, wie mit verhältnismäßig geringem Aufwand ein sinnvolles und pädagogisch wertvolles Smart-Home-Produkt geschaffen werden kann. Kinder können ihre Lieblingsmusik und Hörbücher selbstständig starten, während Eltern die volle Kontrolle über die Inhalte behalten und diese jederzeit aktualisieren können.
Das Projekt zeigt zudem, wie vorhandene Smart-Home-Geräte wie der Google Home Mini auf kreative Weise in neue Anwendungsszenarien integriert werden können. So wird nicht nur ein neues Gerät geschaffen, sondern gleichzeitig der Nutzen bestehender Geräte erweitert.
Besonders wertvoll ist die Möglichkeit, individuelle Spotify-Konten zu nutzen. Dies ermöglicht personalisierte Inhalte für jedes Kind und verhindert, dass die persönlichen Empfehlungen der Eltern durch Kinderinhalte beeinflusst werden.
Mit diesem Projekt hast du eine zukunftssichere, erweiterbare Lösung geschaffen, die mit deinem Kind mitwachsen kann – ein perfektes Beispiel für sinnvolles DIY im Smart-Home-Bereich.
Ich hoffe, dieser Beitrag bietet dir einen Lösungsansatz, um für dich eine alternative zur Toniebox zu bauen. Hast du weitere Einsatzideen, Fragen oder Verbesserungen, dann schreib mir hier bitte direkt einen Kommentar!
Hat dich diese Anleitung inspiriert oder dir bei deinem Projekt geholfen? Falls ja, freue ich mich über deine Spende unkompliziert bei KoFi.
Das motiviert mich, weiterhin mein Wissen zu teilen und die Community damit zu unterstützen. Vielen Dank!