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. Skripten / Logik
  4. Alexa Shopping List mit Bring synchronisieren

NEWS

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

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

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

Alexa Shopping List mit Bring synchronisieren

Geplant Angeheftet Gesperrt Verschoben Skripten / Logik
183 Beiträge 32 Kommentatoren 42.4k Aufrufe 31 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.
  • mcBirneM Offline
    mcBirneM Offline
    mcBirne
    schrieb am zuletzt editiert von mcBirne
    #174

    Ich habe die beiden Datenpunkte angepasst. Leider erhalte ich folgende Fehlermeldung:

    javascript.0	16:24:56.439	error	
    compile failed at: script.js.Alexas.Bring_Synchronisieren2:36
    javascript.0	16:24:56.439	error	
    export default this;
    javascript.0	16:24:56.439	error	
    ^^^^^^
    javascript.0	16:24:56.439	error	
    SyntaxError: Unexpected token 'export'
    javascript.0	16:24:56.439	error	
        at new Script (node:vm:117:7)
    javascript.0	16:24:56.439	error	
        at JavaScript.createVM (/opt/iobroker/node_modules/iobroker.javascript/src/main.ts:2174:25)
    javascript.0	16:24:56.439	error	
        at JavaScript.prepareScript (/opt/iobroker/node_modules/iobroker.javascript/src/main.ts:2472:44)
    javascript.0	16:24:56.439	error	
        at processTicksAndRejections (node:internal/process/task_queues:105:5)
    javascript.0	16:24:56.439	error	
        at JavaScript.onObjectChange (/opt/iobroker/node_modules/iobroker.javascript/src/main.ts:659:25)
    

    Hat das Problem noch jemand? Oder weiß jemand, was ich machen kann?

    grrfieldG 1 Antwort Letzte Antwort
    0
    • mcBirneM mcBirne

      Ich habe die beiden Datenpunkte angepasst. Leider erhalte ich folgende Fehlermeldung:

      javascript.0	16:24:56.439	error	
      compile failed at: script.js.Alexas.Bring_Synchronisieren2:36
      javascript.0	16:24:56.439	error	
      export default this;
      javascript.0	16:24:56.439	error	
      ^^^^^^
      javascript.0	16:24:56.439	error	
      SyntaxError: Unexpected token 'export'
      javascript.0	16:24:56.439	error	
          at new Script (node:vm:117:7)
      javascript.0	16:24:56.439	error	
          at JavaScript.createVM (/opt/iobroker/node_modules/iobroker.javascript/src/main.ts:2174:25)
      javascript.0	16:24:56.439	error	
          at JavaScript.prepareScript (/opt/iobroker/node_modules/iobroker.javascript/src/main.ts:2472:44)
      javascript.0	16:24:56.439	error	
          at processTicksAndRejections (node:internal/process/task_queues:105:5)
      javascript.0	16:24:56.439	error	
          at JavaScript.onObjectChange (/opt/iobroker/node_modules/iobroker.javascript/src/main.ts:659:25)
      

      Hat das Problem noch jemand? Oder weiß jemand, was ich machen kann?

      grrfieldG Offline
      grrfieldG Offline
      grrfield
      schrieb am zuletzt editiert von
      #175

      @mcBirne Es ist zwar schon einige Zeit her, aber hast Du das Skript als TypeScript eingefügt? Die Fehlermeldungen sehen nach JavaScript aus.

      mcBirneM 1 Antwort Letzte Antwort
      0
      • HeimwehH Heimweh

        @martin_olw Ich hab es jetzt doch getestet und noch was geändert:

        Hier die Beschreibung:

        Der Skript überwacht den Datenpunkt alexa2.0.Lists.SHOP.json.

        Sobald Alexa z. B. sagt:

        „Alexa, setze vier Äpfel auf die Einkaufsliste“
        wird das als neuer Eintrag in dieser Liste erkannt.

        Zahlwort-Umwandlung:

        Aus „vier Äpfel“ wird „4 Äpfel“.

        Auch zusammengesetzte Zahlen wie „fünfzehn“ oder „dreiundzwanzig“ werden korrekt erkannt.

        Das erste Wort nach der Zahl wird großgeschrieben: z. B. „4 Äpfel“ statt „4 äpfel“.

        Todoist-Eintrag:

        Der bereinigte und umgewandelte Eintrag wird direkt über die REST-API an Todoist gesendet und dort im definierten Projekt als Aufgabe eingetragen.

        Automatisches Entfernen in Alexa:

        Nach 60 Sekunden wird der passende Alexa-Listeneintrag automatisch als „abgehakt“ markiert.

        Es muss nur die ListID und der Token eingesetzt werden.

        const axios = require('axios');
        
        // Todoist API-Konfiguration
        const todoistProjectId = 'XXXXXXXX'; // Deine Projekt-ID
        const todoistToken = 'XXXXXXXXXXXXXXXXXXXXXXX'; // Dein Token
        
        let previousList = [];
        
        // 1. Alexa-Änderungen überwachen
        on({ id: 'alexa2.0.Lists.SHOP.json', change: 'any' }, function (obj) {
            try {
                const currentList = JSON.parse(obj.state.val);
        
                if (currentList && currentList.length > 0) {
                    if (previousList.length < currentList.length) {
                        const newItem = currentList.find(item =>
                            !previousList.some(prevItem => prevItem.id === item.id)
                        );
        
                        if (newItem) {
                            const umgewandelt = wordsToNumbersSmart(newItem.value);
                            const aufgabe = capitalizeFirst(umgewandelt);
                            addTaskToTodoist(aufgabe);
        
                            // Nach 60 Sekunden Alexa-Eintrag auf "completed" setzen
                            setTimeout(() => {
                                const alexaList = JSON.parse(getState('alexa2.0.Lists.SHOP.json').val);
        
                                const matchingItem = alexaList.find(item => {
                                    const itemText = wordsToNumbersSmart(item.value).trim().toLowerCase();
                                    return itemText === aufgabe.trim().toLowerCase();
                                });
        
                                if (matchingItem) {
                                    const completeState = `alexa2.0.Lists.SHOP.items.${matchingItem.id}.completed`;
                                    setState(completeState, true);
                                } else {
                                    console.warn(`⚠️ Kein passender Alexa-Eintrag zu "${aufgabe}" gefunden.`);
                                }
                            }, 60 * 1000); // 60 Sekunden
                        }
                    }
        
                    previousList = currentList;
                }
            } catch (e) {
                console.error('Fehler beim Parsen der Alexa-Liste:', e.message || e);
            }
        });
        
        // 2. Aufgaben an Todoist senden
        function addTaskToTodoist(itemValue) {
            const todoistData = {
                content: itemValue,
                project_id: todoistProjectId
            };
        
            axios.post('https://api.todoist.com/rest/v2/tasks', todoistData, {
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${todoistToken}`
                }
            })
            .then(() => {
                console.log(`✅ "${itemValue}" zu Todoist hinzugefügt.`);
            })
            .catch(error => {
                console.error('Fehler beim Hinzufügen zu Todoist:', error.message || error);
            });
        }
        
        // 3. Erstes Wort groß
        function capitalizeFirst(text) {
            if (!text || typeof text !== 'string') return '';
            return text.charAt(0).toUpperCase() + text.slice(1);
        }
        
        // 4. Wörter → Zahlen (z. B. „vierzehn“ → 14)
        function wordsToNumbersSmart(text) {
            const ones = {
                'null': 0, 'eins': 1, 'eine': 1, 'einen': 1,
                'zwei': 2, 'drei': 3, 'vier': 4, 'fünf': 5,
                'sechs': 6, 'sieben': 7, 'acht': 8, 'neun': 9,
                'zehn': 10, 'elf': 11, 'zwölf': 12, 'dreizehn': 13,
                'vierzehn': 14, 'fünfzehn': 15, 'sechzehn': 16,
                'siebzehn': 17, 'achtzehn': 18, 'neunzehn': 19
            };
        
            const tens = {
                'zwanzig': 20, 'dreißig': 30, 'vierzig': 40,
                'fünfzig': 50, 'sechzig': 60, 'siebzig': 70,
                'achtzig': 80, 'neunzig': 90
            };
        
            const multipliers = {
                'hundert': 100,
                'tausend': 1000
            };
        
            const skipWords = ['und', 'oder', 'mit', 'für', 'pro'];
            const words = text.toLowerCase().split(/\s+/);
            const finalText = [];
            let i = 0;
            let capitalizeNext = 0;
        
            while (i < words.length) {
                const word = words[i];
        
                if (ones[word] !== undefined) {
                    if (i + 2 < words.length && words[i + 1] === 'und' && tens[words[i + 2]]) {
                        const value = ones[word] + tens[words[i + 2]];
                        finalText.push(value.toString());
                        capitalizeNext = 2;
                        i += 3;
                        continue;
                    }
        
                    if (i + 1 < words.length && multipliers[words[i + 1]]) {
                        const value = ones[word] * multipliers[words[i + 1]];
                        finalText.push(value.toString());
                        capitalizeNext = 2;
                        i += 2;
                        continue;
                    }
        
                    finalText.push(ones[word].toString());
                    capitalizeNext = 2;
                    i++;
                } else if (tens[word] !== undefined) {
                    finalText.push(tens[word].toString());
                    capitalizeNext = 2;
                    i++;
                } else if (!isNaN(word)) {
                    finalText.push(word);
                    capitalizeNext = 2;
                    i++;
                } else {
                    if (capitalizeNext > 0 && !skipWords.includes(word)) {
                        finalText.push(word.charAt(0).toUpperCase() + word.slice(1));
                        capitalizeNext--;
                    } else {
                        finalText.push(word);
                    }
                    i++;
                }
            }
        
            return finalText.join(' ');
        }
        
        
        M Offline
        M Offline
        martin_olw
        schrieb am zuletzt editiert von
        #176

        Hallo @Heimweh und vielen Dank noch einmal für das Script und die Anpassungen. Lief bis vor kurzem alles ohne Probleme bei mir. Jetzt aber bekomme ich immer folgende Fehlermeldung:

        script.js.common.ToDoist.Einkaufsliste_Alexa_Todoist: Fehler beim Hinzufügen zu Todoist:
        

        Mehr nicht. Woran kann das liegen?
        Prinzipiell funktioniert dein Skript aber:

        Updating item "bananentest" ({"id":"ad0e6a37-8dc2-4085-83d6-b282773292ac","listId":"YW16bjEuYWNjb3VudC5BRTVPSlgyRFk0UUhEVUJUNEJOWUpPUEJVSjNBLVNIT1BQSU5HX0lURU0=","value":"bananentest","customerId":"A2FTPWL63RKNMN","completed":true,"createdDateTime":1770990517898,"updatedDateTime":1770990517898,"version":1,"note":null,"index":0,"#delete":false,"listName":"SHOP"}) of the list SHOP.
        

        Funktioniert das bei dir noch alles?
        Danke für die ernuete Hilfe!
        VG Martin

        1 Antwort Letzte Antwort
        0
        • N Offline
          N Offline
          no6mis
          schrieb am zuletzt editiert von
          #177

          Todoist hat die API angepasst, v2 gibt es nicht mehr.
          https://developer.todoist.com/api/v1/#tag/Tasks

          Du musst die API-URL auf https://api.todoist.com/api/v1/tasks ändern und die Projekt-IDs aktualisieren.

          M 1 Antwort Letzte Antwort
          0
          • N no6mis

            Todoist hat die API angepasst, v2 gibt es nicht mehr.
            https://developer.todoist.com/api/v1/#tag/Tasks

            Du musst die API-URL auf https://api.todoist.com/api/v1/tasks ändern und die Projekt-IDs aktualisieren.

            M Offline
            M Offline
            martin_olw
            schrieb am zuletzt editiert von
            #178

            @no6mis Ich danke dir! Genau das hat geholfen!

            1 Antwort Letzte Antwort
            0
            • HeimwehH Offline
              HeimwehH Offline
              Heimweh
              schrieb am zuletzt editiert von
              #179

              Sorry für meine späte Antwort - bei mir geht es auch nicht mehr. Die Projekt ID (zumindest die aus dem Browser) ist keine reine numerische Zahl mehr?

              N 1 Antwort Letzte Antwort
              0
              • HeimwehH Heimweh

                Sorry für meine späte Antwort - bei mir geht es auch nicht mehr. Die Projekt ID (zumindest die aus dem Browser) ist keine reine numerische Zahl mehr?

                N Offline
                N Offline
                no6mis
                schrieb am zuletzt editiert von
                #180

                @Heimweh sagte in Alexa Shopping List mit Bring synchronisieren:

                Die Projekt ID (zumindest die aus dem Browser) ist keine reine numerische Zahl mehr?

                ja, die Zeichenfolge aus dem Browser (der Teil nach dem Klarnamen-) kannst du 1:1 in dein Script übernehmen.

                1 Antwort Letzte Antwort
                0
                • HeimwehH Offline
                  HeimwehH Offline
                  Heimweh
                  schrieb am zuletzt editiert von
                  #181

                  Ich habe diese Änderung der Todoist API zum Anlass genommen den Script zu überarbeiten, da mittlerweile auch wieder die Listen in den Alexa Adapter eingelesen werden. Vielleicht kann den Script ja jemand brauchen.....

                  Alexa Shoppingliste ↔ Todoist (Projekt-Sync)

                  Dieses Script synchronisiert die Alexa Shoppingliste
                  (alexa2.0.Lists.SHOP.json) mit einem definierten Todoist-Projekt.

                  Funktionen

                  Beim Scriptstart werden alle offenen Alexa-Einträge in das Todoist-Projekt übernommen.

                  Neue Einträge in Alexa werden automatisch in Todoist angelegt.

                  Wird ein Task in Todoist erledigt, wird er in Alexa als „completed“ markiert.

                  Gelöschte oder erledigte Tasks in Todoist verschwinden damit auch aus der offenen Alexa-Liste.

                  Doppelimporte werden durch ein internes Mapping verhindert.

                  Deutsche Zahlwörter werden automatisch in Zahlen umgewandelt (z. B. „zwei Liter“ → „2 Liter“).

                  Voraussetzungen

                  ioBroker mit javascript-Adapter

                  alexa2-Adapter mit aktivierter Listenfunktion

                  Todoist API Token (REST v1)

                  axios (muss installiert sein)

                  Unterschied zur älteren Version

                  Es wird nicht mehr der Datenpunkt alexa2.0.History.summary ausgewertet.

                  Es wird direkt der vom alexa2-Adapter bereitgestellte Listen-JSON verwendet.

                  Die Synchronisation funktioniert jetzt sauber in beide Richtungen.

                  Es wird ausschließlich die aktuelle Todoist REST v1 API verwendet.

                  const axios = require("axios");
                  
                  const TAG = "A2S-SHOP-PROD-2";
                  
                  const ALEXA_JSON_DP = "alexa2.0.Lists.SHOP.json";
                  const ALEXA_ITEMS_ROOT = "alexa2.0.Lists.SHOP.items";
                  
                  const TODOIST_TOKEN = "HIER_DEIN_TODOIST_TOKEN";
                  const TODOIST_BASE = "https://api.todoist.com/api/v1";
                  const TODOIST_PROJECT_ID = "HIER_DEINE_TODOIST_PROJEKT_ID";
                  
                  const POLL_SEC = 20;
                  const MIN_TASK_AGE_MS = 5_000;
                  
                  const STORE_ROOT = "0_userdata.0.alexaTodoistBiSync";
                  const MAP_STATE = `${STORE_ROOT}.map_shop`;
                  
                  function headers(extra = {}) {
                    return { Authorization: `Bearer ${TODOIST_TOKEN}`, ...extra };
                  }
                  
                  async function todoistCreateTask(content, projectId) {
                    const res = await axios.post(
                      `${TODOIST_BASE}/tasks`,
                      { content, project_id: projectId },
                      { headers: headers({ "Content-Type": "application/json" }), timeout: 15000 }
                    );
                    return res.data;
                  }
                  
                  async function todoistGetTask(taskId) {
                    const res = await axios.get(`${TODOIST_BASE}/tasks/${taskId}`, {
                      headers: headers(),
                      timeout: 15000,
                      validateStatus: (s) => (s >= 200 && s < 300) || s === 404,
                    });
                    return res;
                  }
                  
                  function ensureStateIfMissing(id, def, common) {
                    if (!existsState(id)) createState(id, def, common);
                  }
                  
                  function readJsonSafe(id, fallback) {
                    const st = getState(id);
                    if (!st || st.val === null || st.val === undefined || st.val === "") return fallback;
                    try { return JSON.parse(st.val); } catch { return fallback; }
                  }
                  
                  function writeJson(id, obj) {
                    setState(id, JSON.stringify(obj), true);
                  }
                  
                  function parseAlexaJson(raw) {
                    if (raw === null || raw === undefined) return [];
                    const data = typeof raw === "string" ? JSON.parse(raw) : raw;
                    return Array.isArray(data) ? data : [];
                  }
                  
                  function isCompletedAlexa(it) {
                    const v = it?.completed;
                    return v === true || v === 1 || v === "1" || v === "true";
                  }
                  
                  function normalizeMap(map) {
                    let changed = false;
                    const now = Date.now();
                    for (const [alexaId, info] of Object.entries(map)) {
                      if (!info || typeof info !== "object") {
                        map[alexaId] = { todoistId: null, value: "", createdAt: now, alexaCompletedPushed: false };
                        changed = true;
                        continue;
                      }
                      if (info.todoistId === undefined) { info.todoistId = null; changed = true; }
                      if (info.value === undefined) { info.value = ""; changed = true; }
                      if (!info.createdAt || isNaN(Number(info.createdAt))) { info.createdAt = now; changed = true; }
                      if (info.alexaCompletedPushed === undefined) { info.alexaCompletedPushed = false; changed = true; }
                    }
                    return changed;
                  }
                  
                  // ---- Text normalisieren (Zahlwörter) ----
                  function capitalizeFirst(text) {
                    if (!text || typeof text !== "string") return "";
                    return text.charAt(0).toUpperCase() + text.slice(1);
                  }
                  
                  function wordsToNumbersSmart(text) {
                    const ones = {
                      "null": 0, "eins": 1, "eine": 1, "einen": 1, "ein": 1,
                      "zwei": 2, "drei": 3, "vier": 4, "fünf": 5,
                      "sechs": 6, "sieben": 7, "acht": 8, "neun": 9,
                      "zehn": 10, "elf": 11, "zwölf": 12, "dreizehn": 13,
                      "vierzehn": 14, "fünfzehn": 15, "sechzehn": 16,
                      "siebzehn": 17, "achtzehn": 18, "neunzehn": 19
                    };
                    const tens = {
                      "zwanzig": 20, "dreißig": 30, "dreissig": 30,
                      "vierzig": 40, "fünfzig": 50, "sechzig": 60,
                      "siebzig": 70, "achtzig": 80, "neunzig": 90
                    };
                    const multipliers = { "hundert": 100, "tausend": 1000 };
                    const skipWords = ["und", "oder", "mit", "für", "pro"];
                  
                    const words = String(text).toLowerCase().split(/\s+/).filter(Boolean);
                    const out = [];
                    let i = 0;
                    let capNext = 0;
                  
                    while (i < words.length) {
                      const w = words[i];
                  
                      if (ones[w] !== undefined) {
                        if (i + 2 < words.length && words[i + 1] === "und" && tens[words[i + 2]] !== undefined) {
                          out.push(String(ones[w] + tens[words[i + 2]]));
                          capNext = 2;
                          i += 3;
                          continue;
                        }
                        if (i + 1 < words.length && multipliers[words[i + 1]] !== undefined) {
                          out.push(String(ones[w] * multipliers[words[i + 1]]));
                          capNext = 2;
                          i += 2;
                          continue;
                        }
                        out.push(String(ones[w]));
                        capNext = 2;
                        i++;
                        continue;
                      }
                  
                      if (tens[w] !== undefined) {
                        out.push(String(tens[w]));
                        capNext = 2;
                        i++;
                        continue;
                      }
                  
                      if (!isNaN(w)) {
                        out.push(w);
                        capNext = 2;
                        i++;
                        continue;
                      }
                  
                      if (capNext > 0 && !skipWords.includes(w)) {
                        out.push(w.charAt(0).toUpperCase() + w.slice(1));
                        capNext--;
                      } else {
                        out.push(w);
                      }
                      i++;
                    }
                  
                    return out.join(" ");
                  }
                  
                  function normalizeTaskText(raw) {
                    const withNums = wordsToNumbersSmart(String(raw || "").trim());
                    return capitalizeFirst(withNums);
                  }
                  
                  // ---- Debounce/Locks ----
                  let alexaSyncRunning = false;
                  let pollRunning = false;
                  let debounceTimer = null;
                  
                  function triggerAlexaSyncDebounced(fullImport = false) {
                    if (debounceTimer) clearTimeout(debounceTimer);
                    debounceTimer = setTimeout(() => {
                      syncAlexaToTodoist(fullImport).catch(e => log(`[${TAG}] [Alexa→Todoist] ERROR ${e.message}`, "error"));
                    }, 800);
                  }
                  
                  // ---- Alexa -> Todoist ----
                  async function syncAlexaToTodoist(fullImport) {
                    if (alexaSyncRunning) return;
                    alexaSyncRunning = true;
                  
                    try {
                      const raw = getState(ALEXA_JSON_DP)?.val;
                      const items = parseAlexaJson(raw);
                  
                      const map = readJsonSafe(MAP_STATE, {});
                      const changed = normalizeMap(map);
                      if (changed) writeJson(MAP_STATE, map);
                  
                      for (const it of items) {
                        const alexaId = it?.id ? String(it.id) : null;
                        const valueRaw = it?.value ? String(it.value).trim() : "";
                        if (!alexaId || !valueRaw) continue;
                  
                        if (isCompletedAlexa(it)) continue;
                        if (map[alexaId]?.todoistId) continue;
                  
                        const value = normalizeTaskText(valueRaw);
                        const task = await todoistCreateTask(value, TODOIST_PROJECT_ID);
                  
                        map[alexaId] = {
                          todoistId: String(task.id),
                          value,
                          createdAt: Date.now(),
                          alexaCompletedPushed: false,
                        };
                  
                        writeJson(MAP_STATE, map);
                        log(`[${TAG}] [Alexa→Todoist] Angelegt: "${value}"`, "info");
                      }
                  
                      if (fullImport) log(`[${TAG}] [Init] Start-Import abgeschlossen.`, "info");
                    } finally {
                      alexaSyncRunning = false;
                    }
                  }
                  
                  // ---- Todoist -> Alexa ----
                  async function pollTodoistAndMirror() {
                    if (pollRunning) return;
                    pollRunning = true;
                  
                    try {
                      const map = readJsonSafe(MAP_STATE, {});
                      const changed = normalizeMap(map);
                      if (changed) writeJson(MAP_STATE, map);
                  
                      const now = Date.now();
                      const entries = Object.entries(map).filter(([, info]) => info?.todoistId && !info.alexaCompletedPushed);
                  
                      for (const [alexaId, info] of entries) {
                        const todoistId = String(info.todoistId);
                        const age = now - Number(info.createdAt || now);
                        if (age < MIN_TASK_AGE_MS) continue;
                  
                        const res = await todoistGetTask(todoistId);
                  
                        let markDone = false;
                  
                        if (res.status === 404) {
                          // Task nicht mehr abrufbar -> behandeln wir als "weg/erledigt"
                          markDone = true;
                        } else if (res.status >= 200 && res.status < 300) {
                          const t = res.data || {};
                          const checked = t.checked === true;
                          const deleted = t.is_deleted === true;
                  
                          // Wenn erledigt ODER gelöscht, dann Alexa abhaken
                          markDone = (checked || deleted);
                        } else {
                          continue;
                        }
                  
                        if (!markDone) continue;
                  
                        const completedDp = `${ALEXA_ITEMS_ROOT}.${alexaId}.completed`;
                        if (existsState(completedDp)) setState(completedDp, true, false);
                  
                        info.alexaCompletedPushed = true;
                        writeJson(MAP_STATE, map);
                  
                        log(`[${TAG}] [Todoist→Alexa] Abgehakt (TodoistId=${todoistId})`, "info");
                      }
                    } catch (e) {
                      const status = e.response?.status;
                      const body = e.response?.data ? JSON.stringify(e.response.data) : e.message;
                      log(`[${TAG}] [Poll] ERROR status=${status} body=${body}`, "error");
                    } finally {
                      pollRunning = false;
                    }
                  }
                  
                  // ---- INIT ----
                  ensureStateIfMissing(MAP_STATE, "{}", { type: "string", role: "json", read: true, write: true });
                  
                  log(`[${TAG}] START poll=${POLL_SEC}s`, "info");
                  
                  triggerAlexaSyncDebounced(true);
                  on({ id: ALEXA_JSON_DP, change: "any" }, () => triggerAlexaSyncDebounced(false));
                  schedule(`*/${POLL_SEC} * * * * *`, () => pollTodoistAndMirror());
                  
                  
                  1 Antwort Letzte Antwort
                  0
                  • grrfieldG grrfield

                    @mcBirne Es ist zwar schon einige Zeit her, aber hast Du das Skript als TypeScript eingefügt? Die Fehlermeldungen sehen nach JavaScript aus.

                    mcBirneM Offline
                    mcBirneM Offline
                    mcBirne
                    schrieb am zuletzt editiert von
                    #182

                    @grrfield sagte in Alexa Shopping List mit Bring synchronisieren:

                    @mcBirne Es ist zwar schon einige Zeit her, aber hast Du das Skript als TypeScript eingefügt? Die Fehlermeldungen sehen nach JavaScript aus.

                    nein, das wars, danke für den Tipp!

                    1 Antwort Letzte Antwort
                    0
                    • S Online
                      S Online
                      SchiefVanCleef
                      schrieb zuletzt editiert von
                      #183

                      Hallo zusammen, 🤗 nach dem ich hier viel über IO Broker in Verbindung mit Bring und der Alexa Shopping Liste gelernt habe 🤠 und immer mal wieder "tweaks" an meinem abgewandelten Script vorgenommen habe und es nun super geschmeidigt läuft, möchte ich gerne das aktuelle ALEXA BRING SCRIPT mit euch teilen und der hervorragenden Community auf diesem Weg etwas zurück geben und einfach nur DANKE 🙌 sagen.

                      Zum Script:

                      Bei der brindBaseID müsst ihr XXX wie gewohnt durch eure Bring Shopping List ID ersetzen, damit die richtige Liste befüllt wird. Ansonste alles wie immer: Alexa2.0 Adapter und bring.0 Adapter sind Voraussetzung, dass es funktioniert. Steht aber auch auskommentiert im Script.

                      // =====================================================================
                      //  Alexa Shopping List  <->  Bring!  Zwei-Wege-Sync
                      //  Engine: 3-Wege-Merge gegen persistierten Schatten + Zeitstempel-Konfliktlösung
                      //  Behandelt korrekt: Hinzufügen, Abhaken, Reaktivieren, Löschen (beidseitig)
                      // =====================================================================
                      
                      // --- KONFIGURATION: an die eigene Installation anpassen ---
                      // Bring-Listen-ID findest du in den Objekten unter "bring.0" -> Channel deiner Liste (UUID):
                      const bringBaseId  = 'bring.0.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
                      // Alexa-Einkaufsliste, bei den meisten Setups:
                      const alexa2BaseId = 'alexa2.0.Lists.SHOPPING_LIST';
                      // Voraussetzungen: Adapter "alexa2" + "bring" eingerichtet, 0_userdata.0 vorhanden.
                      
                      // Persistenter Speicher für den Schatten (0_userdata.0 existiert in modernen ioBroker-Installs)
                      const SHADOW_STATE  = '0_userdata.0.alexaBringSync.shadow';
                      
                      // Timing
                      const DEBOUNCE_MS   = 2000;   // warte nach der letzten Änderung, bevor synchronisiert wird
                      const LOCK_MS       = 5000;   // Beruhigungsfenster, bis async Schreibvorgänge der Adapter durch sind
                      
                      // Bei einem echten Konflikt (beide Seiten gleichzeitig geändert, Zeitstempel uneindeutig):
                      // true  = im Zweifel "active" gewinnen lassen (nichts verlieren, was man evtl. noch braucht)
                      const CONFLICT_PREFER_ACTIVE = true;
                      
                      // Optionaler Watchdog: warnt im Log, wenn die alexa2-Cloud-Verbindung wegbricht
                      const WATCH_ALEXA_CONNECTION = true;
                      
                      // --- ABGELEITETE IDs ---
                      const bringListId          = bringBaseId + '.content';
                      const bringListCompletedId = bringBaseId + '.recentContent';
                      const bringAddToList       = bringBaseId + '.saveItem';            // hinzufügen / aus recent reaktivieren
                      const bringCompleteItem    = bringBaseId + '.moveToRecentContent'; // abhaken (-> recent)
                      const bringRemoveItem      = bringBaseId + '.removeItem';          // komplett entfernen (content + recent)
                      const alexaAddToList       = alexa2BaseId + '.#New';
                      const alexaListId          = alexa2BaseId + '.json';
                      
                      // --- LAUFZEIT-ZUSTAND ---
                      let isSyncing   = false;
                      let pendingSync = false;
                      let syncTimeout = null;
                      let debounceTimer = null;
                      
                      // --- HILFSFUNKTIONEN ---
                      function cleanName(str) { return str ? String(str).trim().toLowerCase() : ''; }
                      function formatName(str) {
                          if (!str) return '';
                          str = String(str).trim();
                          return str.charAt(0).toUpperCase() + str.slice(1);
                      }
                      
                      function loadShadow() {
                          try { const s = getState(SHADOW_STATE); return (s && s.val) ? JSON.parse(s.val) : {}; }
                          catch (e) { log('Schatten nicht lesbar, starte leer: ' + e, 'warn'); return {}; }
                      }
                      function saveShadow(sh) { setState(SHADOW_STATE, JSON.stringify(sh), true); }
                      
                      // Konfliktlösung: jüngerer Zeitstempel gewinnt, sonst konfigurierte Vorzugsregel
                      function resolveConflict(aStatus, bStatus, aTs, bTs, displayName) {
                          let winner;
                          if (aTs > bTs)      winner = aStatus;
                          else if (bTs > aTs) winner = bStatus;
                          else                winner = CONFLICT_PREFER_ACTIVE
                                                        ? ((aStatus === 'active' || bStatus === 'active') ? 'active' : 'done')
                                                        : ((aStatus === 'done'   || bStatus === 'done')   ? 'done'   : 'active');
                          log(`[KONFLIKT] "${displayName}": Alexa=${aStatus}(${aTs}) vs Bring=${bStatus}(${bTs}) -> ${winner}`, 'warn');
                          return winner;
                      }
                      
                      function finishLock() {
                          if (syncTimeout) clearTimeout(syncTimeout);
                          syncTimeout = setTimeout(() => {
                              isSyncing = false;
                              if (pendingSync) { pendingSync = false; triggerSync(); } // Nachlauf für während der Sperre Eingetroffenes
                          }, LOCK_MS);
                      }
                      
                      // --- HAUPTLOGIK ---
                      function doSync() {
                          if (isSyncing) { pendingSync = true; return; }
                          isSyncing = true;
                      
                          try {
                              const aState = getState(alexaListId);
                              const bState = getState(bringListId);
                              const rState = getState(bringListCompletedId);
                      
                              if (!aState || aState.val == null || !bState || bState.val == null) { finishLock(); return; }
                      
                              const alexaList   = JSON.parse(aState.val);
                              const bringActive = JSON.parse(bState.val);
                              const bringRecent = (rState && rState.val) ? JSON.parse(rState.val) : [];
                      
                              // grobe Bring-Zeitstempel (Listenebene) nur für die Konfliktlösung
                              const bringContentLc = bState.lc || bState.ts || 0;
                              const bringRecentLc  = (rState && (rState.lc || rState.ts)) || 0;
                      
                              // --- Maps der aktuellen Zustände ---
                              const alexaByName = {};   // active gewinnt bei Namens-Duplikaten
                              alexaList.forEach(it => {
                                  const k = cleanName(it.value); if (!k) return;
                                  const st = it.completed ? 'done' : 'active';
                                  if (!alexaByName[k] || st === 'active') alexaByName[k] = it;
                              });
                              const bringActiveMap = {}; bringActive.forEach(it => { const k = cleanName(it.name); if (k) bringActiveMap[k] = it; });
                              const bringRecentMap = {}; bringRecent.forEach(it => { const k = cleanName(it.name); if (k) bringRecentMap[k] = it; });
                      
                              const shadow    = loadShadow();
                              const newShadow = {};
                      
                              const names = new Set([
                                  ...Object.keys(alexaByName), ...Object.keys(bringActiveMap),
                                  ...Object.keys(bringRecentMap), ...Object.keys(shadow)
                              ]);
                      
                              names.forEach(name => {
                                  const aItem   = alexaByName[name];
                                  const aStatus = aItem ? (aItem.completed ? 'done' : 'active') : 'absent';
                                  const bStatus = bringActiveMap[name] ? 'active' : (bringRecentMap[name] ? 'done' : 'absent');
                                  const prev    = shadow[name] || { a: 'absent', b: 'absent', c: 'absent' };
                      
                                  const aChanged = aStatus !== prev.a;
                                  const bChanged = bStatus !== prev.b;
                      
                                  const displayName = (bringActiveMap[name] || bringRecentMap[name] || {}).name
                                                      || (aItem ? aItem.value : null) || prev.name || formatName(name);
                      
                                  const aTs = aItem ? (aItem.updatedDateTime || 0) : 0;
                                  const bTs = (bStatus === 'active') ? bringContentLc : (bStatus === 'done' ? bringRecentLc : 0);
                      
                                  // --- Zielzustand T bestimmen ---
                                  let T;
                                  if      (aChanged && !bChanged) T = aStatus;
                                  else if (bChanged && !aChanged) T = bStatus;
                                  else if (aChanged &&  bChanged) T = (aStatus === bStatus) ? aStatus
                                                                        : resolveConflict(aStatus, bStatus, aTs, bTs, displayName);
                                  else                            T = prev.c; // nichts geändert -> Kanon halten
                                  if (!T) T = (aStatus !== 'absent') ? aStatus : bStatus;
                      
                                  // --- Alexa angleichen ---
                                  let aResult = aStatus;
                                  if (T === 'active') {
                                      if (aStatus === 'absent') { log(`[ALEXA +]          ${displayName}`); setState(alexaAddToList, displayName); aResult = 'active'; }
                                      else if (aStatus === 'done') { log(`[ALEXA reaktiv.]    ${displayName}`); setState(`${alexa2BaseId}.items.${aItem.id}.completed`, false); aResult = 'active'; }
                                  } else if (T === 'done') {
                                      if (aStatus === 'active') { log(`[ALEXA abgehakt]    ${displayName}`); setState(`${alexa2BaseId}.items.${aItem.id}.completed`, true); aResult = 'done'; }
                                      // aStatus 'absent' -> erledigtes Item lässt sich in Alexa nicht erzeugen, bleibt absent
                                  } else if (T === 'absent') {
                                      if (aStatus !== 'absent') { log(`[ALEXA gelöscht]    ${displayName}`); setState(`${alexa2BaseId}.items.${aItem.id}.#delete`, true); aResult = 'absent'; }
                                  }
                      
                                  // --- Bring angleichen ---
                                  let bResult = bStatus;
                                  if (T === 'active') {
                                      if (bStatus !== 'active') { log(`[BRING +]           ${displayName}`); setState(bringAddToList, displayName); bResult = 'active'; }
                                  } else if (T === 'done') {
                                      if (bStatus === 'active') { log(`[BRING abgehakt]    ${displayName}`); setState(bringCompleteItem, displayName); bResult = 'done'; }
                                      // bStatus 'absent' -> kann nicht als erledigt erzeugt werden, bleibt absent
                                  } else if (T === 'absent') {
                                      if (bStatus !== 'absent') { log(`[BRING gelöscht]    ${displayName}`); setState(bringRemoveItem, displayName); bResult = 'absent'; }
                                  }
                      
                                  // --- neuen Schatten schreiben (komplett verschwundene Items fallen raus) ---
                                  if (!(aResult === 'absent' && bResult === 'absent')) {
                                      newShadow[name] = { a: aResult, b: bResult, c: T, name: displayName, ts: Date.now() };
                                  }
                              });
                      
                              saveShadow(newShadow);
                      
                          } catch (e) {
                              log('Fehler im Sync-Skript: ' + e, 'error');
                          }
                          finishLock();
                      }
                      
                      // --- TRIGGER mit Debounce ---
                      function triggerSync() {
                          if (debounceTimer) clearTimeout(debounceTimer);
                          debounceTimer = setTimeout(doSync, DEBOUNCE_MS);
                      }
                      
                      on({ id: bringListId,          change: 'ne' }, triggerSync);
                      on({ id: bringListCompletedId, change: 'ne' }, triggerSync);
                      on({ id: alexaListId,          change: 'ne' }, triggerSync);
                      
                      if (WATCH_ALEXA_CONNECTION) {
                          on({ id: 'alexa2.0.info.connection', change: 'ne' }, obj => {
                              if (obj.state.val === false) log('alexa2-Cloud-Verbindung verloren – Sync evtl. inaktiv bis zum Reconnect.', 'warn');
                          });
                      }
                      
                      // --- START: Schatten-State sicherstellen, dann initial abgleichen ---
                      createState(SHADOW_STATE, '{}', false, { name: 'Alexa-Bring Sync Shadow', type: 'string', read: true, write: true }, () => {
                          log('Alexa <-> Bring Sync (Shadow-Merge) gestartet. Initialer Abgleich …');
                          triggerSync();
                      });
                      

                      Das Script müsste ihr einfach als JS Script in IO Broker kopieren und starten. Der Rest funktioniert dann automatisch. WICHTIG NOCHMAL: Die Bring Base ID bzw. der Platzhalter XXX mit eurer echten Listen ID ersetzen.

                      DANKE nochmal an die gesamte Community, mit deren Hilfe dieses angepasste und für meine Zwecke hervorragend funktionierende Script entstanden ist. KUDOS an alle die ihre Scripts geteilt haben. 🤙 👏

                      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

                      538

                      Online

                      32.9k

                      Benutzer

                      83.1k

                      Themen

                      1.3m

                      Beiträge
                      Community
                      Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                      ioBroker Community 2014-2025
                      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