Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Tester
  4. ...nicht in offiziellem Repo
  5. Test Adapter für Blink Kameras entwickelt mit KI

NEWS

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    13
    1
    1.5k

  • Neuer ioBroker-Blog online: Monatsrückblick März/April 2026
    BluefoxB
    Bluefox
    8
    1
    2.3k

  • Verwendung von KI bitte immer deutlich kennzeichnen
    HomoranH
    Homoran
    11
    1
    1.1k

Test Adapter für Blink Kameras entwickelt mit KI

Geplant Angeheftet Gesperrt Verschoben ...nicht in offiziellem Repo
167 Beiträge 15 Kommentatoren 3.5k Aufrufe 16 Beobachtet
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • mcm1957M Online
    mcm1957M Online
    mcm1957
    schrieb am zuletzt editiert von
    #149

    Du kannst jederzeit neue Releases erstellen und auf npm deployen. Dazu ist kein Repository notwendig.
    Von direkten OInstallation von NPM wird - außer zu Testzwecken - abgeraten

    Entwicklung u Betreuung: envertech-pv, hoymiles-ms, ns-client, pid, snmp Adapter;
    Support Repositoryverwaltung.

    Wer 'nen Kaffee spendieren will: https://paypal.me

    LESEN - gute Forenbeitrage

    PischleuderP 1 Antwort Letzte Antwort
    0
    • mcm1957M mcm1957

      Du kannst jederzeit neue Releases erstellen und auf npm deployen. Dazu ist kein Repository notwendig.
      Von direkten OInstallation von NPM wird - außer zu Testzwecken - abgeraten

      PischleuderP Online
      PischleuderP Online
      Pischleuder
      schrieb am zuletzt editiert von
      #150

      @mcm1957 : ok verstanden, ich gehe vor wie bisher :-)

      1 Antwort Letzte Antwort
      0
      • M Offline
        M Offline
        McCavity
        schrieb am zuletzt editiert von McCavity
        #151

        Moin,

        ich häng mich hier mal mit rein... ich hatte auch ewig Blink mit Homebridge Adapter im Einsatz und das hat auch für meine Zwecke ausreichend funktioniert: ich möchte nur meine Kameras tageszeitgesteuert (zu Sonnenunter- und -aufgang) aktivieren und deaktivieren, was mit den bescheuerten Zeitplänen in der Blink-App schlichtweg nicht möglich ist). Mit dem Homebridge Adapter war das zwar recht umständlich, aber ich hab's hingekriegt, mit ein bisschen Gefrickel, und es lief.

        Bis ja, keine Ahnung wann. Plötzlich ging die automatische Steuerung nicht mehr und meine Suche wurde irgendwann so kompliziert, daß ich aufgegeben habe - so wichtig war mir das dann auch wieder nicht. Bis ich dann im März begann, mich beruflich mit KI auseinanderzusetzen und dabei deren Potential entdeckte. Mittlerweile habe ich mir sowohl daheim als auch in der Firma einen regelrechten KI-Assistenten eingerichtet und kriege damit Dinge gebacken, von denen ich vorher nur träumen konnte.

        Deshalb habe ich mich am Pfingstwochenende mal daran gemacht, endlich dieses Blink-Problem zu lösen - was mir auch so gut gelungen ist, daß ich voller Stolz das Ergebnis hier präsentiert habe - nur um prompt einen Nasenstüber zu bekommen, daß ich mal wieder zu spät dran bin (wer's lesen möchte: https://forum.iobroker.net/topic/84626/projekt-entwicklung-iobroker.mcp-und-iobroker.blink/ ;-)) Ich bin da aber auch gar nicht böse drum und freue mich, daß es hier eine schon viel ausgereiftere Lösung gibt!

        Für die habe ich heute mal meine alte Dev-Instanz entstaubt (na gut: neu aufgesetzt, die war irgendwann 2024 zuletzt in Betrieb und noch auf Node 18 oder sowas) und den Blink-Adapter installiert - funktioniert perfekt soweit! Für meinen Use Case zwar auch schon wieder Overkill (bei mir läuft in Prod gerade ein simples Python-Script, das sämtliche Funktionalität, die ich wirklich brauche, enthält ;-)), aber so viel Komfortabler. Anmeldeprozess funktionierte reibungslos (bei der ersten Anmeldung war ich ein wenig nervös, weil's ein bisschen gebraucht hat - aber dann wurde der Adapter doch grün) und ich kann ihn nutzen.

        Ich habe auch mal die HTML-Seite ausprobiert und auch die funktioniert ausgezeichnet - sobald man alle Voraussetzungen dafür geschaffen hat. Ich habe den Dev Server aus einem Backup meiner Prod Instanz geclont, daher hatte ich dort erstmal alles disabled, damit sich das nicht gegenseitig ins Brötchen fährt. Wichtig zu wissen: für den Minimalbetrieb braucht es neben der Admin-Adapter-Instanz und einer Instanz des Blink-Adapters natürlich auch Javascript (ich habe da jetzt meine aus Prod importierten Scripte disabled, aber die Instanz selbst und das neu angelegte Blink Script laufen), mich dann aber gewundert, warum ich auf der Seite einen "Verbindungsfehler" angezeigt bekomme - bis ich dann hier im Thread irgendwo entdeckt habe, mal einen Blick Richtung Web-Instanzen zu werfen - und die war bei mir ebenfalls disabled. Mit enabelter Web Instanz funktioniert auch die HTML-Übersicht 1a. Man braucht also:

        • Admin
        • Blink
        • Javascript
        • Web

        im Minimalbetrieb, damit der Adapter vollständig funktioniert, Javascript und Web kann man weglassen, wenn man die HTML Seite nicht verwendet.

        Jetzt hatte ich am Schluß nur noch eine Verständnisfrage: sehe ich das richtig, daß sich die HTML-Seiten ausschließlich auf Videos stützen? Die im Objektbaum hinterlegten Snapshots werden nicht benutzt? Das war so ein bisschen meine Hoffnung gewesen, daß ich in der Übersicht (/grid) die letzten Snapshots sehe und dann bei Bedarf die Videos abrufen kann - oder habe ich da noch etwas übersehen?

        Ich werde das hier jedenfalls mit großem Interesse beobachten und hoffe, daß der Adapter in Bälde im IOBroker Repo verfügbar werden wird. Wenn ich irgendwas beitragen kann, gerne auch immer Bescheid sagen.

        LG,
        McCavity

        1 Antwort Letzte Antwort
        0
        • PischleuderP Online
          PischleuderP Online
          Pischleuder
          schrieb am zuletzt editiert von Pischleuder
          #152

          Die snapshots (Einzelbilder) werden lediglich aus den Datenpunkten bei der/den einzelnen Kameras ausgelöst, so als würdest Du in der App auf "Miniaturansicht aktualiseren" klicken. Andersherum bedeutet das auch, dass, wenn Du unter commands diesen fetch auslöst, sich das Bild in der App verändert. Ich persönlich benötige das nicht, weshalb ich Schwerpunkt auf die Video Historie (10 Slots je Kamera) gelegt habe und mittlerweile zusätzlich einen live view lauffähig habe (siehe letztes Bild). Dieser ist in der aktuellen Version jedoch noch nicht integriert. Also ja HTML bezieht sich nur auf Videos.

          1 Antwort Letzte Antwort
          1
          • M Offline
            M Offline
            McCavity
            schrieb am zuletzt editiert von
            #153

            Ah, verstehe, vielen Dank! Heißt also, daß die Snapshotfunktion sich tatsächlich auf die gesamte Blink-Umgebung auswirkt und demnach die "Livebild per Snapshot aktivieren" und "Livebild Intervall (Sek.)" Funktionen dafür sorgen, daß in diesem Intervall das Miniaturbild auch in der App erneuert wird. Ich hab's gerade in der App verifiziert und stimmt: alle Miniaturbilder wurden innerhalb der letzten Stunde erneuert... nachdem ich das zuletzt vor über einem Jahr manuell getan hatte (normalerweise nur einmal bei Einrichtung der Kamera oder wenn ich mal eine umsetze). Aber gut zu wissen, daß sich das auf die App auswirkt... das brauche ich nämlich eigentlich auch nicht unbedingt.

            1 Antwort Letzte Antwort
            0
            • PischleuderP Online
              PischleuderP Online
              Pischleuder
              schrieb am zuletzt editiert von
              #154

              genau und der Tab "Streaming" fällt demnächst auch raus, weil dieser noch als Funktion zur Verfügung stehen sollte, bevor ein "live stream" möglich ist - nämlich eine Bildfolge als Quasi-Stream zur Verfügung zu stellen. Ich würde das zunächst immer deaktiviert lassen.

              1 Antwort Letzte Antwort
              1
              • PischleuderP Online
                PischleuderP Online
                Pischleuder
                schrieb am zuletzt editiert von
                #155

                Guten Morgen,
                ein großer Dank geht an @ofbeqnpolkkl6mby5e13 für das gemeinsame Testen der alpha-Version (nun 0.0.15-alpha1), bei der die Live-View Integration bisher gut funktioniert hat. Diese ist selbstverständlich für ein Produktivsystem nicht unbedingt geeignet und sollte wenn, auf einem Testsystem installiert werden. Bin jetzt erst einmal zwei Wochen im Urlaub und werde dann voraussichtlich ein neues Release erstellen.

                PischleuderP 1 Antwort Letzte Antwort
                0
                • PischleuderP Pischleuder

                  Guten Morgen,
                  ein großer Dank geht an @ofbeqnpolkkl6mby5e13 für das gemeinsame Testen der alpha-Version (nun 0.0.15-alpha1), bei der die Live-View Integration bisher gut funktioniert hat. Diese ist selbstverständlich für ein Produktivsystem nicht unbedingt geeignet und sollte wenn, auf einem Testsystem installiert werden. Bin jetzt erst einmal zwei Wochen im Urlaub und werde dann voraussichtlich ein neues Release erstellen.

                  PischleuderP Online
                  PischleuderP Online
                  Pischleuder
                  schrieb am zuletzt editiert von Pischleuder
                  #156

                  Platzhalter

                  1 Antwort Letzte Antwort
                  0
                  • PischleuderP Pischleuder
                    Aktuelle Testversion 0.0.14
                    Github Link https://github.com/Pischleuder1/ioBroker.blink
                    NPM Link https://www.npmjs.com/package/iobroker.blink
                    Veröffentlichungsdatum 29.05.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.
                    c49e2058-65ac-4a20-ad62-e068827483e1-image.jpeg
                    3801838a-66ff-4fc0-be85-e12b886d227a-image.jpeg

                    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 (dafür muss das javascript eingebunden sein)
                    • Speicherort kann im Admin Bereich festgelegt werden
                    • Video Doorbell wird unterstützt, jedoch keine Temperaturanzeige, da diese offensichtlich bei der Doorbell nicht verbaut ist
                    • unterstützt "Smart Detection states" für bestimmte Bewegungsvorgänge wie Paket, Tier etc. (funktioniert nur bei Abo-Modell)
                    • unterstützt Cloud und Lokal gespeicherte Videos (SyncModule 2 und XR) via Server auf port 8085 - JavaScript benötigt, siehe unten !

                    Was funktioniert (noch) nicht:
                    • kein Echtzeit Video (28.05.: läuft bereits in der alpha Version)

                    Visualisierung
                    • ffmpeg muss installiert sein
                    • Javascript adapter
                    • Web-Adapter
                    • Es werden auf einem Pi4 relativ große Ressourcen benötigt, 4 GB und mehr empfohlen
                    • dieses Javascript einbinden:

                    // ============================================================
                    // Blink Multi-Camera Server + Widget
                    //   http://<host>:8085/                  → Single + 10-Slot History darunter
                    //   http://<host>:8085/?camera=548730    → Single für eine fixe Kamera
                    //   http://<host>:8085/grid              → Alle Kameras im Grid (mit History-Blättern)
                    //   http://<host>:8085/history?camera=ID → Reine History-Ansicht, 10 Slots in Reihe
                    //   http://<host>:8085/blink/<file>      → Video-Datei
                    //   http://<host>:8085/cameras           → JSON mit allen Kameras (inkl. History-Datenpunkten)
                    // ============================================================
                    
                    const http = require('http');
                    const fs   = require('fs');
                    const path = require('path');
                    
                    // ============= KONFIGURATION =============
                    const PORT          = 8085;
                    const ROOT_DIR      = '/opt/iobroker/iobroker-data/blink';
                    const VIDEO_BASE    = '/blink/';
                    const CAMERA_PREFIX = 'blink.0.cameras.';
                    const VIDEO_STATE   = '.video.file';
                    const NAME_STATE    = '.info.name';
                    const TS_STATE      = '.video.timestamp';
                    const READY_STATE   = '.video.ready';
                    const ERROR_STATE   = '.video.lastError';
                    const HISTORY_SIZE  = 10;
                    const IOBROKER_PORT = 8082;
                    // =========================================
                    
                    if (typeof globalThis.__blinkServer !== 'undefined') {
                       try { globalThis.__blinkServer.close(); log('Vorherigen Blink-Server gestoppt'); }
                       catch (e) { /* ignore */ }
                    }
                    
                    // ---------- Kameras automatisch entdecken ----------
                    function discoverCameras() {
                       return new Promise((resolve) => {
                           const cams = [];
                           const seen = new Set();
                           $(`state[id=${CAMERA_PREFIX}*${NAME_STATE}]`).each((id) => {
                               const rest = id.slice(CAMERA_PREFIX.length);
                               const camId = rest.split('.')[0];
                               if (!seen.has(camId)) {
                                   seen.add(camId);
                                   const history = [];
                                   for (let i = 0; i < HISTORY_SIZE; i++) {
                                       history.push({
                                           slot: i,
                                           file_datapoint:      `${CAMERA_PREFIX}${camId}.video.history.${i}.file`,
                                           timestamp_datapoint: `${CAMERA_PREFIX}${camId}.video.history.${i}.timestamp`,
                                           id_datapoint:        `${CAMERA_PREFIX}${camId}.video.history.${i}.id`,
                                           source_datapoint:    `${CAMERA_PREFIX}${camId}.video.history.${i}.source`
                                       });
                                   }
                                   cams.push({
                                       id: camId,
                                       datapoint:       CAMERA_PREFIX + camId + VIDEO_STATE,
                                       ts_datapoint:    CAMERA_PREFIX + camId + TS_STATE,
                                       ready_datapoint: CAMERA_PREFIX + camId + READY_STATE,
                                       error_datapoint: CAMERA_PREFIX + camId + ERROR_STATE,
                                       history: history,
                                       name: null
                                   });
                               }
                           });
                           const promises = cams.map(c => new Promise((res) => {
                               const nameDp = CAMERA_PREFIX + c.id + NAME_STATE;
                               getState(nameDp, (err, st) => {
                                   if (!err && st && st.val) c.name = String(st.val).trim();
                                   res();
                               });
                           }));
                           Promise.all(promises).then(() => resolve(cams.sort((a, b) =>
                               (a.name || a.id).localeCompare(b.name || b.id)
                           )));
                       });
                    }
                    
                    // ============================================================
                    // Gemeinsamer JS-Helper-Code
                    // ============================================================
                    const COMMON_JS = `
                    const VIDEO_PREFIX = location.protocol + '//' + location.hostname + ':__VIDEO_PORT__' + '__VIDEO_BASE__';
                    const IOBROKER_URL = location.protocol + '//' + location.hostname + ':__IOBROKER_PORT__';
                    const HISTORY_SIZE = __HISTORY_SIZE__;
                    
                    function buildUrl(v, ts) {
                     if (!v) return null;
                     if (/^https?:\\/\\//.test(v)) return v;
                     const fn = encodeURIComponent(String(v).split('/').pop());
                     return VIDEO_PREFIX + fn + '?t=' + (ts || Date.now());
                    }
                    function tsFromName(n) {
                     const m = n && n.match(/(\\d{4}-\\d{2}-\\d{2})T(\\d{2})-(\\d{2})-(\\d{2})/);
                     return m ? \`\${m[1]} \${m[2]}:\${m[3]}:\${m[4]}\` : null;
                    }
                    function formatTs(isoOrMs) {
                     if (!isoOrMs) return '';
                     const d = new Date(isoOrMs);
                     if (isNaN(d.getTime())) return '';
                     return d.toLocaleString('de-DE', {
                       day: '2-digit', month: '2-digit', year: 'numeric',
                       hour: '2-digit', minute: '2-digit', second: '2-digit'
                     });
                    }
                    function relativeTime(ms) {
                     if (!ms) return '';
                     const diff = Math.max(0, Date.now() - (typeof ms === 'string' ? new Date(ms).getTime() : ms));
                     const sec = Math.floor(diff / 1000);
                     if (sec < 60) return 'gerade eben';
                     const min = Math.floor(sec / 60);
                     if (min < 60) return 'vor ' + min + ' Min';
                     const h = Math.floor(min / 60);
                     if (h < 24) return 'vor ' + h + ' Std';
                     const d = Math.floor(h / 24);
                     if (d < 30) return 'vor ' + d + ' Tag' + (d===1?'':'en');
                     return new Date(ms).toLocaleDateString('de-DE');
                    }
                    
                    function isVideoValid(value, ready, lastError) {
                     if (!value) return false;
                     if (lastError && String(lastError).trim() !== '' && String(lastError).toLowerCase() !== 'null') return false;
                     if (ready === false) return false;
                     return true;
                    }
                    `;
                    
                    // ============================================================
                    // Widget 1: Single + History-Streifen darunter
                    // ============================================================
                    const WIDGET_HTML = `<!DOCTYPE html>
                    <html lang="de">
                    <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>Blink Video Player</title>
                    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
                    <style>
                    * { box-sizing: border-box; margin: 0; padding: 0; }
                    body { background:#1a1a1a; color:#eee; font-family:-apple-system,system-ui,sans-serif;
                          min-height:100vh; display:flex; flex-direction:column; align-items:center; padding:16px; }
                    .container { width:100%; max-width:900px; background:#2a2a2a; border-radius:12px;
                                overflow:hidden; box-shadow:0 4px 12px rgba(0,0,0,0.4); }
                    .header { padding:12px 16px; background:#333; display:flex; justify-content:space-between;
                             align-items:center; gap:12px; border-bottom:1px solid #444; flex-wrap:wrap; }
                    .title { font-size:14px; font-weight:600; flex:1; min-width:0;
                            overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
                    .status { font-size:11px; padding:3px 8px; border-radius:10px; background:#555; flex-shrink:0; }
                    .status.ok { background:#2d6a3e; }
                    .status.err { background:#8b2d2d; }
                    select { background:#444; color:#eee; border:1px solid #555; padding:5px 8px;
                            border-radius:6px; font-size:12px; max-width:200px; }
                    video { width:100%; display:block; background:#000; max-height:600px; }
                    .info { padding:12px 16px; font-size:12px; color:#999; word-break:break-all;
                           border-top:1px solid #444; }
                    .info .ts { color:#ccc; font-weight:500; margin-bottom:4px; }
                    .info .err { color:#e88; font-weight:500; }
                    .info .source { display:inline-block; padding:2px 6px; border-radius:4px;
                                   font-size:10px; background:#444; margin-left:6px; vertical-align:middle; }
                    .info .source.cloud { background:#2d4a6a; }
                    .info .source.local_storage { background:#4a3a2d; }
                    .empty { padding:60px 20px; text-align:center; color:#777; }
                    .empty .err-msg { color:#e88; font-size:13px; margin-top:8px; }
                    .controls { padding:8px 16px; border-top:1px solid #444; display:flex; gap:8px;
                               justify-content:space-between; align-items:center; }
                    .relative-ts { font-size:12px; color:#aaa; }
                    button { background:#444; color:#eee; border:none; padding:6px 12px; border-radius:6px;
                            font-size:12px; cursor:pointer; }
                    button:hover { background:#555; }
                    
                    /* History-Streifen */
                    .history-section { border-top:1px solid #444; padding:12px 16px; background:#252525; }
                    .history-section h3 { font-size:12px; color:#999; margin-bottom:8px; font-weight:500; }
                    .history-strip { display:flex; gap:6px; overflow-x:auto; padding-bottom:4px; }
                    .history-strip::-webkit-scrollbar { height:6px; }
                    .history-strip::-webkit-scrollbar-thumb { background:#555; border-radius:3px; }
                    .hslot { flex:0 0 130px; background:#1a1a1a; border-radius:6px; cursor:pointer;
                            border:2px solid transparent; transition:border-color 0.15s, transform 0.15s; }
                    .hslot:hover { border-color:#666; transform:translateY(-2px); }
                    .hslot.active { border-color:#4a8; }
                    .hslot.empty { opacity:0.3; cursor:not-allowed; }
                    .hslot-thumb { position:relative; aspect-ratio:16/9; background:#000; border-radius:4px 4px 0 0;
                                  overflow:hidden; }
                    .hslot-thumb video { width:100%; height:100%; object-fit:cover; pointer-events:none; }
                    .hslot-thumb .badge { position:absolute; top:4px; left:4px; background:rgba(0,0,0,0.7);
                                        color:white; padding:1px 5px; border-radius:3px; font-size:10px; }
                    .hslot-info { padding:4px 6px; font-size:10px; color:#aaa; line-height:1.3; }
                    .hslot-info .time { color:#ddd; font-weight:500; }
                    .hslot-info .src { font-size:9px; color:#888; }
                    </style>
                    </head>
                    <body>
                    <div class="container">
                     <div class="header">
                       <span class="title" id="title">📹 Blink</span>
                       <select id="picker" style="display:none"></select>
                       <span class="status" id="status">Verbinde…</span>
                     </div>
                     <div id="player"><div class="empty">Lade Kameras…</div></div>
                     <div class="info" id="info"></div>
                     <div class="controls">
                       <span class="relative-ts" id="reltime"></span>
                       <button id="reload">🔄 Neu laden</button>
                     </div>
                    
                     <div class="history-section" id="history-section" style="display:none">
                       <h3>📚 Verlauf (10 neueste)</h3>
                       <div class="history-strip" id="history-strip"></div>
                     </div>
                    </div>
                    <script>
                    __COMMON_JS__
                    
                    const params  = new URLSearchParams(location.search);
                    const fixedCamera = params.get('camera');
                    const $title  = document.getElementById('title');
                    const $status = document.getElementById('status');
                    const $player = document.getElementById('player');
                    const $info   = document.getElementById('info');
                    const $reload = document.getElementById('reload');
                    const $picker = document.getElementById('picker');
                    const $reltime = document.getElementById('reltime');
                    const $histSection = document.getElementById('history-section');
                    const $histStrip = document.getElementById('history-strip');
                    
                    let socket, currentCam = null, cameras = [];
                    let curValue = null, curTs = null, curReady = null, curError = null;
                    // History-State pro Kamera-Wechsel neu aufgebaut
                    let histSlots = []; // [{file, timestamp, id, source}, ...]
                    let manualPick = null; // null = aktuellen Live-Clip zeigen, sonst Slot-Index
                    
                    function setStatus(t, c) { $status.textContent = t; $status.className = 'status' + (c?' '+c:''); }
                    function updateRelativeTime() { $reltime.textContent = curTs ? relativeTime(curTs) : ''; }
                    
                    function renderEmpty(msg, errMsg) {
                     let html = '<div class="empty">' + msg;
                     if (errMsg) html += '<div class="err-msg">⚠ ' + errMsg + '</div>';
                     html += '</div>';
                     $player.innerHTML = html;
                     $info.textContent = '';
                    }
                    
                    function renderCurrent() {
                     updateRelativeTime();
                    
                     // Wenn manuell ein History-Slot gewählt, den dort gezeigten Clip verwenden
                     let showFile, showTs, showSource, showInfo;
                     if (manualPick !== null && histSlots[manualPick] && histSlots[manualPick].file) {
                       const s = histSlots[manualPick];
                       showFile = s.file;
                       showTs = s.timestamp;
                       showSource = s.source;
                       showInfo = 'Slot ' + manualPick;
                     } else if (isVideoValid(curValue, curReady, curError)) {
                       showFile = curValue;
                       showTs = curTs;
                       showSource = null;
                       showInfo = 'Live';
                     } else {
                       const errText = curError && String(curError).trim() && String(curError).toLowerCase() !== 'null'
                         ? String(curError) : null;
                       renderEmpty('Kein aktuelles Video', errText);
                       refreshHistoryStrip();
                       return;
                     }
                    
                     const url = buildUrl(showFile, showTs);
                     if (!url) { renderEmpty('Kein Video verfügbar'); return; }
                    
                     const fn = String(showFile).split('/').pop();
                    
                     $player.innerHTML = '';
                     const video = document.createElement('video');
                     video.controls = true;
                     video.autoplay = true;
                     video.muted = true;
                     video.playsInline = true;
                     const source = document.createElement('source');
                     source.setAttribute('src', url);
                     source.setAttribute('type', 'video/mp4');
                     video.appendChild(source);
                     $player.appendChild(video);
                     video.load();
                    
                     const ts = formatTs(showTs);
                     let infoHtml = '<div class="ts">🕒 ' + (ts || '—') + ' · ' + showInfo;
                     if (showSource) {
                       infoHtml += '<span class="source ' + showSource + '">' + showSource + '</span>';
                     }
                     infoHtml += '</div><div>' + fn + '</div>';
                     $info.innerHTML = infoHtml;
                    
                     refreshHistoryStrip();
                    }
                    
                    function refreshHistoryStrip() {
                     $histSection.style.display = '';
                     $histStrip.innerHTML = '';
                     for (let i = 0; i < HISTORY_SIZE; i++) {
                       const s = histSlots[i] || {};
                       const slot = document.createElement('div');
                       slot.className = 'hslot' + (s.file ? '' : ' empty') + (manualPick === i ? ' active' : '');
                       slot.dataset.slot = i;
                    
                       const thumb = document.createElement('div');
                       thumb.className = 'hslot-thumb';
                       const badge = document.createElement('div');
                       badge.className = 'badge';
                       badge.textContent = '#' + i;
                       thumb.appendChild(badge);
                    
                       if (s.file) {
                         const v = document.createElement('video');
                         v.preload = 'metadata';
                         v.muted = true;
                         const sourceEl = document.createElement('source');
                         sourceEl.setAttribute('src', buildUrl(s.file, s.timestamp));
                         sourceEl.setAttribute('type', 'video/mp4');
                         v.appendChild(sourceEl);
                         thumb.appendChild(v);
                       }
                    
                       const inf = document.createElement('div');
                       inf.className = 'hslot-info';
                       const t = document.createElement('div');
                       t.className = 'time';
                       t.textContent = s.timestamp ? formatTs(s.timestamp).replace(/, \\d{4}/, '') : '—';
                       const src = document.createElement('div');
                       src.className = 'src';
                       src.textContent = s.source || (s.file ? '' : 'leer');
                       inf.appendChild(t);
                       inf.appendChild(src);
                    
                       slot.appendChild(thumb);
                       slot.appendChild(inf);
                    
                       if (s.file) {
                         slot.addEventListener('click', () => {
                           manualPick = (manualPick === i) ? null : i;
                           renderCurrent();
                         });
                       }
                       $histStrip.appendChild(slot);
                     }
                    }
                    
                    $reload.addEventListener('click', () => { manualPick = null; renderCurrent(); });
                    setInterval(updateRelativeTime, 30000);
                    
                    function unsubscribeCurrent() {
                     if (!currentCam || !socket) return;
                     socket.emit('unsubscribe', currentCam.datapoint);
                     socket.emit('unsubscribe', currentCam.ts_datapoint);
                     socket.emit('unsubscribe', currentCam.ready_datapoint);
                     socket.emit('unsubscribe', currentCam.error_datapoint);
                     currentCam.history.forEach(h => {
                       socket.emit('unsubscribe', h.file_datapoint);
                       socket.emit('unsubscribe', h.timestamp_datapoint);
                       socket.emit('unsubscribe', h.id_datapoint);
                       socket.emit('unsubscribe', h.source_datapoint);
                     });
                    }
                    
                    function switchCamera(cam) {
                     unsubscribeCurrent();
                     currentCam = cam;
                     manualPick = null;
                     histSlots = new Array(HISTORY_SIZE).fill(null).map(() => ({}));
                     $title.textContent = '📹 ' + (cam.name || 'Kamera ' + cam.id);
                     $player.innerHTML = '<div class="empty">Lade Video…</div>';
                     $info.textContent = '';
                     curValue = null; curTs = null; curReady = null; curError = null;
                     updateRelativeTime();
                    
                     // Live-States: 4 parallel
                     let pending = 4 + 4 * HISTORY_SIZE;
                     const done = () => { if (--pending === 0) renderCurrent(); };
                    
                     socket.emit('getState', cam.ts_datapoint, (e, st) => {
                       if (st && st.val) curTs = st.val;
                       done();
                     });
                     socket.emit('getState', cam.ready_datapoint, (e, st) => {
                       if (st) curReady = st.val;
                       done();
                     });
                     socket.emit('getState', cam.error_datapoint, (e, st) => {
                       if (st) curError = st.val;
                       done();
                     });
                     socket.emit('getState', cam.datapoint, (e, st) => {
                       if (st) curValue = st.val;
                       done();
                     });
                    
                     socket.emit('subscribe', cam.datapoint);
                     socket.emit('subscribe', cam.ts_datapoint);
                     socket.emit('subscribe', cam.ready_datapoint);
                     socket.emit('subscribe', cam.error_datapoint);
                    
                     // History-States: 10 Slots × 4 Felder
                     cam.history.forEach((h, idx) => {
                       socket.emit('getState', h.file_datapoint, (e, st) => {
                         histSlots[idx].file = st ? st.val : null; done();
                       });
                       socket.emit('getState', h.timestamp_datapoint, (e, st) => {
                         histSlots[idx].timestamp = st ? st.val : null; done();
                       });
                       socket.emit('getState', h.id_datapoint, (e, st) => {
                         histSlots[idx].id = st ? st.val : null; done();
                       });
                       socket.emit('getState', h.source_datapoint, (e, st) => {
                         histSlots[idx].source = st ? st.val : null; done();
                       });
                       socket.emit('subscribe', h.file_datapoint);
                       socket.emit('subscribe', h.timestamp_datapoint);
                       socket.emit('subscribe', h.id_datapoint);
                       socket.emit('subscribe', h.source_datapoint);
                     });
                    }
                    
                    fetch('/cameras').then(r => r.json()).then(list => {
                     cameras = list;
                     if (!cameras.length) {
                       setStatus('Keine Kameras', 'err');
                       $player.innerHTML = '<div class="empty">Keine Kameras gefunden</div>';
                       return;
                     }
                     if (fixedCamera) {
                       const cam = cameras.find(c => c.id === fixedCamera);
                       if (!cam) {
                         setStatus('Unbekannt', 'err');
                         $player.innerHTML = '<div class="empty">Kamera ' + fixedCamera + ' nicht gefunden</div>';
                         return;
                       }
                       connectAndStart(cam);
                     } else {
                       $picker.style.display = '';
                       cameras.forEach(c => {
                         const o = document.createElement('option');
                         o.value = c.id;
                         o.textContent = c.name || ('Kamera ' + c.id);
                         $picker.appendChild(o);
                       });
                       $picker.addEventListener('change', () => {
                         const cam = cameras.find(c => c.id === $picker.value);
                         if (cam) switchCamera(cam);
                       });
                       connectAndStart(cameras[0]);
                     }
                    }).catch(e => { setStatus('Server-Fehler', 'err'); console.error(e); });
                    
                    function findHistorySlot(id) {
                     if (!currentCam) return -1;
                     return currentCam.history.findIndex(h =>
                       id === h.file_datapoint || id === h.timestamp_datapoint ||
                       id === h.id_datapoint || id === h.source_datapoint);
                    }
                    
                    function connectAndStart(cam) {
                     if (typeof io === 'undefined') { setStatus('Socket.IO Lib fehlt', 'err'); return; }
                     socket = io(IOBROKER_URL, { transports: ['websocket', 'polling'] });
                     socket.on('connect', () => { setStatus('Verbunden', 'ok'); switchCamera(cam); });
                     socket.on('disconnect',    () => setStatus('Getrennt', 'err'));
                     socket.on('connect_error', (e) => { setStatus('Verbindungsfehler', 'err'); console.error(e); });
                     socket.on('stateChange', (id, state) => {
                       if (!state || !currentCam) return;
                       if (id === currentCam.datapoint)        { curValue = state.val; renderCurrent(); return; }
                       if (id === currentCam.ts_datapoint)     { if (state.val) curTs = state.val; renderCurrent(); return; }
                       if (id === currentCam.ready_datapoint)  { curReady = state.val; renderCurrent(); return; }
                       if (id === currentCam.error_datapoint)  { curError = state.val; renderCurrent(); return; }
                    
                       const slot = findHistorySlot(id);
                       if (slot >= 0) {
                         const h = currentCam.history[slot];
                         if (id === h.file_datapoint)      histSlots[slot].file = state.val;
                         else if (id === h.timestamp_datapoint) histSlots[slot].timestamp = state.val;
                         else if (id === h.id_datapoint)        histSlots[slot].id = state.val;
                         else if (id === h.source_datapoint)    histSlots[slot].source = state.val;
                         refreshHistoryStrip();
                       }
                     });
                    }
                    </script>
                    </body>
                    </html>`;
                    
                    // ============================================================
                    // Widget 2: Grid (alle Kameras, History per Klick durchblättern)
                    // ============================================================
                    const GRID_HTML = `<!DOCTYPE html>
                    <html lang="de">
                    <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>Blink Cameras</title>
                    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
                    <style>
                    * { box-sizing: border-box; margin: 0; padding: 0; }
                    body { background:#1a1a1a; color:#eee; font-family:-apple-system,system-ui,sans-serif;
                          min-height:100vh; padding:12px; }
                    .topbar { display:flex; justify-content:space-between; align-items:center;
                             padding:0 4px 12px; gap:12px; }
                    .topbar .title { font-size:14px; font-weight:600; }
                    .status { font-size:11px; padding:3px 8px; border-radius:10px; background:#555; }
                    .status.ok { background:#2d6a3e; }
                    .status.err { background:#8b2d2d; }
                    .grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));
                           gap:12px; }
                    .cam { background:#2a2a2a; border-radius:10px; overflow:hidden;
                          box-shadow:0 2px 6px rgba(0,0,0,0.3); display:flex; flex-direction:column; }
                    .cam-head { padding:8px 12px; background:#333; display:flex;
                               justify-content:space-between; align-items:center; gap:8px; }
                    .cam-name { font-size:13px; font-weight:600; flex:1; min-width:0;
                               overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
                    .cam-time { font-size:11px; color:#aaa; flex-shrink:0; }
                    .cam-content { display:block; position:relative; }
                    .cam-video-wrap { position:relative; background:#000; aspect-ratio:16/9; cursor:pointer; }
                    .cam-video-wrap video { width:100%; height:100%; display:block; object-fit:cover; }
                    .cam-video-wrap .overlay { position:absolute; inset:0; display:flex;
                                             align-items:center; justify-content:center; pointer-events:none;
                                             background:rgba(0,0,0,0.25); transition:opacity 0.2s; }
                    .cam-video-wrap.playing .overlay { opacity:0; }
                    .cam-video-wrap .play-btn {
                     width:56px; height:56px; border-radius:50%;
                     background:rgba(0,0,0,0.6); border:2px solid rgba(255,255,255,0.8);
                     display:flex; align-items:center; justify-content:center;
                    }
                    .cam-video-wrap .play-btn::after {
                     content:''; width:0; height:0; margin-left:4px;
                     border-top:10px solid transparent; border-bottom:10px solid transparent;
                     border-left:16px solid white;
                    }
                    .cam-video-wrap .slot-badge {
                     position:absolute; top:6px; left:6px;
                     background:rgba(0,0,0,0.7); color:white;
                     padding:2px 8px; border-radius:4px; font-size:11px; font-weight:500;
                     pointer-events:none;
                    }
                    .cam-empty { aspect-ratio:16/9; display:flex; flex-direction:column;
                                align-items:center; justify-content:center; gap:6px;
                                color:#888; font-size:13px; background:#1a1a1a; padding:8px; text-align:center; }
                    .cam-empty .err-msg { color:#e88; font-size:11px; }
                    
                    /* History-Navigation */
                    .cam-nav { display:flex; align-items:center; justify-content:space-between;
                              padding:6px 8px; background:#222; gap:6px; }
                    .cam-nav button { background:#444; color:#eee; border:none;
                                     padding:4px 10px; border-radius:4px; font-size:11px; cursor:pointer; }
                    .cam-nav button:hover { background:#555; }
                    .cam-nav button:disabled { opacity:0.4; cursor:not-allowed; }
                    .cam-nav .label { font-size:11px; color:#bbb; flex:1; text-align:center;
                                     overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
                    .cam-nav .label .source { font-size:9px; padding:1px 4px; border-radius:3px;
                                             background:#444; margin-left:4px; vertical-align:middle; }
                    .cam-nav .label .source.cloud { background:#2d4a6a; }
                    .cam-nav .label .source.local_storage { background:#4a3a2d; }
                    </style>
                    </head>
                    <body>
                    <div class="topbar">
                     <span class="title">📹 Blink Kameras</span>
                     <span class="status" id="status">Verbinde…</span>
                    </div>
                    <div class="grid" id="grid"></div>
                    <script>
                    __COMMON_JS__
                    
                    const $status = document.getElementById('status');
                    const $grid   = document.getElementById('grid');
                    
                    let socket;
                    const cards = {};   // camId → {value, ts, ready, error, hist[], pickIdx, root, contentHost, ...}
                    
                    function setStatus(t, c) { $status.textContent = t; $status.className = 'status' + (c?' '+c:''); }
                    
                    function buildCard(cam) {
                     const card = document.createElement('div');
                     card.className = 'cam';
                     card.dataset.cam = cam.id;
                    
                     const head = document.createElement('div');
                     head.className = 'cam-head';
                     const nameSpan = document.createElement('span');
                     nameSpan.className = 'cam-name';
                     nameSpan.textContent = cam.name || ('Kamera ' + cam.id);
                     const timeSpan = document.createElement('span');
                     timeSpan.className = 'cam-time';
                     head.appendChild(nameSpan);
                     head.appendChild(timeSpan);
                    
                     const contentHost = document.createElement('div');
                     contentHost.className = 'cam-content';
                    
                     const empty = document.createElement('div');
                     empty.className = 'cam-empty';
                     empty.textContent = 'Lade…';
                     contentHost.appendChild(empty);
                    
                     // History-Navigation
                     const nav = document.createElement('div');
                     nav.className = 'cam-nav';
                     const prevBtn = document.createElement('button');
                     prevBtn.textContent = '◀';
                     prevBtn.title = 'Älterer Clip';
                     const label = document.createElement('div');
                     label.className = 'label';
                     label.textContent = 'Live';
                     const nextBtn = document.createElement('button');
                     nextBtn.textContent = '▶';
                     nextBtn.title = 'Neuerer Clip';
                     const liveBtn = document.createElement('button');
                     liveBtn.textContent = '⏺ Live';
                     liveBtn.title = 'Live-Ansicht';
                     nav.appendChild(prevBtn);
                     nav.appendChild(label);
                     nav.appendChild(liveBtn);
                     nav.appendChild(nextBtn);
                    
                     card.appendChild(head);
                     card.appendChild(contentHost);
                     card.appendChild(nav);
                     $grid.appendChild(card);
                    
                     cards[cam.id] = {
                       value: null, ts: null, ready: null, error: null,
                       hist: new Array(HISTORY_SIZE).fill(null).map(() => ({})),
                       pickIdx: null,  // null = Live, 0..9 = History-Slot
                       root: card, contentHost: contentHost, timeEl: timeSpan,
                       prevBtn, nextBtn, liveBtn, label,
                       datapoint:       cam.datapoint,
                       ts_datapoint:    cam.ts_datapoint,
                       ready_datapoint: cam.ready_datapoint,
                       error_datapoint: cam.error_datapoint,
                       history:         cam.history,
                       name: cam.name || ('Kamera ' + cam.id)
                     };
                    
                     prevBtn.addEventListener('click', () => navigate(cam.id, +1));
                     nextBtn.addEventListener('click', () => navigate(cam.id, -1));
                     liveBtn.addEventListener('click', () => { cards[cam.id].pickIdx = null; renderCard(cam.id); });
                    }
                    
                    function navigate(camId, delta) {
                     const c = cards[camId];
                     if (!c) return;
                     let next = c.pickIdx == null ? 0 : c.pickIdx + delta;
                     if (next < 0) { c.pickIdx = null; renderCard(camId); return; }
                     if (next >= HISTORY_SIZE) next = HISTORY_SIZE - 1;
                     // Suche nächsten Slot mit Datei
                     while (next >= 0 && next < HISTORY_SIZE && !c.hist[next].file) next += delta;
                     if (next >= 0 && next < HISTORY_SIZE) {
                       c.pickIdx = next;
                     } else {
                       c.pickIdx = null;
                     }
                     renderCard(camId);
                    }
                    
                    function setEmpty(c, text, errMsg) {
                     while (c.contentHost.firstChild) c.contentHost.removeChild(c.contentHost.firstChild);
                     const empty = document.createElement('div');
                     empty.className = 'cam-empty';
                     const main = document.createElement('div');
                     main.textContent = text;
                     empty.appendChild(main);
                     if (errMsg) {
                       const sub = document.createElement('div');
                       sub.className = 'err-msg';
                       sub.textContent = '⚠ ' + errMsg;
                       empty.appendChild(sub);
                     }
                     c.contentHost.appendChild(empty);
                    }
                    
                    function renderCard(camId) {
                     const c = cards[camId];
                     if (!c) return;
                    
                     if (c.timeEl) c.timeEl.textContent = c.ts ? relativeTime(c.ts) : '';
                    
                     let showFile, showTs, showSource, slotLabel;
                     if (c.pickIdx !== null && c.hist[c.pickIdx] && c.hist[c.pickIdx].file) {
                       const h = c.hist[c.pickIdx];
                       showFile = h.file;
                       showTs = h.timestamp;
                       showSource = h.source;
                       slotLabel = '#' + c.pickIdx;
                     } else if (isVideoValid(c.value, c.ready, c.error)) {
                       showFile = c.value;
                       showTs = c.ts;
                       showSource = null;
                       slotLabel = null; // Live
                       c.pickIdx = null;
                     } else {
                       const errText = c.error && String(c.error).trim() && String(c.error).toLowerCase() !== 'null'
                         ? String(c.error) : null;
                       setEmpty(c, 'Kein aktuelles Video', errText);
                       updateNav(c);
                       return;
                     }
                    
                     const url = buildUrl(showFile, showTs);
                     if (!url) { setEmpty(c, 'Kein Video'); updateNav(c); return; }
                    
                     const wrap = document.createElement('div');
                     wrap.className = 'cam-video-wrap';
                     wrap.dataset.cam = camId;
                    
                     const video = document.createElement('video');
                     video.preload = 'metadata';
                     video.muted = true;
                     video.playsInline = true;
                     video.dataset.cam = camId;
                    
                     const source = document.createElement('source');
                     source.setAttribute('src', url);
                     source.setAttribute('type', 'video/mp4');
                     video.appendChild(source);
                    
                     const overlay = document.createElement('div');
                     overlay.className = 'overlay';
                     const playBtn = document.createElement('div');
                     playBtn.className = 'play-btn';
                     overlay.appendChild(playBtn);
                    
                     wrap.appendChild(video);
                     wrap.appendChild(overlay);
                    
                     if (slotLabel) {
                       const badge = document.createElement('div');
                       badge.className = 'slot-badge';
                       badge.textContent = slotLabel;
                       wrap.appendChild(badge);
                     }
                    
                     wrap.addEventListener('click', () => {
                       if (video.paused) {
                         video.controls = true;
                         wrap.classList.add('playing');
                         video.muted = false;
                         video.play().catch(() => { video.muted = true; video.play(); });
                       }
                     });
                     video.addEventListener('ended', () => { wrap.classList.remove('playing'); video.controls = false; });
                     video.addEventListener('pause', () => { if (video.ended) wrap.classList.remove('playing'); });
                    
                     while (c.contentHost.firstChild) c.contentHost.removeChild(c.contentHost.firstChild);
                     c.contentHost.appendChild(wrap);
                     video.load();
                    
                     // Label + Quelle anzeigen
                     let lbl = (slotLabel ? (slotLabel + ' · ') : 'Live · ') + (formatTs(showTs).replace(/, \\d{4}/, '') || '—');
                     if (showSource) lbl += '<span class="source ' + showSource + '">' + showSource + '</span>';
                     c.label.innerHTML = lbl;
                    
                     updateNav(c);
                    }
                    
                    function updateNav(c) {
                     // ◀ wäre älter (höhere Index-Zahl), ▶ wäre neuer
                     const nextIdx = c.pickIdx == null ? 0 : c.pickIdx + 1;
                     const prevIdx = c.pickIdx == null ? null : c.pickIdx - 1;
                     c.prevBtn.disabled = !c.hist.slice(nextIdx).some(h => h && h.file);
                     c.nextBtn.disabled = c.pickIdx === null;
                    }
                    
                    function updateAllTimes() {
                     Object.keys(cards).forEach(id => {
                       const c = cards[id];
                       if (c.timeEl) c.timeEl.textContent = c.ts ? relativeTime(c.ts) : '';
                     });
                    }
                    setInterval(updateAllTimes, 30000);
                    
                    fetch('/cameras').then(r => r.json()).then(list => {
                     if (!list.length) {
                       setStatus('Keine Kameras', 'err');
                       $grid.innerHTML = '<div style="padding:40px;text-align:center;color:#777">Keine Kameras gefunden</div>';
                       return;
                     }
                     list.forEach(buildCard);
                    
                     if (typeof io === 'undefined') { setStatus('Socket.IO Lib fehlt', 'err'); return; }
                     socket = io(IOBROKER_URL, { transports: ['websocket', 'polling'] });
                     socket.on('connect', () => {
                       setStatus('Verbunden', 'ok');
                       list.forEach(cam => {
                         let pending = 4 + 4 * HISTORY_SIZE;
                         const done = () => { if (--pending === 0) renderCard(cam.id); };
                    
                         socket.emit('getState', cam.ts_datapoint, (e, st) => {
                           if (st && st.val) cards[cam.id].ts = st.val;
                           done();
                         });
                         socket.emit('getState', cam.ready_datapoint, (e, st) => {
                           if (st) cards[cam.id].ready = st.val;
                           done();
                         });
                         socket.emit('getState', cam.error_datapoint, (e, st) => {
                           if (st) cards[cam.id].error = st.val;
                           done();
                         });
                         socket.emit('getState', cam.datapoint, (e, st) => {
                           if (st) cards[cam.id].value = st.val;
                           done();
                         });
                    
                         socket.emit('subscribe', cam.datapoint);
                         socket.emit('subscribe', cam.ts_datapoint);
                         socket.emit('subscribe', cam.ready_datapoint);
                         socket.emit('subscribe', cam.error_datapoint);
                    
                         cam.history.forEach((h, idx) => {
                           socket.emit('getState', h.file_datapoint, (e, st) => {
                             cards[cam.id].hist[idx].file = st ? st.val : null; done();
                           });
                           socket.emit('getState', h.timestamp_datapoint, (e, st) => {
                             cards[cam.id].hist[idx].timestamp = st ? st.val : null; done();
                           });
                           socket.emit('getState', h.id_datapoint, (e, st) => {
                             cards[cam.id].hist[idx].id = st ? st.val : null; done();
                           });
                           socket.emit('getState', h.source_datapoint, (e, st) => {
                             cards[cam.id].hist[idx].source = st ? st.val : null; done();
                           });
                           socket.emit('subscribe', h.file_datapoint);
                           socket.emit('subscribe', h.timestamp_datapoint);
                           socket.emit('subscribe', h.id_datapoint);
                           socket.emit('subscribe', h.source_datapoint);
                         });
                       });
                     });
                     socket.on('disconnect',    () => setStatus('Getrennt', 'err'));
                     socket.on('connect_error', (e) => { setStatus('Verbindungsfehler', 'err'); console.error(e); });
                     socket.on('stateChange', (id, state) => {
                       if (!state) return;
                       for (const cam of list) {
                         if (id === cam.datapoint)        { cards[cam.id].value = state.val; cards[cam.id].ts = state.ts || cards[cam.id].ts; renderCard(cam.id); return; }
                         if (id === cam.ts_datapoint)     { if (state.val) cards[cam.id].ts = state.val; renderCard(cam.id); return; }
                         if (id === cam.ready_datapoint)  { cards[cam.id].ready = state.val; renderCard(cam.id); return; }
                         if (id === cam.error_datapoint)  { cards[cam.id].error = state.val; renderCard(cam.id); return; }
                         // History-States?
                         for (let idx = 0; idx < HISTORY_SIZE; idx++) {
                           const h = cam.history[idx];
                           if (id === h.file_datapoint)      { cards[cam.id].hist[idx].file = state.val; renderCard(cam.id); return; }
                           if (id === h.timestamp_datapoint) { cards[cam.id].hist[idx].timestamp = state.val; renderCard(cam.id); return; }
                           if (id === h.id_datapoint)        { cards[cam.id].hist[idx].id = state.val; renderCard(cam.id); return; }
                           if (id === h.source_datapoint)    { cards[cam.id].hist[idx].source = state.val; renderCard(cam.id); return; }
                         }
                       }
                     });
                    }).catch(e => { setStatus('Server-Fehler', 'err'); console.error(e); });
                    </script>
                    </body>
                    </html>`;
                    
                    // ============================================================
                    // Widget 3: History (10 Slots einer Kamera in einer Reihe)
                    // ============================================================
                    const HISTORY_HTML = `<!DOCTYPE html>
                    <html lang="de">
                    <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>Blink History</title>
                    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
                    <style>
                    * { box-sizing: border-box; margin: 0; padding: 0; }
                    body { background:#1a1a1a; color:#eee; font-family:-apple-system,system-ui,sans-serif;
                          min-height:100vh; padding:12px; }
                    .topbar { display:flex; justify-content:space-between; align-items:center;
                             padding:0 4px 12px; gap:12px; flex-wrap:wrap; }
                    .topbar .title { font-size:14px; font-weight:600; }
                    .status { font-size:11px; padding:3px 8px; border-radius:10px; background:#555; }
                    .status.ok { background:#2d6a3e; }
                    .status.err { background:#8b2d2d; }
                    select { background:#444; color:#eee; border:1px solid #555; padding:5px 8px;
                            border-radius:6px; font-size:12px; }
                    .player { background:#2a2a2a; border-radius:10px; overflow:hidden; margin-bottom:12px;
                             box-shadow:0 4px 12px rgba(0,0,0,0.4); }
                    .player video { width:100%; display:block; background:#000; max-height:540px; }
                    .player .info { padding:10px 14px; font-size:12px; color:#bbb;
                                   border-top:1px solid #444; display:flex; gap:12px; flex-wrap:wrap; align-items:center; }
                    .player .info .src { padding:2px 6px; border-radius:4px; font-size:10px; background:#444; }
                    .player .info .src.cloud { background:#2d4a6a; }
                    .player .info .src.local_storage { background:#4a3a2d; }
                    .player .empty { padding:80px 20px; text-align:center; color:#777; }
                    
                    .grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));
                           gap:10px; }
                    .slot { background:#2a2a2a; border-radius:8px; overflow:hidden;
                           cursor:pointer; border:2px solid transparent; transition:all 0.15s; }
                    .slot:hover { border-color:#666; transform:translateY(-2px); }
                    .slot.active { border-color:#4a8; }
                    .slot.empty { opacity:0.3; cursor:not-allowed; }
                    .slot-thumb { position:relative; aspect-ratio:16/9; background:#000; }
                    .slot-thumb video { width:100%; height:100%; object-fit:cover; pointer-events:none; }
                    .slot-thumb .badge { position:absolute; top:4px; left:4px;
                                        background:rgba(0,0,0,0.7); color:white;
                                        padding:2px 6px; border-radius:3px; font-size:10px; font-weight:500; }
                    .slot-thumb .src-badge { position:absolute; top:4px; right:4px;
                                            background:#444; color:white;
                                            padding:2px 6px; border-radius:3px; font-size:9px; }
                    .slot-thumb .src-badge.cloud { background:#2d4a6a; }
                    .slot-thumb .src-badge.local_storage { background:#4a3a2d; }
                    .slot-info { padding:6px 8px; font-size:11px; }
                    .slot-info .time { color:#ddd; font-weight:500; }
                    .slot-info .rel { color:#888; font-size:10px; }
                    </style>
                    </head>
                    <body>
                    <div class="topbar">
                     <span class="title">📚 Verlauf</span>
                     <select id="picker"></select>
                     <span class="status" id="status">Verbinde…</span>
                    </div>
                    
                    <div class="player" id="player">
                     <div class="empty">Wähle einen Clip aus dem Verlauf</div>
                    </div>
                    
                    <div class="grid" id="grid"></div>
                    
                    <script>
                    __COMMON_JS__
                    
                    const params  = new URLSearchParams(location.search);
                    const fixedCamera = params.get('camera');
                    const $status = document.getElementById('status');
                    const $picker = document.getElementById('picker');
                    const $player = document.getElementById('player');
                    const $grid   = document.getElementById('grid');
                    
                    let socket, currentCam = null, cameras = [];
                    let histSlots = [];
                    let pickIdx = null;
                    
                    function setStatus(t, c) { $status.textContent = t; $status.className = 'status' + (c?' '+c:''); }
                    
                    function renderPlayer() {
                     if (pickIdx === null || !histSlots[pickIdx] || !histSlots[pickIdx].file) {
                       $player.innerHTML = '<div class="empty">Wähle einen Clip aus dem Verlauf</div>';
                       return;
                     }
                     const s = histSlots[pickIdx];
                     const url = buildUrl(s.file, s.timestamp);
                     if (!url) {
                       $player.innerHTML = '<div class="empty">Datei nicht verfügbar</div>';
                       return;
                     }
                     $player.innerHTML = '';
                     const video = document.createElement('video');
                     video.controls = true;
                     video.autoplay = true;
                     video.muted = false;
                     video.playsInline = true;
                     const source = document.createElement('source');
                     source.setAttribute('src', url);
                     source.setAttribute('type', 'video/mp4');
                     video.appendChild(source);
                     $player.appendChild(video);
                     video.load();
                     video.play().catch(() => { video.muted = true; video.play(); });
                    
                     const info = document.createElement('div');
                     info.className = 'info';
                     const slot = document.createElement('span');
                     slot.innerHTML = '<strong>Slot #' + pickIdx + '</strong>';
                     const time = document.createElement('span');
                     time.textContent = '🕒 ' + (formatTs(s.timestamp) || '—');
                     const rel = document.createElement('span');
                     rel.textContent = relativeTime(s.timestamp);
                     info.appendChild(slot);
                     info.appendChild(time);
                     info.appendChild(rel);
                     if (s.source) {
                       const src = document.createElement('span');
                       src.className = 'src ' + s.source;
                       src.textContent = s.source;
                       info.appendChild(src);
                     }
                     $player.appendChild(info);
                    }
                    
                    function renderGrid() {
                     $grid.innerHTML = '';
                     for (let i = 0; i < HISTORY_SIZE; i++) {
                       const s = histSlots[i] || {};
                       const slot = document.createElement('div');
                       slot.className = 'slot' + (s.file ? '' : ' empty') + (pickIdx === i ? ' active' : '');
                    
                       const thumb = document.createElement('div');
                       thumb.className = 'slot-thumb';
                       const badge = document.createElement('div');
                       badge.className = 'badge';
                       badge.textContent = '#' + i;
                       thumb.appendChild(badge);
                    
                       if (s.source) {
                         const srcBadge = document.createElement('div');
                         srcBadge.className = 'src-badge ' + s.source;
                         srcBadge.textContent = s.source === 'cloud' ? '☁' : '📥';
                         thumb.appendChild(srcBadge);
                       }
                    
                       if (s.file) {
                         const v = document.createElement('video');
                         v.preload = 'metadata';
                         v.muted = true;
                         const sourceEl = document.createElement('source');
                         sourceEl.setAttribute('src', buildUrl(s.file, s.timestamp));
                         sourceEl.setAttribute('type', 'video/mp4');
                         v.appendChild(sourceEl);
                         thumb.appendChild(v);
                       }
                    
                       const inf = document.createElement('div');
                       inf.className = 'slot-info';
                       const t = document.createElement('div');
                       t.className = 'time';
                       t.textContent = s.timestamp ? formatTs(s.timestamp).replace(/, \\d{4}/, '') : '—';
                       const r = document.createElement('div');
                       r.className = 'rel';
                       r.textContent = relativeTime(s.timestamp) || (s.file ? '' : 'leer');
                       inf.appendChild(t);
                       inf.appendChild(r);
                    
                       slot.appendChild(thumb);
                       slot.appendChild(inf);
                    
                       if (s.file) {
                         slot.addEventListener('click', () => {
                           pickIdx = (pickIdx === i) ? null : i;
                           renderPlayer();
                           renderGrid();
                         });
                       }
                       $grid.appendChild(slot);
                     }
                    }
                    
                    function unsubscribeCurrent() {
                     if (!currentCam || !socket) return;
                     currentCam.history.forEach(h => {
                       socket.emit('unsubscribe', h.file_datapoint);
                       socket.emit('unsubscribe', h.timestamp_datapoint);
                       socket.emit('unsubscribe', h.id_datapoint);
                       socket.emit('unsubscribe', h.source_datapoint);
                     });
                    }
                    
                    function switchCamera(cam) {
                     unsubscribeCurrent();
                     currentCam = cam;
                     pickIdx = null;
                     histSlots = new Array(HISTORY_SIZE).fill(null).map(() => ({}));
                     $player.innerHTML = '<div class="empty">Lade Verlauf…</div>';
                     $grid.innerHTML = '';
                    
                     let pending = 4 * HISTORY_SIZE;
                     const done = () => { if (--pending === 0) { renderGrid(); renderPlayer(); } };
                    
                     cam.history.forEach((h, idx) => {
                       socket.emit('getState', h.file_datapoint, (e, st) => {
                         histSlots[idx].file = st ? st.val : null; done();
                       });
                       socket.emit('getState', h.timestamp_datapoint, (e, st) => {
                         histSlots[idx].timestamp = st ? st.val : null; done();
                       });
                       socket.emit('getState', h.id_datapoint, (e, st) => {
                         histSlots[idx].id = st ? st.val : null; done();
                       });
                       socket.emit('getState', h.source_datapoint, (e, st) => {
                         histSlots[idx].source = st ? st.val : null; done();
                       });
                       socket.emit('subscribe', h.file_datapoint);
                       socket.emit('subscribe', h.timestamp_datapoint);
                       socket.emit('subscribe', h.id_datapoint);
                       socket.emit('subscribe', h.source_datapoint);
                     });
                    }
                    
                    fetch('/cameras').then(r => r.json()).then(list => {
                     cameras = list;
                     if (!cameras.length) {
                       setStatus('Keine Kameras', 'err');
                       return;
                     }
                     cameras.forEach(c => {
                       const o = document.createElement('option');
                       o.value = c.id;
                       o.textContent = c.name || ('Kamera ' + c.id);
                       $picker.appendChild(o);
                     });
                     $picker.addEventListener('change', () => {
                       const cam = cameras.find(c => c.id === $picker.value);
                       if (cam) switchCamera(cam);
                     });
                    
                     const startCam = fixedCamera
                       ? cameras.find(c => c.id === fixedCamera) || cameras[0]
                       : cameras[0];
                     $picker.value = startCam.id;
                    
                     if (typeof io === 'undefined') { setStatus('Socket.IO Lib fehlt', 'err'); return; }
                     socket = io(IOBROKER_URL, { transports: ['websocket', 'polling'] });
                     socket.on('connect', () => { setStatus('Verbunden', 'ok'); switchCamera(startCam); });
                     socket.on('disconnect', () => setStatus('Getrennt', 'err'));
                     socket.on('connect_error', (e) => { setStatus('Verbindungsfehler', 'err'); console.error(e); });
                     socket.on('stateChange', (id, state) => {
                       if (!state || !currentCam) return;
                       for (let idx = 0; idx < HISTORY_SIZE; idx++) {
                         const h = currentCam.history[idx];
                         if (id === h.file_datapoint)      { histSlots[idx].file = state.val; renderGrid(); return; }
                         if (id === h.timestamp_datapoint) { histSlots[idx].timestamp = state.val; renderGrid(); return; }
                         if (id === h.id_datapoint)        { histSlots[idx].id = state.val; renderGrid(); return; }
                         if (id === h.source_datapoint)    { histSlots[idx].source = state.val; renderGrid(); return; }
                       }
                     });
                    }).catch(e => { setStatus('Server-Fehler', 'err'); console.error(e); });
                    </script>
                    </body>
                    </html>`;
                    
                    function buildHTML(template) {
                       return template
                           .replace('__COMMON_JS__',     COMMON_JS)
                           .replace(/__VIDEO_BASE__/g,    VIDEO_BASE)
                           .replace(/__VIDEO_PORT__/g,    PORT)
                           .replace(/__IOBROKER_PORT__/g, IOBROKER_PORT)
                           .replace(/__HISTORY_SIZE__/g,  HISTORY_SIZE);
                    }
                    const SINGLE_PAGE  = buildHTML(WIDGET_HTML);
                    const GRID_PAGE    = buildHTML(GRID_HTML);
                    const HISTORY_PAGE = buildHTML(HISTORY_HTML);
                    
                    const MIME = {
                       '.mp4':'video/mp4','.webm':'video/webm','.mov':'video/quicktime',
                       '.jpg':'image/jpeg','.jpeg':'image/jpeg','.png':'image/png',
                       '.html':'text/html; charset=utf-8','.json':'application/json'
                    };
                    
                    const server = http.createServer(async (req, res) => {
                       res.setHeader('Access-Control-Allow-Origin', '*');
                       res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
                    
                       if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
                    
                       const urlPath = req.url.split('?')[0];
                    
                       if (urlPath === '/cameras') {
                           try {
                               const cams = await discoverCameras();
                               res.writeHead(200, {
                                   'Content-Type': 'application/json',
                                   'Cache-Control': 'no-cache, no-store, must-revalidate'
                               });
                               res.end(JSON.stringify(cams));
                           } catch (e) {
                               log('Kamera-Discovery Fehler: ' + e.message, 'error');
                               res.writeHead(500); res.end('Error');
                           }
                           return;
                       }
                    
                       if (urlPath === '/grid' || urlPath === '/grid.html') {
                           res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
                           res.end(GRID_PAGE);
                           return;
                       }
                    
                       if (urlPath === '/history' || urlPath === '/history.html') {
                           res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
                           res.end(HISTORY_PAGE);
                           return;
                       }
                    
                       if (urlPath === '/' || urlPath === '/index.html') {
                           res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
                           res.end(SINGLE_PAGE);
                           return;
                       }
                    
                       if (!urlPath.startsWith(VIDEO_BASE)) { res.writeHead(404); res.end('Not Found'); return; }
                    
                       const filename = decodeURIComponent(urlPath.slice(VIDEO_BASE.length));
                       if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
                           res.writeHead(403); res.end('Forbidden'); return;
                       }
                    
                       const fullPath = path.join(ROOT_DIR, filename);
                       if (!fullPath.startsWith(ROOT_DIR)) { res.writeHead(403); res.end('Forbidden'); return; }
                    
                       fs.stat(fullPath, (err, stat) => {
                           if (err || !stat.isFile()) { res.writeHead(404); res.end('File Not Found'); return; }
                    
                           const ext = path.extname(filename).toLowerCase();
                           const mimeType = MIME[ext] || 'application/octet-stream';
                           const range = req.headers.range;
                    
                           const noCacheHeaders = mimeType.startsWith('video/')
                               ? {
                                   'Cache-Control': 'no-cache, no-store, must-revalidate',
                                   'Pragma':        'no-cache',
                                   'Expires':       '0'
                                 }
                               : {};
                    
                           if (range && mimeType.startsWith('video/')) {
                               const parts = range.replace(/bytes=/, '').split('-');
                               const start = parseInt(parts[0], 10);
                               const end   = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
                               res.writeHead(206, {
                                   ...noCacheHeaders,
                                   'Content-Range':  `bytes ${start}-${end}/${stat.size}`,
                                   'Accept-Ranges':  'bytes',
                                   'Content-Length': end - start + 1,
                                   'Content-Type':   mimeType
                               });
                               fs.createReadStream(fullPath, { start, end }).pipe(res);
                           } else {
                               res.writeHead(200, {
                                   ...noCacheHeaders,
                                   'Content-Length': stat.size,
                                   'Content-Type':   mimeType,
                                   'Accept-Ranges':  'bytes'
                               });
                               fs.createReadStream(fullPath).pipe(res);
                           }
                       });
                    });
                    
                    server.listen(PORT, () => {
                       log(`Blink-Server läuft: http://<host>:${PORT}/  (Single + History-Streifen)  ·  /grid (Multi mit Blättern)  ·  /history (Verlauf-Galerie)`);
                    });
                    server.on('error', (err) => log(`Blink-Server Fehler: ${err.message}`, 'error'));
                    
                    globalThis.__blinkServer = server;
                    
                    onStop(() => {
                       if (server) { server.close(); log('Blink-Server gestoppt'); }
                    }, 2000);
                    

                    2af22229-da6f-4a9c-999e-6655b43b3cc2-image.jpeg

                    sigi234S Online
                    sigi234S Online
                    sigi234
                    Forum Testing Most Active
                    schrieb am zuletzt editiert von
                    #157

                    @Pischleuder

                    Hallo, kannst noch eine Benachrichtigung per Mail einbauen?

                    Bitte benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.
                    Immer Daten sichern!

                    PischleuderP 1 Antwort Letzte Antwort
                    0
                    • sigi234S sigi234

                      @Pischleuder

                      Hallo, kannst noch eine Benachrichtigung per Mail einbauen?

                      PischleuderP Online
                      PischleuderP Online
                      Pischleuder
                      schrieb am zuletzt editiert von
                      #158

                      @sigi234 sagte:

                      @Pischleuder

                      Hallo, kannst noch eine Benachrichtigung per Mail einbauen?

                      was benötigst Du genau ?

                      sigi234S 1 Antwort Letzte Antwort
                      0
                      • PischleuderP Pischleuder

                        @sigi234 sagte:

                        @Pischleuder

                        Hallo, kannst noch eine Benachrichtigung per Mail einbauen?

                        was benötigst Du genau ?

                        sigi234S Online
                        sigi234S Online
                        sigi234
                        Forum Testing Most Active
                        schrieb am zuletzt editiert von
                        #159

                        @Pischleuder sagte:

                        was benötigst Du genau ?

                        Bei Bewegung und Batterie

                        Bitte benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.
                        Immer Daten sichern!

                        1 Antwort Letzte Antwort
                        0
                        • PischleuderP Online
                          PischleuderP Online
                          Pischleuder
                          schrieb am zuletzt editiert von
                          #160

                          nehme ich auf die Liste, wird dann aber in die Version mit Live-view einfliessen.

                          1 Antwort Letzte Antwort
                          1
                          • A Offline
                            A Offline
                            adarof
                            schrieb am zuletzt editiert von
                            #161

                            Hallo
                            Heute nacht hatte ich einen Internetausfall - sowohl Router sagt dass wie auch der Provider per Statusmails.
                            Zeitgleich mit Wiederherstellen der Verbindung wurde ein neuer 2FA Code von Blink gesendet
                            Kann es sein dass der Adapter das war?

                            Er hat gerade auch nicht (orange) und ich musste ihn neustarten und den dann bekommenen 2FA Code eintragen. Nun tuts wieder.
                            Kann es sein dass der Adapter bei verbindungsverlust unnötigerweise immer einen neuen 2FA Code anfordet?

                            Gruss

                            1 Antwort Letzte Antwort
                            0
                            • PischleuderP Online
                              PischleuderP Online
                              Pischleuder
                              schrieb am zuletzt editiert von Pischleuder
                              #162

                              Der Adapter fordert immer dann einen neuen Pin an, wenn beim IoBroker der Session Cache verloren geht. In der Regel, wenn ein Neustart gemacht wird. Ich habe aber den Login auf drei Versuche beschränkt, um zu verhindern, dass der Account 24 Stunden gesperrt wird. Dafür musst du aber auf Version 0.0.14 sein.

                              1 Antwort Letzte Antwort
                              0
                              • H Offline
                                H Offline
                                helfi9999
                                schrieb am zuletzt editiert von
                                #163

                                Hi musste mein System neu starten und jetzt zeigt der Adapter folgendes an: Initialer Connect/Poll fehlgeschlagen: Blink Login fehlgeschlagen: HTTP 202 – Location="" Body={"next_time_in_secs":60,"phone":"+49xxxxxxxx99","tsv_methods":["sms","whatsapp","voice"],"tsv_state":"sms","user_id":121027091}Siehe Debug-Log: /tmp/blink_debug.log
                                Was kann ich dagegen machen habe schon einen neuen code eingegeben aber es ändert sich nichts

                                Intel NUC mit Iobroker

                                Thomas BraunT PischleuderP 2 Antworten Letzte Antwort
                                0
                                • H helfi9999

                                  Hi musste mein System neu starten und jetzt zeigt der Adapter folgendes an: Initialer Connect/Poll fehlgeschlagen: Blink Login fehlgeschlagen: HTTP 202 – Location="" Body={"next_time_in_secs":60,"phone":"+49xxxxxxxx99","tsv_methods":["sms","whatsapp","voice"],"tsv_state":"sms","user_id":121027091}Siehe Debug-Log: /tmp/blink_debug.log
                                  Was kann ich dagegen machen habe schon einen neuen code eingegeben aber es ändert sich nichts

                                  Thomas BraunT Online
                                  Thomas BraunT Online
                                  Thomas Braun
                                  Most Active
                                  schrieb am zuletzt editiert von
                                  #164

                                  @helfi9999 sagte:

                                  Siehe Debug-Log: /tmp/blink_debug.log

                                  Linux-Werkzeugkasten:
                                  https://forum.iobroker.net/topic/42952/der-kleine-iobroker-linux-werkzeugkasten
                                  NodeJS Fixer Skript:
                                  https://forum.iobroker.net/topic/68035/iob-node-fix-skript
                                  iob_diag: curl -sLf -o diag.sh https://iobroker.net/diag.sh && bash diag.sh

                                  1 Antwort Letzte Antwort
                                  0
                                  • H helfi9999

                                    Hi musste mein System neu starten und jetzt zeigt der Adapter folgendes an: Initialer Connect/Poll fehlgeschlagen: Blink Login fehlgeschlagen: HTTP 202 – Location="" Body={"next_time_in_secs":60,"phone":"+49xxxxxxxx99","tsv_methods":["sms","whatsapp","voice"],"tsv_state":"sms","user_id":121027091}Siehe Debug-Log: /tmp/blink_debug.log
                                    Was kann ich dagegen machen habe schon einen neuen code eingegeben aber es ändert sich nichts

                                    PischleuderP Online
                                    PischleuderP Online
                                    Pischleuder
                                    schrieb am zuletzt editiert von
                                    #165

                                    @helfi9999 sagte:

                                    Hi musste mein System neu starten und jetzt zeigt der Adapter folgendes an: Initialer Connect/Poll fehlgeschlagen: Blink Login fehlgeschlagen: HTTP 202 – Location="" Body

                                    Hi,

                                    der Fehler ist bisher noch nie aufgetaucht, werde ihn aber im nächsten Release berücksichtigen. Habe dir etwas per Chat gesendet, probiere das bitte einmal aus.

                                    1 Antwort Letzte Antwort
                                    0
                                    • nograxN Offline
                                      nograxN Offline
                                      nograx
                                      Developer
                                      schrieb am zuletzt editiert von
                                      #166

                                      Habe exakt das selbe Problem, kam aus heiterem Himmel. One Time Code wird nicht akzeptiert

                                      1 Antwort Letzte Antwort
                                      0
                                      • PischleuderP Online
                                        PischleuderP Online
                                        Pischleuder
                                        schrieb am zuletzt editiert von
                                        #167

                                        Du hast Post

                                        1 Antwort Letzte Antwort
                                        0

                                        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
                                        Antworten
                                        • In einem neuen Thema antworten
                                        Anmelden zum Antworten
                                        • Älteste zuerst
                                        • Neuste zuerst
                                        • Meiste Stimmen


                                        Support us

                                        ioBroker
                                        Community Adapters
                                        Donate

                                        496

                                        Online

                                        32.9k

                                        Benutzer

                                        83.2k

                                        Themen

                                        1.3m

                                        Beiträge
                                        Community
                                        Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                                        ioBroker Community 2014-2026
                                        logo
                                        • Anmelden

                                        • Du hast noch kein Konto? Registrieren

                                        • Anmelden oder registrieren, um zu suchen
                                        • Erster Beitrag
                                          Letzter Beitrag
                                        0
                                        • Home
                                        • Aktuell
                                        • Tags
                                        • Ungelesen 0
                                        • Kategorien
                                        • Unreplied
                                        • Beliebt
                                        • GitHub
                                        • Docu
                                        • Hilfe