NEWS
Test Adapter für Blink Kameras entwickelt mit KI
-
Aktuelle Testversion 0.0.6 Github Link https://github.com/Pischleuder1/ioBroker.blink Veröffentlichungsdatum 28.04.2026 In den letzten Jahren gab es diverse BLINK Adapter für die Amazon Kameras, die jedoch nicht weiterentwickelt wurden und alle, mehr oder weniger, auf blinkpy basierten. Als Alternative funktionierten temporär python scripte mit blinkpy oder die Option über IFTTT.
Zuletzt sind die meisten sicherlich am HAM Adapter oder Homeassistant hängen geblieben, um die Kameras zu steuern. Da Amazon jedoch wieder einmal an den API herumbastelt, funktioniert das tlw. nur noch suboptimal.
Die Idee mit blinkpy zu arbeiten habe ich auf anraten des Forums verworfen und einen Adapter (unter Zuhilfenahme von KI) erstellt, um die gesamte Login - Logik im Adapter nachzubauen.Funktionen:
• Kameras und Sync-Modul werden ausgelesen und die entsprechenden States etc. angezeigt
• Temperaturanzeige über die Kamera in Grad Celsius und Fahrenheit
• Batterienanzeige der Kamera, umgerechnet in Volt
• bei geringem Batteriestand wird / kann eine pushover oder telegram Info gesendet werden
• Snapshot von Bildern über commands in einen state bzw. auch lokal in den Ordner /opt/iobroker/iobroker-data/blink
• Snapshot als image_base64 mit Zeitstempel
• automatische Erzeugung von Snapshots nach Zeit über admin Bereich
• motion detect
• aktuell, von Blink gespeicherte Videos werden in die entsprechenden Datenpunkte geschrieben oder per fetch geholt
• Speicherort kann im Admin Bereich festgelegt werdenWas funktioniert (noch) nicht:
• kein Echtzeit Video (in Arbeit)
• Video Doorbell(in Arbeit)sollte mit 0.0.4 unterstützt werden, jedoch keine Temperaturanzeige, da diese offensichtlich bei der Doorbell nicht verbaut istVIS-Widget
Zusätzlich habe ich ein VIS-Widget Generator erstellt, bei dem ihr lediglich die Kamera ID´s eingeben müsst und es wird daraus ein json erstellt, welches als widget in Vis importiert werden kann1.) Widgetgenerator für Cameras ohne letzte Videodarstellung:
2.) Widgetgenerator mit Anzeige der zuletzt gespeicherten Videos. Dazu muss ein javascript in den Scripten erstellt werden mit dem Inhalt unter Code, im Code bitte die Kamera ID´s anpassen !:
// ioBroker JavaScript Adapter Script // Blink-Videos aus blink.0.cameras.<ID>.video.file lesen, // per ffmpeg konvertieren und direkt als Data-URL-States bereitstellen. // // Ergebnis: // javascript.0.blinkVideos.<ID>.data_url = MP4 / H.264 + AAC, gut für Safari // javascript.0.blinkVideos.<ID>.data_url_webm = WebM / VP8 + Opus, gut für Chrome/Edge // // Voraussetzung auf dem ioBroker-Server: // sudo apt install ffmpeg const fs = require('fs'); const path = require('path'); const { execFile } = require('child_process'); const BLINK_INSTANCE = 'blink.0'; // HIER deine Kamera-IDs eintragen/anpassen: const CAMERA_IDS = [ '548730', '1136145', '1136121', '1723473' ]; const OUT_PREFIX = 'blinkVideos'; const FFMPEG = '/usr/bin/ffmpeg'; const FFPROBE = '/usr/bin/ffprobe'; const CACHE_DIR = '/opt/iobroker/iobroker-data/blink-vis-cache'; // Limit pro fertiger Datei, bevor sie als Base64 in einen State geschrieben wird const MAX_MB_MP4 = 50; const MAX_MB_WEBM = 50; // Warten, damit Blink/ioBroker die Datei sicher fertig geschrieben hat const READ_DELAY_MS = 5000; const timers = {}; const running = {}; const rerun = {}; function localId(camera, name) { return `${OUT_PREFIX}.${camera}.${name}`; } function ensureState(id, def, common) { try { createState(id, def, Object.assign({ read: true, write: false }, common)); } catch (e) { log(`createState ${id}: ${e.message || e}`, 'warn'); } } function setLocal(camera, name, value) { try { setState(localId(camera, name), value, true); } catch (e) { log(`setState ${localId(camera, name)}: ${e.message || e}`, 'warn'); } } function ensureCameraStates(camera) { ensureState(localId(camera, 'data_url'), '', { name: `Blink ${camera} MP4 Data URL`, type: 'string', role: 'text' }); ensureState(localId(camera, 'data_url_webm'), '', { name: `Blink ${camera} WebM Data URL`, type: 'string', role: 'text' }); ensureState(localId(camera, 'source_file'), '', { name: `Blink ${camera} Original video.file`, type: 'string', role: 'text' }); ensureState(localId(camera, 'converted_file'), '', { name: `Blink ${camera} konvertierte MP4-Datei`, type: 'string', role: 'text' }); ensureState(localId(camera, 'converted_file_webm'), '', { name: `Blink ${camera} konvertierte WebM-Datei`, type: 'string', role: 'text' }); ensureState(localId(camera, 'original_size'), 0, { name: `Blink ${camera} Originalgröße`, type: 'number', role: 'value', unit: 'Bytes' }); ensureState(localId(camera, 'converted_size'), 0, { name: `Blink ${camera} MP4 Größe`, type: 'number', role: 'value', unit: 'Bytes' }); ensureState(localId(camera, 'converted_size_webm'), 0, { name: `Blink ${camera} WebM Größe`, type: 'number', role: 'value', unit: 'Bytes' }); ensureState(localId(camera, 'data_url_length'), 0, { name: `Blink ${camera} MP4 Data URL Länge`, type: 'number', role: 'value' }); ensureState(localId(camera, 'data_url_webm_length'), 0, { name: `Blink ${camera} WebM Data URL Länge`, type: 'number', role: 'value' }); ensureState(localId(camera, 'updated'), 0, { name: `Blink ${camera} aktualisiert`, type: 'number', role: 'date' }); ensureState(localId(camera, 'ffprobe'), '', { name: `Blink ${camera} ffprobe MP4`, type: 'string', role: 'text' }); ensureState(localId(camera, 'ffprobe_webm'), '', { name: `Blink ${camera} ffprobe WebM`, type: 'string', role: 'text' }); ensureState(localId(camera, 'status'), '', { name: `Blink ${camera} Status`, type: 'string', role: 'text' }); ensureState(localId(camera, 'error'), '', { name: `Blink ${camera} Fehler`, type: 'string', role: 'text' }); } function readStateValue(id) { const s = getState(id); return s && s.val !== null && s.val !== undefined ? String(s.val).trim() : ''; } function finish(camera) { running[camera] = false; if (rerun[camera]) { rerun[camera] = false; scheduleConvert(camera, 3000); } } function fail(camera, message) { setLocal(camera, 'status', 'Fehler'); setLocal(camera, 'error', message); log(`Blink ${camera}: ${message}`, 'warn'); finish(camera); } function waitForStableFile(filePath, callback) { let lastSize = -1; let stableCount = 0; let tries = 0; const timer = setInterval(() => { tries++; fs.stat(filePath, (err, stat) => { if (err) { clearInterval(timer); callback(err); return; } if (stat.size > 0 && stat.size === lastSize) { stableCount++; } else { stableCount = 0; lastSize = stat.size; } if (stableCount >= 2) { clearInterval(timer); callback(null, stat); return; } if (tries >= 25) { clearInterval(timer); callback(new Error(`Dateigröße wurde nicht stabil: ${filePath}`)); } }); }, 1000); } function probeFile(camera, filePath, stateName, callback) { const args = [ '-v', 'error', '-show_streams', '-show_format', filePath ]; execFile(FFPROBE, args, { timeout: 30000 }, (err, stdout, stderr) => { const output = String(stdout || stderr || (err && err.message) || '').slice(0, 12000); setLocal(camera, stateName, output); callback(); }); } function removeIfExists(filePath) { try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } catch (e) { // ignorieren } } function transcodeToMp4(inputFile, outputFile, callback) { const tmpFile = `${outputFile}.tmp.mp4`; removeIfExists(tmpFile); const args = [ '-hide_banner', '-nostdin', '-y', '-fflags', '+genpts', '-err_detect', 'ignore_err', '-i', inputFile, '-map', '0:v:0', '-map', '0:a:0?', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23', '-pix_fmt', 'yuv420p', '-profile:v', 'baseline', '-level', '3.1', '-tag:v', 'avc1', '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', '-c:a', 'aac', '-b:a', '128k', '-ac', '2', '-ar', '44100', '-movflags', '+faststart', '-f', 'mp4', tmpFile ]; execFile(FFMPEG, args, { timeout: 120000 }, (err, stdout, stderr) => { if (err) { callback(new Error(`ffmpeg MP4 Fehler: ${stderr || err.message}`)); return; } try { removeIfExists(outputFile); fs.renameSync(tmpFile, outputFile); } catch (e) { callback(new Error(`MP4 konnte nicht übernommen werden: ${e.message}`)); return; } callback(null); }); } function transcodeToWebm(inputFile, outputFile, callback) { const tmpFile = `${outputFile}.tmp.webm`; removeIfExists(tmpFile); const args = [ '-hide_banner', '-nostdin', '-y', '-fflags', '+genpts', '-err_detect', 'ignore_err', '-i', inputFile, '-map', '0:v:0', '-map', '0:a:0?', // Chrome/Edge-kompatibles WebM '-c:v', 'libvpx', '-deadline', 'realtime', '-cpu-used', '5', '-b:v', '0', '-crf', '32', '-pix_fmt', 'yuv420p', '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', // Audio behalten und nach Opus wandeln '-c:a', 'libopus', '-b:a', '96k', '-ac', '2', '-ar', '48000', '-f', 'webm', tmpFile ]; execFile(FFMPEG, args, { timeout: 180000 }, (err, stdout, stderr) => { if (err) { callback(new Error(`ffmpeg WebM Fehler: ${stderr || err.message}`)); return; } try { removeIfExists(outputFile); fs.renameSync(tmpFile, outputFile); } catch (e) { callback(new Error(`WebM konnte nicht übernommen werden: ${e.message}`)); return; } callback(null); }); } function writeDataUrl(camera, filePath, mimeType, dataState, lengthState, sizeState, maxMb, callback) { fs.stat(filePath, (statErr, stat) => { if (statErr) { callback(new Error(`Datei nicht lesbar: ${statErr.message}`)); return; } const maxBytes = maxMb * 1024 * 1024; if (stat.size > maxBytes) { callback(new Error(`Video zu groß für State: ${stat.size} Bytes, Limit ${maxBytes} Bytes`)); return; } fs.readFile(filePath, (readErr, buf) => { if (readErr) { callback(new Error(`Video konnte nicht gelesen werden: ${readErr.message}`)); return; } const dataUrl = `data:${mimeType};base64,${buf.toString('base64')}`; setLocal(camera, dataState, dataUrl); setLocal(camera, lengthState, dataUrl.length); setLocal(camera, sizeState, stat.size); callback(null); }); }); } function convertVideo(camera) { if (running[camera]) { rerun[camera] = true; return; } running[camera] = true; const sourceState = `${BLINK_INSTANCE}.cameras.${camera}.video.file`; const sourceFile = readStateValue(sourceState); if (!sourceFile) { fail(camera, `${sourceState} ist leer`); return; } setLocal(camera, 'source_file', sourceFile); setLocal(camera, 'data_url', ''); setLocal(camera, 'data_url_webm', ''); setLocal(camera, 'data_url_length', 0); setLocal(camera, 'data_url_webm_length', 0); setLocal(camera, 'error', ''); setLocal(camera, 'status', 'Warte auf stabile Videodatei ...'); setTimeout(() => { waitForStableFile(sourceFile, (stableErr, originalStat) => { if (stableErr) { fail(camera, stableErr.message); return; } setLocal(camera, 'original_size', originalStat.size); try { if (!fs.existsSync(CACHE_DIR)) { fs.mkdirSync(CACHE_DIR, { recursive: true }); } } catch (e) { fail(camera, `Cache-Verzeichnis konnte nicht erstellt werden: ${e.message}`); return; } const mp4File = path.join(CACHE_DIR, `${camera}_browser.mp4`); const webmFile = path.join(CACHE_DIR, `${camera}_browser.webm`); setLocal(camera, 'converted_file', mp4File); setLocal(camera, 'converted_file_webm', webmFile); setLocal(camera, 'status', 'Konvertiere MP4 für Safari ...'); transcodeToMp4(sourceFile, mp4File, (mp4Err) => { if (mp4Err) { fail(camera, mp4Err.message); return; } setLocal(camera, 'status', 'Prüfe MP4 ...'); probeFile(camera, mp4File, 'ffprobe', () => { setLocal(camera, 'status', 'Schreibe MP4 Data URL ...'); writeDataUrl( camera, mp4File, 'video/mp4', 'data_url', 'data_url_length', 'converted_size', MAX_MB_MP4, (mp4WriteErr) => { if (mp4WriteErr) { fail(camera, mp4WriteErr.message); return; } setLocal(camera, 'status', 'Konvertiere WebM für Chrome/Edge ...'); transcodeToWebm(sourceFile, webmFile, (webmErr) => { if (webmErr) { fail(camera, webmErr.message); return; } setLocal(camera, 'status', 'Prüfe WebM ...'); probeFile(camera, webmFile, 'ffprobe_webm', () => { setLocal(camera, 'status', 'Schreibe WebM Data URL ...'); writeDataUrl( camera, webmFile, 'video/webm', 'data_url_webm', 'data_url_webm_length', 'converted_size_webm', MAX_MB_WEBM, (webmWriteErr) => { if (webmWriteErr) { fail(camera, webmWriteErr.message); return; } setLocal(camera, 'updated', Date.now()); setLocal(camera, 'status', 'Fertig'); setLocal(camera, 'error', ''); log(`Blink ${camera}: MP4 + WebM Data-URLs aktualisiert`, 'info'); finish(camera); } ); }); }); } ); }); }); }); }, READ_DELAY_MS); } function scheduleConvert(camera, delayMs) { if (timers[camera]) { clearTimeout(timers[camera]); } timers[camera] = setTimeout(() => { convertVideo(camera); }, delayMs || 3000); } CAMERA_IDS.forEach((camera) => { ensureCameraStates(camera); on({ id: `${BLINK_INSTANCE}.cameras.${camera}.video.file`, change: 'any' }, () => scheduleConvert(camera, 3000)); on({ id: `${BLINK_INSTANCE}.cameras.${camera}.video.timestamp`, change: 'any' }, () => scheduleConvert(camera, 3000)); on({ id: `${BLINK_INSTANCE}.cameras.${camera}.video.ready`, change: 'any' }, () => scheduleConvert(camera, 3000)); // Einmal beim Scriptstart ausführen scheduleConvert(camera, 5000); }); [/s] -
H Homoran verschob dieses Thema von ioBroker Allgemein am
-
Aktuelle Testversion 0.0.6 Github Link https://github.com/Pischleuder1/ioBroker.blink Veröffentlichungsdatum 28.04.2026 In den letzten Jahren gab es diverse BLINK Adapter für die Amazon Kameras, die jedoch nicht weiterentwickelt wurden und alle, mehr oder weniger, auf blinkpy basierten. Als Alternative funktionierten temporär python scripte mit blinkpy oder die Option über IFTTT.
Zuletzt sind die meisten sicherlich am HAM Adapter oder Homeassistant hängen geblieben, um die Kameras zu steuern. Da Amazon jedoch wieder einmal an den API herumbastelt, funktioniert das tlw. nur noch suboptimal.
Die Idee mit blinkpy zu arbeiten habe ich auf anraten des Forums verworfen und einen Adapter (unter Zuhilfenahme von KI) erstellt, um die gesamte Login - Logik im Adapter nachzubauen.Funktionen:
• Kameras und Sync-Modul werden ausgelesen und die entsprechenden States etc. angezeigt
• Temperaturanzeige über die Kamera in Grad Celsius und Fahrenheit
• Batterienanzeige der Kamera, umgerechnet in Volt
• bei geringem Batteriestand wird / kann eine pushover oder telegram Info gesendet werden
• Snapshot von Bildern über commands in einen state bzw. auch lokal in den Ordner /opt/iobroker/iobroker-data/blink
• Snapshot als image_base64 mit Zeitstempel
• automatische Erzeugung von Snapshots nach Zeit über admin Bereich
• motion detect
• aktuell, von Blink gespeicherte Videos werden in die entsprechenden Datenpunkte geschrieben oder per fetch geholt
• Speicherort kann im Admin Bereich festgelegt werdenWas funktioniert (noch) nicht:
• kein Echtzeit Video (in Arbeit)
• Video Doorbell(in Arbeit)sollte mit 0.0.4 unterstützt werden, jedoch keine Temperaturanzeige, da diese offensichtlich bei der Doorbell nicht verbaut istVIS-Widget
Zusätzlich habe ich ein VIS-Widget Generator erstellt, bei dem ihr lediglich die Kamera ID´s eingeben müsst und es wird daraus ein json erstellt, welches als widget in Vis importiert werden kann1.) Widgetgenerator für Cameras ohne letzte Videodarstellung:
2.) Widgetgenerator mit Anzeige der zuletzt gespeicherten Videos. Dazu muss ein javascript in den Scripten erstellt werden mit dem Inhalt unter Code, im Code bitte die Kamera ID´s anpassen !:
// ioBroker JavaScript Adapter Script // Blink-Videos aus blink.0.cameras.<ID>.video.file lesen, // per ffmpeg konvertieren und direkt als Data-URL-States bereitstellen. // // Ergebnis: // javascript.0.blinkVideos.<ID>.data_url = MP4 / H.264 + AAC, gut für Safari // javascript.0.blinkVideos.<ID>.data_url_webm = WebM / VP8 + Opus, gut für Chrome/Edge // // Voraussetzung auf dem ioBroker-Server: // sudo apt install ffmpeg const fs = require('fs'); const path = require('path'); const { execFile } = require('child_process'); const BLINK_INSTANCE = 'blink.0'; // HIER deine Kamera-IDs eintragen/anpassen: const CAMERA_IDS = [ '548730', '1136145', '1136121', '1723473' ]; const OUT_PREFIX = 'blinkVideos'; const FFMPEG = '/usr/bin/ffmpeg'; const FFPROBE = '/usr/bin/ffprobe'; const CACHE_DIR = '/opt/iobroker/iobroker-data/blink-vis-cache'; // Limit pro fertiger Datei, bevor sie als Base64 in einen State geschrieben wird const MAX_MB_MP4 = 50; const MAX_MB_WEBM = 50; // Warten, damit Blink/ioBroker die Datei sicher fertig geschrieben hat const READ_DELAY_MS = 5000; const timers = {}; const running = {}; const rerun = {}; function localId(camera, name) { return `${OUT_PREFIX}.${camera}.${name}`; } function ensureState(id, def, common) { try { createState(id, def, Object.assign({ read: true, write: false }, common)); } catch (e) { log(`createState ${id}: ${e.message || e}`, 'warn'); } } function setLocal(camera, name, value) { try { setState(localId(camera, name), value, true); } catch (e) { log(`setState ${localId(camera, name)}: ${e.message || e}`, 'warn'); } } function ensureCameraStates(camera) { ensureState(localId(camera, 'data_url'), '', { name: `Blink ${camera} MP4 Data URL`, type: 'string', role: 'text' }); ensureState(localId(camera, 'data_url_webm'), '', { name: `Blink ${camera} WebM Data URL`, type: 'string', role: 'text' }); ensureState(localId(camera, 'source_file'), '', { name: `Blink ${camera} Original video.file`, type: 'string', role: 'text' }); ensureState(localId(camera, 'converted_file'), '', { name: `Blink ${camera} konvertierte MP4-Datei`, type: 'string', role: 'text' }); ensureState(localId(camera, 'converted_file_webm'), '', { name: `Blink ${camera} konvertierte WebM-Datei`, type: 'string', role: 'text' }); ensureState(localId(camera, 'original_size'), 0, { name: `Blink ${camera} Originalgröße`, type: 'number', role: 'value', unit: 'Bytes' }); ensureState(localId(camera, 'converted_size'), 0, { name: `Blink ${camera} MP4 Größe`, type: 'number', role: 'value', unit: 'Bytes' }); ensureState(localId(camera, 'converted_size_webm'), 0, { name: `Blink ${camera} WebM Größe`, type: 'number', role: 'value', unit: 'Bytes' }); ensureState(localId(camera, 'data_url_length'), 0, { name: `Blink ${camera} MP4 Data URL Länge`, type: 'number', role: 'value' }); ensureState(localId(camera, 'data_url_webm_length'), 0, { name: `Blink ${camera} WebM Data URL Länge`, type: 'number', role: 'value' }); ensureState(localId(camera, 'updated'), 0, { name: `Blink ${camera} aktualisiert`, type: 'number', role: 'date' }); ensureState(localId(camera, 'ffprobe'), '', { name: `Blink ${camera} ffprobe MP4`, type: 'string', role: 'text' }); ensureState(localId(camera, 'ffprobe_webm'), '', { name: `Blink ${camera} ffprobe WebM`, type: 'string', role: 'text' }); ensureState(localId(camera, 'status'), '', { name: `Blink ${camera} Status`, type: 'string', role: 'text' }); ensureState(localId(camera, 'error'), '', { name: `Blink ${camera} Fehler`, type: 'string', role: 'text' }); } function readStateValue(id) { const s = getState(id); return s && s.val !== null && s.val !== undefined ? String(s.val).trim() : ''; } function finish(camera) { running[camera] = false; if (rerun[camera]) { rerun[camera] = false; scheduleConvert(camera, 3000); } } function fail(camera, message) { setLocal(camera, 'status', 'Fehler'); setLocal(camera, 'error', message); log(`Blink ${camera}: ${message}`, 'warn'); finish(camera); } function waitForStableFile(filePath, callback) { let lastSize = -1; let stableCount = 0; let tries = 0; const timer = setInterval(() => { tries++; fs.stat(filePath, (err, stat) => { if (err) { clearInterval(timer); callback(err); return; } if (stat.size > 0 && stat.size === lastSize) { stableCount++; } else { stableCount = 0; lastSize = stat.size; } if (stableCount >= 2) { clearInterval(timer); callback(null, stat); return; } if (tries >= 25) { clearInterval(timer); callback(new Error(`Dateigröße wurde nicht stabil: ${filePath}`)); } }); }, 1000); } function probeFile(camera, filePath, stateName, callback) { const args = [ '-v', 'error', '-show_streams', '-show_format', filePath ]; execFile(FFPROBE, args, { timeout: 30000 }, (err, stdout, stderr) => { const output = String(stdout || stderr || (err && err.message) || '').slice(0, 12000); setLocal(camera, stateName, output); callback(); }); } function removeIfExists(filePath) { try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } catch (e) { // ignorieren } } function transcodeToMp4(inputFile, outputFile, callback) { const tmpFile = `${outputFile}.tmp.mp4`; removeIfExists(tmpFile); const args = [ '-hide_banner', '-nostdin', '-y', '-fflags', '+genpts', '-err_detect', 'ignore_err', '-i', inputFile, '-map', '0:v:0', '-map', '0:a:0?', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23', '-pix_fmt', 'yuv420p', '-profile:v', 'baseline', '-level', '3.1', '-tag:v', 'avc1', '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', '-c:a', 'aac', '-b:a', '128k', '-ac', '2', '-ar', '44100', '-movflags', '+faststart', '-f', 'mp4', tmpFile ]; execFile(FFMPEG, args, { timeout: 120000 }, (err, stdout, stderr) => { if (err) { callback(new Error(`ffmpeg MP4 Fehler: ${stderr || err.message}`)); return; } try { removeIfExists(outputFile); fs.renameSync(tmpFile, outputFile); } catch (e) { callback(new Error(`MP4 konnte nicht übernommen werden: ${e.message}`)); return; } callback(null); }); } function transcodeToWebm(inputFile, outputFile, callback) { const tmpFile = `${outputFile}.tmp.webm`; removeIfExists(tmpFile); const args = [ '-hide_banner', '-nostdin', '-y', '-fflags', '+genpts', '-err_detect', 'ignore_err', '-i', inputFile, '-map', '0:v:0', '-map', '0:a:0?', // Chrome/Edge-kompatibles WebM '-c:v', 'libvpx', '-deadline', 'realtime', '-cpu-used', '5', '-b:v', '0', '-crf', '32', '-pix_fmt', 'yuv420p', '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', // Audio behalten und nach Opus wandeln '-c:a', 'libopus', '-b:a', '96k', '-ac', '2', '-ar', '48000', '-f', 'webm', tmpFile ]; execFile(FFMPEG, args, { timeout: 180000 }, (err, stdout, stderr) => { if (err) { callback(new Error(`ffmpeg WebM Fehler: ${stderr || err.message}`)); return; } try { removeIfExists(outputFile); fs.renameSync(tmpFile, outputFile); } catch (e) { callback(new Error(`WebM konnte nicht übernommen werden: ${e.message}`)); return; } callback(null); }); } function writeDataUrl(camera, filePath, mimeType, dataState, lengthState, sizeState, maxMb, callback) { fs.stat(filePath, (statErr, stat) => { if (statErr) { callback(new Error(`Datei nicht lesbar: ${statErr.message}`)); return; } const maxBytes = maxMb * 1024 * 1024; if (stat.size > maxBytes) { callback(new Error(`Video zu groß für State: ${stat.size} Bytes, Limit ${maxBytes} Bytes`)); return; } fs.readFile(filePath, (readErr, buf) => { if (readErr) { callback(new Error(`Video konnte nicht gelesen werden: ${readErr.message}`)); return; } const dataUrl = `data:${mimeType};base64,${buf.toString('base64')}`; setLocal(camera, dataState, dataUrl); setLocal(camera, lengthState, dataUrl.length); setLocal(camera, sizeState, stat.size); callback(null); }); }); } function convertVideo(camera) { if (running[camera]) { rerun[camera] = true; return; } running[camera] = true; const sourceState = `${BLINK_INSTANCE}.cameras.${camera}.video.file`; const sourceFile = readStateValue(sourceState); if (!sourceFile) { fail(camera, `${sourceState} ist leer`); return; } setLocal(camera, 'source_file', sourceFile); setLocal(camera, 'data_url', ''); setLocal(camera, 'data_url_webm', ''); setLocal(camera, 'data_url_length', 0); setLocal(camera, 'data_url_webm_length', 0); setLocal(camera, 'error', ''); setLocal(camera, 'status', 'Warte auf stabile Videodatei ...'); setTimeout(() => { waitForStableFile(sourceFile, (stableErr, originalStat) => { if (stableErr) { fail(camera, stableErr.message); return; } setLocal(camera, 'original_size', originalStat.size); try { if (!fs.existsSync(CACHE_DIR)) { fs.mkdirSync(CACHE_DIR, { recursive: true }); } } catch (e) { fail(camera, `Cache-Verzeichnis konnte nicht erstellt werden: ${e.message}`); return; } const mp4File = path.join(CACHE_DIR, `${camera}_browser.mp4`); const webmFile = path.join(CACHE_DIR, `${camera}_browser.webm`); setLocal(camera, 'converted_file', mp4File); setLocal(camera, 'converted_file_webm', webmFile); setLocal(camera, 'status', 'Konvertiere MP4 für Safari ...'); transcodeToMp4(sourceFile, mp4File, (mp4Err) => { if (mp4Err) { fail(camera, mp4Err.message); return; } setLocal(camera, 'status', 'Prüfe MP4 ...'); probeFile(camera, mp4File, 'ffprobe', () => { setLocal(camera, 'status', 'Schreibe MP4 Data URL ...'); writeDataUrl( camera, mp4File, 'video/mp4', 'data_url', 'data_url_length', 'converted_size', MAX_MB_MP4, (mp4WriteErr) => { if (mp4WriteErr) { fail(camera, mp4WriteErr.message); return; } setLocal(camera, 'status', 'Konvertiere WebM für Chrome/Edge ...'); transcodeToWebm(sourceFile, webmFile, (webmErr) => { if (webmErr) { fail(camera, webmErr.message); return; } setLocal(camera, 'status', 'Prüfe WebM ...'); probeFile(camera, webmFile, 'ffprobe_webm', () => { setLocal(camera, 'status', 'Schreibe WebM Data URL ...'); writeDataUrl( camera, webmFile, 'video/webm', 'data_url_webm', 'data_url_webm_length', 'converted_size_webm', MAX_MB_WEBM, (webmWriteErr) => { if (webmWriteErr) { fail(camera, webmWriteErr.message); return; } setLocal(camera, 'updated', Date.now()); setLocal(camera, 'status', 'Fertig'); setLocal(camera, 'error', ''); log(`Blink ${camera}: MP4 + WebM Data-URLs aktualisiert`, 'info'); finish(camera); } ); }); }); } ); }); }); }); }, READ_DELAY_MS); } function scheduleConvert(camera, delayMs) { if (timers[camera]) { clearTimeout(timers[camera]); } timers[camera] = setTimeout(() => { convertVideo(camera); }, delayMs || 3000); } CAMERA_IDS.forEach((camera) => { ensureCameraStates(camera); on({ id: `${BLINK_INSTANCE}.cameras.${camera}.video.file`, change: 'any' }, () => scheduleConvert(camera, 3000)); on({ id: `${BLINK_INSTANCE}.cameras.${camera}.video.timestamp`, change: 'any' }, () => scheduleConvert(camera, 3000)); on({ id: `${BLINK_INSTANCE}.cameras.${camera}.video.ready`, change: 'any' }, () => scheduleConvert(camera, 3000)); // Einmal beim Scriptstart ausführen scheduleConvert(camera, 5000); }); [/s]Ganz herzlichen Dank für deinen Adapter! Und ganz großen Respekt für die Geschwindigkeit, in der du den Adapter entwickelt hast.
Funktioniert auf Anhieb. Sehr ordentliche Objektstruktur.
Batterieanzeige in % gibt die API nicht her?
-
Ganz herzlichen Dank für deinen Adapter! Und ganz großen Respekt für die Geschwindigkeit, in der du den Adapter entwickelt hast.
Funktioniert auf Anhieb. Sehr ordentliche Objektstruktur.
Batterieanzeige in % gibt die API nicht her?
Da bin ich noch dran, da es sich in der Regel aber um Lithium Batterien handelt wird das ggf. nicht so einfach. Deshalb kann im Admin Bereich ein Schwellenwert eingegeben werden. Zwischen 1,2 und 1,1 ist die Batterie fast leer.
-
Da bin ich noch dran, da es sich in der Regel aber um Lithium Batterien handelt wird das ggf. nicht so einfach. Deshalb kann im Admin Bereich ein Schwellenwert eingegeben werden. Zwischen 1,2 und 1,1 ist die Batterie fast leer.
Wir haben fast 40 Kameras und 5 Sync-Module. Auch eine größere Installation scheint deinem Adapter keine Probleme zu bereiten.

-
freut mich sehr ! :-)
-
Wir haben fast 40 Kameras und 5 Sync-Module. Auch eine größere Installation scheint deinem Adapter keine Probleme zu bereiten.

Hast Du auch eine Amazon Doorbell, die funktioniert - die habe ich eben nicht.
-
Hast Du auch eine Amazon Doorbell, die funktioniert - die habe ich eben nicht.
Wie witzig, genau damit habe ich mich gerade beschäftig. Ja, haben wir. Diese konnte über den HAM-Adapter nicht gesteuert werden. Leider mit deinem Adapter auch nicht:
Befehl fehlgeschlagen (blink.0.cameras.******.commands.motion_detect): HTTP 404: {"message":"Camera not found","error":null,"code":500} -
Wie witzig, genau damit habe ich mich gerade beschäftig. Ja, haben wir. Diese konnte über den HAM-Adapter nicht gesteuert werden. Leider mit deinem Adapter auch nicht:
Befehl fehlgeschlagen (blink.0.cameras.******.commands.motion_detect): HTTP 404: {"message":"Camera not found","error":null,"code":500}wird denn das dazugehörige sync-modul korrekt eingebunden ?
-
wird denn das dazugehörige sync-modul korrekt eingebunden ?
-
ich habe zumindest eine Idee: vermutlich wird der Gerätetyp nicht korrekt ausgelesen. Wenn das richtig gemacht wird, dann wird auch die Doorbell erkannt. Ich nehme das einmal mit auf die To-Do Liste - habe aber keine Doorbell zum Testen , dafür musst Du dann herhalten :-)
-
ich habe zumindest eine Idee: vermutlich wird der Gerätetyp nicht korrekt ausgelesen. Wenn das richtig gemacht wird, dann wird auch die Doorbell erkannt. Ich nehme das einmal mit auf die To-Do Liste - habe aber keine Doorbell zum Testen , dafür musst Du dann herhalten :-)
Mache ich selbstverständlich gerne!
-
hab dir etwas per chat gesendet
-
Hallo
Nun bleibt der Adapter bei mir Gelb - die Verbindung scheint mir aber da zu sein, wenn ich das Log richtig verstehe. Hat jemand eine Idee ?Gruss -
blink.0 Zeit silly Nachricht blink.0 2026-04-25 12:41:17.408 silly sendTo "send" to system.adapter.pushover.0 from system.adapter.blink.0 blink.0 2026-04-25 12:41:17.318 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.1097548.commands.fetch_video:{"val":false,"ack":true,"ts":1777113677315,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057429539} blink.0 2026-04-25 12:41:17.270 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.1097548.commands.motion_detect:{"val":true,"ack":true,"ts":1777113677270,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057429574} blink.0 2026-04-25 12:41:17.232 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.928274.commands.fetch_video:{"val":false,"ack":true,"ts":1777113677230,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057429363} blink.0 2026-04-25 12:41:17.186 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.928274.commands.motion_detect:{"val":true,"ack":true,"ts":1777113677186,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057429400} blink.0 2026-04-25 12:41:17.074 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.897864.commands.fetch_video:{"val":false,"ack":true,"ts":1777113677071,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057429177} blink.0 2026-04-25 12:41:17.028 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.897864.commands.motion_detect:{"val":true,"ack":true,"ts":1777113677027,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057429219} blink.0 2026-04-25 12:41:16.934 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.851424.commands.fetch_video:{"val":false,"ack":true,"ts":1777113676931,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057429008} blink.0 2026-04-25 12:41:16.888 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.851424.commands.motion_detect:{"val":true,"ack":true,"ts":1777113676888,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057429037} blink.0 2026-04-25 12:41:16.797 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.786011.commands.fetch_video:{"val":false,"ack":true,"ts":1777113676795,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057428833} blink.0 2026-04-25 12:41:16.752 silly States user redis pmessage blink.0.cameras.*.commands.*/blink.0.cameras.786011.commands.motion_detect:{"val":true,"ack":true,"ts":1777113676751,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057428867} blink.0 2026-04-25 12:41:16.655 silly States user redis pmessage blink.0.sync.*.commands.*/blink.0.sync.548399.commands.armed:{"val":true,"ack":true,"ts":1777113676654,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057428695} blink.0 2026-04-25 12:41:16.586 silly States user redis pmessage blink.0.sync.*.commands.*/blink.0.sync.395317.commands.armed:{"val":true,"ack":true,"ts":1777113676585,"q":0,"from":"system.adapter.blink.0","user":"system.user.admin","lc":1777057428656} blink.0 2026-04-25 12:41:15.241 silly States system redis pmessage system.adapter.blink.0.logLevel/system.adapter.blink.0.logLevel:{"val":"silly","ack":true,"ts":1777113675236,"q":0,"from":"system.adapter.blink.0","lc":1777113573231} blink.0 2026-04-25 12:41:15.228 info starting. Version 0.0.2 (non-npm: Pischleuder1/ioBroker.blink#10cdba4ba984acc72eb546e632d851ddfcc9ea6a) in /opt/iobroker/node_modules/iobroker.blink, node: v22.22.2, js-controller: 7.0.7 -
da sehe ich kein Problem - es startet alles ganz normal. Stell das Log doch einmal auf Info, oder starte den Adapter neu.
-
da sehe ich kein Problem - es startet alles ganz normal. Stell das Log doch einmal auf Info, oder starte den Adapter neu.
Danke, läuft bei mir.

Erkannte Instanzen: 92 (73 aktiv, 19 inaktiv) Plattform: Windows js-controller: 7.1.1 Node.js: v22.22.2 npm: 10.9.7 RAM: ~4157 MB (alle Adapter) CPU: 9.27 % Host: SmartHome Repository: beta -
Mit @ofbeqnpolkkl6mby5e13 arbeite ich gerade daran, dass die Video Doorbell eingebunden werden kann. Motion detect geht wohl schon, auch die Batterieanzeige - jedoch wird die Temperatur noch nicht korrekt ausgelesen. Sobald wir das hinbekommen erscheint ein neues Release.
-
Wer das Widget für die Videos benötigt, bitte das javascript und das html neu aus dem Startpost laden. Hier habe ich Veränderungen eingearbeitet, da der Chrome-Browser Probleme bereitet hatte und Überlagerungen entfernt wurden.
-
da sehe ich kein Problem - es startet alles ganz normal. Stell das Log doch einmal auf Info, oder starte den Adapter neu.
da sehe ich kein Problem - es startet alles ganz normal. Stell das Log doch einmal auf Info, oder starte den Adapter neu.
Ich starte neu und es kommt genau die Ausgabe von oben. Es scheint auch keine Funktion zu geben (ausser dass die Kameras gefunden werden -> Also die Verbindung wohl da ist)
-
stoppe den Adapter einmal, lösche dann Deinen Pin und starte ihn erneut. Dann solltest Du einen neuen Pin erhalten. Diesen wieder eingeben und neu starten.
Die Kameras sind immer da, insofern er einmal den Adapter sauber gestartet hat. Die states werden dann aber nicht aktualisiert.
Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.
Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.
Mit deinem Input könnte dieser Beitrag noch besser werden 💗
Registrieren Anmelden
