NEWS
Obi Bluetooth Lichterkette in IOBroker integrieren
-
Hallo Zusammen,
ich bin echt am verzweifeln, habe keine Lust mehr und brauche dringend ein frisches paar Augen!
Alles hat damit angefangen, dass meine Frau davon abgeraten hat, die 5. Twinkly Lichterkette für den Garten zu bestellen!
Also bin ich zum Obi und habe mich nach Alternativen umgeguckt und bin dabei auf diese Lichterkette gestoßen!
Die perfekte Länge und Bluetooth Steuerung per App, da dachte ich mir, perfekt, die kriege ich bestimmt irgendwie in den IOBroker integriert und kann sie dann zusammen mit den anderen Lichterketten über den Außenlichtsensor dimmen bzw. ein und ausschalten. (Außerdem liebe ich die Herausforderung )Zu diesem Zeitpunkt dachte ich noch, der schwierigste Teil wird, die funktionsweise der App reverse zu engineeren und herauszufinden, was ich senden muss um die verschiedenen Funktionen zu triggern. Das war allerdings dank eines alten Android Handys meiner Frau und Wireshark ziemlich easy. Also kannte ich jetzt die Daten die ich senden muss und die richtigen Addressen. Am Handy hat es sofort funktioniert und ich dachte, ok das war einfach, jetzt lade ich mir einfach einen IOB Adapter runter der Bluetooth Befehle senden kann (sowie die 1000 IPhone und Android Apps die es da gibt) und das war's. Leider hab ich dann gemerkt den gibt es nicht!
Also bin ich selbst zur tat geschritten und habe folgendes Skript programmiert und es funktioniert fast alles!
Die Lichterkette wird verbunden, ich kann die Services auslesen aber ich KRIEG EINFACH KEINE BEFEHLE GESENDET, immer Fehler beim Schreiben des Wertes in die Characteristic!!Ich bin echt mit meinem Latein am Ende! Hier das Skript:
// Laden des dbus-next Moduls const dbus = require('dbus-next'); // Pfad zum Datenpunkt const datapointPath = 'javascript.0.MeineVariablen.Lichterkette_OnOff'; // Zustand 'Lichterkette_OnOff' erstellen, falls nicht vorhanden createState(datapointPath, false, { name: 'Lichterkette On/Off', type: 'boolean', role: 'switch', read: true, write: true, def: false }, () => { // Event Listener für Zustandsänderungen registrieren on({ id: datapointPath, change: 'ne' }, (obj) => { const action = obj.state.val ? 'on' : 'off'; controlLight(action) .then(() => { console.log(`Lichterkette wurde ${action === 'on' ? 'eingeschaltet' : 'ausgeschaltet'}`); }) .catch((err) => { console.error('Fehler bei der Steuerung der Lichterkette:', err.message || err); }); }); }); // Hauptfunktion zur Steuerung der Lichterkette function controlLight(action) { return new Promise(async (resolve, reject) => { const bus = dbus.systemBus(); const bluezServiceName = 'org.bluez'; const adapterPath = '/org/bluez/hci0'; // Bluetooth-Adapter (hci0) const deviceAddress = '24:35:02:27:DE:6E'; // MAC-Adresse Lichterkette const devicePath = adapterPath + '/dev_' + deviceAddress.replace(/:/g, '_'); try { // Verbindung zum BlueZ-Service herstellen const bluez = await bus.getProxyObject(bluezServiceName, '/'); const manager = bluez.getInterface('org.freedesktop.DBus.ObjectManager'); // Alle verwalteten Objekte abrufen const objects = await manager.GetManagedObjects(); // Prüfen, ob das Gerät bereits bekannt ist let deviceFound = false; for (const [path, interfaces] of Object.entries(objects)) { if (path === devicePath) { deviceFound = true; break; } } // Wenn das Gerät nicht gefunden wurde, Discovery starten if (!deviceFound) { console.log('Gerät nicht gefunden, starte Discovery...'); const adapter = await bus.getProxyObject(bluezServiceName, adapterPath); const adapterInterface = adapter.getInterface('org.bluez.Adapter1'); await adapterInterface.StartDiscovery(); // Warten, bis das Gerät gefunden wird oder Timeout nach 15 Sekunden await new Promise((resolveDiscovery, rejectDiscovery) => { const timeout = setTimeout(() => { rejectDiscovery(new Error('Gerät wurde während der Discovery nicht gefunden')); }, 15000); // Timeout nach 15 Sekunden manager.on('InterfacesAdded', (path, interfaces) => { if (path === devicePath) { clearTimeout(timeout); resolveDiscovery(); } }); }); await adapterInterface.StopDiscovery(); } // Verbindung zum Gerät herstellen const device = await bus.getProxyObject(bluezServiceName, devicePath); const deviceInterface = device.getInterface('org.bluez.Device1'); try { await deviceInterface.Connect(); console.log('Verbunden mit der Lichterkette'); // Verzögerung erhöhen await new Promise((resolveDelay) => setTimeout(resolveDelay, 5000)); // 5 Sekunden warten // Services und Characteristics auflisten await listServicesAndCharacteristics(devicePath, bus); } catch (error) { console.error('Fehler beim Verbinden mit dem Gerät:', error.message || error); return reject(error); } // **Characteristic für Initialisierung und Ein/Aus finden** const characteristicUUID = '0000fff1-0000-1000-8000-00805f9b34fb'; // Verwenden von 0000fff1-... für beide Befehle const characteristicPath = await findCharacteristicPath(devicePath, characteristicUUID, bus); if (!characteristicPath) { console.error('Characteristic für Ein/Aus nicht gefunden'); await deviceInterface.Disconnect(); return reject(new Error('Characteristic für Ein/Aus nicht gefunden')); } // Interface der Characteristic abrufen const characteristic = await bus.getProxyObject(bluezServiceName, characteristicPath); const characteristicInterface = characteristic.getInterface('org.bluez.GattCharacteristic1'); // **Ein-/Ausschaltbefehl senden** // Befehl basierend auf der Aktion definieren const value = action === 'on' ? [0x01, 0x01, 0x01, 0x01] : [0x01, 0x01, 0x01, 0x00]; try { await characteristicInterface.WriteValue(Buffer.from(value), { type: dbus.Variant('s', 'command') }); console.log(`Lichterkette wurde ${action === 'on' ? 'eingeschaltet' : 'ausgeschaltet'}`); } catch (error) { console.error('Fehler beim Schreiben des Wertes in die Characteristic:', error.message || error); } // Verbindung zum Gerät trennen try { await deviceInterface.Disconnect(); console.log('Verbindung zur Lichterkette getrennt'); } catch (error) { console.error('Fehler beim Trennen der Verbindung zum Gerät:', error.message || error); } resolve(); } catch (err) { console.error('Fehler in controlLight:', err.message || err); reject(err); } }); } // Hilfsfunktion, um den Pfad der Characteristic zu finden function findCharacteristicPath(devicePath, characteristicUUID, bus) { return new Promise(async (resolve) => { const bluezServiceName = 'org.bluez'; const manager = await bus.getProxyObject(bluezServiceName, '/').then((obj) => obj.getInterface('org.freedesktop.DBus.ObjectManager')); const objects = await manager.GetManagedObjects(); for (const [path, interfaces] of Object.entries(objects)) { if (path.startsWith(devicePath) && 'org.bluez.GattCharacteristic1' in interfaces) { const uuid = interfaces['org.bluez.GattCharacteristic1'].UUID.value.toLowerCase(); if (uuid === characteristicUUID.toLowerCase()) { return resolve(path); } } } resolve(null); }); } // Funktion zum Auflisten der verfügbaren Services und Characteristics function listServicesAndCharacteristics(devicePath, bus) { return new Promise(async (resolve) => { const manager = await bus.getProxyObject('org.bluez', '/').then((obj) => obj.getInterface('org.freedesktop.DBus.ObjectManager')); const objects = await manager.GetManagedObjects(); console.log(`Anzahl der Objekte: ${Object.keys(objects).length}`); console.log('Verfügbare Services und Characteristics:'); let found = false; for (const [path, interfaces] of Object.entries(objects)) { if (path.startsWith(devicePath)) { found = true; console.log(`Gefundenes Objekt: ${path}`); if ('org.bluez.GattService1' in interfaces) { const uuid = interfaces['org.bluez.GattService1'].UUID.value; console.log(`Service: ${path}, UUID: ${uuid}`); } if ('org.bluez.GattCharacteristic1' in interfaces) { const uuid = interfaces['org.bluez.GattCharacteristic1'].UUID.value; const flags = interfaces['org.bluez.GattCharacteristic1'].Flags.value; console.log(`Characteristic: ${path}, UUID: ${uuid}, Flags: ${flags}`); } } } if (!found) { console.log('Keine Services oder Characteristics gefunden.'); } resolve(); }); }
In der Konsole funktioniert es übrigens:
mitbluetoothctl power on scan on connect 24:35:02:27:DE:6E gatt.select-attribute 0000fff1-0000-1000-8000-00805f9b34fb Aus: gatt.write "0x01 0x01 0x01 0x00" Ein: gatt.write "0x01 0x01 0x01 0x01"
Kann ich die Lichterkette aus der Konsole ein- und ausschalten, aber ich finde einfach keinen Weg, das ganze in ein Skript zu packen.
Vielen Dank für eure Hilfe,
Fabian -
ok ich habe es hinbekommen, zwar mit exec, aber besser als garnichts!
Ich habe jetzt also eine Obi Bluetooth Lichterkette die per Alexa steuerbar ist.
Es funktioniert bis jetzt An und Aus und Helligkeit in %. Falls noch jemand vor einer ähnlichen Problem steht, hier die Lösung:const { exec } = require('child_process'); // MAC-Adresse des Geräts und UUID der GATT-Characteristic const deviceAddress = '24:35:02:27:DE:6E'; const characteristicUUID = '0000fff1-0000-1000-8000-00805f9b34fb'; // Pfade zu den Datenpunkten const datapointOnOff = 'javascript.0.MeineVariablen.Lichterkette_OnOff'; const datapointBrightness = 'javascript.0.MeineVariablen.Lichterkette_Helligkeit'; // Warteschlange und Status const actionQueue = []; let isProcessing = false; // Funktion: Shell-Befehl ausführen mit Debugging und Timeout function executeBluetoothCommand(command, timeout = 10000) { console.log(`Führe aus: ${command}`); return new Promise((resolve, reject) => { const process = exec(command, (error, stdout, stderr) => { if (error) { console.error(`Fehler: ${stderr || error.message}`); return reject(stderr || error.message); } console.log(`Erfolg: ${stdout}`); resolve(stdout.trim()); }); // Timeout hinzufügen setTimeout(() => { process.kill('SIGTERM'); reject('Befehl abgebrochen (Timeout)'); }, timeout); }); } // Funktion: Gerät ist verbunden prüfen async function isDeviceConnected() { try { const output = await executeBluetoothCommand(`bluetoothctl info ${deviceAddress}`); return output.includes('Connected: yes'); } catch (error) { console.warn('Verbindungsprüfung fehlgeschlagen:', error); return false; } } // Funktion: Wiederholungsversuch für einen Befehl async function retryCommand(command, retries = 10) { for (let i = 0; i < retries; i++) { try { return await executeBluetoothCommand(command); } catch (error) { console.warn(`Fehler beim Befehl: ${command}, Versuch ${i + 1} von ${retries}`); } } throw new Error(`Befehl fehlgeschlagen nach ${retries} Versuchen: ${command}`); } // Funktion: Gerät verbinden async function connectToDevice() { try { console.log('Versuche Verbindung zum Gerät herzustellen...'); if (!(await isDeviceConnected())) { await retryCommand(`bluetoothctl connect ${deviceAddress}`); } console.log('Verbindung erfolgreich hergestellt.'); } catch (error) { console.error('Verbindung fehlgeschlagen, versuche Fehler zu beheben...'); await retryCommand(`bluetoothctl power on`); await retryCommand(`bluetoothctl connect ${deviceAddress}`); console.log('Verbindung nach Fehlerbehebung erfolgreich.'); } } // Funktion: Gerät trennen async function disconnectDevice() { try { console.log('Trenne Verbindung zum Gerät...'); if (await isDeviceConnected()) { await retryCommand(`bluetoothctl disconnect ${deviceAddress}`); } console.log('Verbindung erfolgreich getrennt.'); } catch (error) { console.error('Fehler beim Trennen der Verbindung:', error); } } // Funktion: GATT-Wert schreiben async function writeGattValue(value) { try { console.log(`Schreibe Wert: ${value} an die GATT-Characteristic...`); const script = ` bluetoothctl << EOF menu gatt select-attribute ${characteristicUUID} write ${value} exit EOF `; await executeBluetoothCommand(script); } catch (error) { console.error('Fehler beim Schreiben auf die GATT-Characteristic:', error); throw new Error('GATT-Wert konnte nicht geschrieben werden.'); } } // Funktion: Helligkeit berechnen function calculateBrightnessHex(brightnessPercent) { const brightnessHex = Math.round((brightnessPercent / 100) * 0x64).toString(16).padStart(2, '0'); return `"0x03 0x01 0x01 0x${brightnessHex}"`; } // Funktion: Lichterkette steuern async function controlLight(action, brightness = null) { try { await connectToDevice(); if (action) { if (brightness !== null) { const brightnessValue = calculateBrightnessHex(brightness); await writeGattValue(brightnessValue); } else { const value = action === 'on' ? '"0x01 0x01 0x01 0x01"' : '"0x01 0x01 0x01 0x00"'; await writeGattValue(value); } } await disconnectDevice(); } catch (error) { console.error('Fehler bei der Steuerung der Lichterkette:', error); } } // Funktion: Warteschlange abarbeiten async function processQueue() { if (isProcessing) return; // Verhindert gleichzeitige Verarbeitung isProcessing = true; while (actionQueue.length > 0) { const { action, brightness } = actionQueue.shift(); // Nächsten Befehl aus der Warteschlange holen console.log(`Verarbeite Aktion: ${action}, Helligkeit: ${brightness}`); await controlLight(action, brightness); } isProcessing = false; } // Datenpunkte erstellen, falls nicht vorhanden createState(datapointOnOff, false, { name: 'Lichterkette On/Off', type: 'boolean', role: 'switch', read: true, write: true, def: false }, () => { // Event Listener für Zustandsänderungen registrieren on({ id: datapointOnOff, change: 'ne' }, async (obj) => { const action = obj.state.val ? 'on' : 'off'; actionQueue.push({ action, brightness: null }); processQueue(); }); }); createState(datapointBrightness, 100, { name: 'Lichterkette Helligkeit', type: 'number', role: 'level.dimmer', read: true, write: true, def: 100, min: 0, max: 100 }, () => { // Event Listener für Helligkeitsänderungen registrieren on({ id: datapointBrightness, change: 'ne' }, async (obj) => { const brightness = obj.state.val; if (brightness < 3) { // Wenn Helligkeit unter 3%, Lichterkette ausschalten console.log('Helligkeit unter 3% - Lichterkette wird ausgeschaltet.'); setState(datapointOnOff, false); // Setzt On/Off-Datenpunkt auf false } else { // Ansonsten Helligkeit setzen actionQueue.push({ action: 'on', brightness }); processQueue(); } }); }); // Debugging: Manuelle Steuerung (async () => { try { console.log('Teste Steuerung: Einschalten mit 50% Helligkeit...'); actionQueue.push({ action: 'on', brightness: 50 }); processQueue(); console.log('Teste Steuerung: Ausschalten...'); actionQueue.push({ action: 'off', brightness: null }); processQueue(); } catch (error) { console.error('Fehler bei manueller Steuerung:', error); } })();
-
@fabian1 sagte in Obi Bluetooth Lichterkette in IOBroker integrieren:
bluetoothctl
das hier ist die javascript bibliothek die direkten zugriff auf den bluetooth stack bereitstellt.. ob das da so einfach ist wie mit bluetoothctl, weiß ich nicht.
https://www.npmjs.com/package/node-bleich hab mir mal was mit dbus events gebaut, die auf bluetooth events horchen.
das war ein krampf. das war allerdings auch nicht in javascript sondern mit python. -
@oliverio node-ble und noble habe ich zuerst versucht, das hat überhaupt nicht funktioniert, darum bin ich dann auf dbus gewechselt und das hätte auch super funktioniert, wenn ich rausgefunden hätte, wie ich die Befehle im richtigen Format sende. Die Lichterkette erwartet nämlich strings und das habe ich einfach nicht hinbekommen und meine online suche hat gezeigt, ich bin da nicht der Einzige. Irgendwie ist alles was mit bluetooth zu tun hat immer ein absoluter krampf!
Ich habe jetzt eine neue Version die 100% zuverlässig ist, falls das jemand gebrauchen kann: (diese benutzt das gatttool)
const { exec } = require('child_process'); // MAC-Adresse des Geräts und Handle der GATT-Characteristic const deviceAddress = '24:35:02:27:DE:6E'; const characteristicHandle = '0x0025'; // Pfade zu den Datenpunkten const datapointOnOff = 'javascript.0.MeineVariablen.Lichterkette_OnOff'; const datapointBrightness = 'javascript.0.MeineVariablen.Lichterkette_Helligkeit'; // Warteschlange und Status const actionQueue = []; let isProcessing = false; // Funktion: Shell-Befehl ausführen mit Debugging und Timeout function executeCommand(command, timeout = 10000) { console.log(`Führe aus: ${command}`); return new Promise((resolve, reject) => { const process = exec(command, (error, stdout, stderr) => { if (error) { console.error(`Fehler: ${stderr || error.message}`); return reject(stderr || error.message); } console.log(`Erfolg: ${stdout}`); resolve(stdout.trim()); }); setTimeout(() => { process.kill('SIGTERM'); reject('Befehl abgebrochen (Timeout)'); }, timeout); }); } // Funktion: GATT-Wert schreiben mit Wiederholungslogik async function writeGattValueWithRetries(value, maxRetries = 10, initialDelay = 500) { let attempts = 0; let delay = initialDelay; while (attempts < maxRetries) { try { console.log(`Versuch ${attempts + 1}/${maxRetries}: Sende GATT-Befehl`); await writeGattValue(value); console.log('Befehl erfolgreich gesendet.'); return; // Erfolgreich, keine weiteren Versuche notwendig } catch (error) { attempts++; console.error(`Fehler beim Senden (Versuch ${attempts}):`, error); if (attempts >= maxRetries) { console.error('Maximale Anzahl an Versuchen erreicht. Abbruch.'); throw new Error('GATT-Befehl konnte nach mehreren Versuchen nicht gesendet werden.'); } console.log(`Warte ${delay} ms vor erneutem Versuch...`); await new Promise((resolve) => setTimeout(resolve, delay)); delay *= 2; // Verzögerung verdoppeln } } } // Funktion: GATT-Wert schreiben async function writeGattValue(value) { const command = `gatttool -b ${deviceAddress} --char-write-req --handle=${characteristicHandle} --value="${value}"`; console.log(`Sende GATT-Befehl: ${command}`); try { const output = await executeCommand(command); console.log(`GATT-Befehl erfolgreich: ${output}`); } catch (error) { console.error('Fehler beim Schreiben auf die GATT-Characteristic:', error); throw new Error('GATT-Wert konnte nicht geschrieben werden.'); } } // Funktion: Helligkeit berechnen function calculateBrightnessHex(brightnessPercent) { const brightnessHex = Math.round((brightnessPercent / 100) * 0x64).toString(16).padStart(2, '0'); return `030101${brightnessHex}`; } // Funktion: Lichterkette steuern async function controlLight(action, brightness = null) { try { if (action) { if (brightness !== null) { const brightnessValue = calculateBrightnessHex(brightness); await writeGattValueWithRetries(brightnessValue); } else { const value = action === 'on' ? '01010101' : '01010100'; await writeGattValueWithRetries(value); } } } catch (error) { console.error('Fehler bei der Steuerung der Lichterkette:', error); } } // Funktion: Warteschlange abarbeiten async function processQueue() { if (isProcessing) return; // Verhindert gleichzeitige Verarbeitung isProcessing = true; while (actionQueue.length > 0) { const { action, brightness } = actionQueue.shift(); // Nächsten Befehl aus der Warteschlange holen console.log(`Verarbeite Aktion: ${action}, Helligkeit: ${brightness}`); await controlLight(action, brightness); } isProcessing = false; } // Datenpunkte erstellen, falls nicht vorhanden createState(datapointOnOff, false, { name: 'Lichterkette On/Off', type: 'boolean', role: 'switch', read: true, write: true, def: false }, () => { // Event Listener für Zustandsänderungen registrieren on({ id: datapointOnOff, change: 'ne' }, async (obj) => { const action = obj.state.val ? 'on' : 'off'; actionQueue.push({ action, brightness: null }); processQueue(); }); }); createState(datapointBrightness, 100, { name: 'Lichterkette Helligkeit', type: 'number', role: 'level.dimmer', read: true, write: true, def: 100, min: 0, max: 100 }, () => { // Event Listener für Helligkeitsänderungen registrieren on({ id: datapointBrightness, change: 'ne' }, async (obj) => { const brightness = obj.state.val; if (brightness < 3) { // Wenn Helligkeit unter 3%, Lichterkette ausschalten console.log('Helligkeit unter 3% - Lichterkette wird ausgeschaltet.'); setState(datapointOnOff, false); // Setzt On/Off-Datenpunkt auf false } else { // Ansonsten Helligkeit setzen actionQueue.push({ action: 'on', brightness }); processQueue(); } }); }); // Debugging: Manuelle Steuerung /*(async () => { try { console.log('Teste Steuerung: Einschalten mit 50% Helligkeit...'); actionQueue.push({ action: 'on', brightness: 50 }); processQueue(); console.log('Teste Steuerung: Ausschalten...'); actionQueue.push({ action: 'off', brightness: null }); processQueue(); } catch (error) { console.error('Fehler bei manueller Steuerung:', error); } })(); */