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

  • wichtiges UPDATE für controller 7.2.2 im stable
    HomoranH
    Homoran
    10
    1
    2.4k

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    16
    1
    4.3k

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

Alexa Shopping List mit Bring synchronisieren

Geplant Angeheftet Gesperrt Verschoben Skripten / Logik
187 Beiträge 32 Kommentatoren 52.2k 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.
  • 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 Offline
                  S Offline
                  SchiefVanCleef
                  schrieb am 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
                  • I Offline
                    I Offline
                    IOMax
                    schrieb am zuletzt editiert von
                    #184

                    @schiefvancleef danke, eine Umsetzung auf Groß/Klein wäre noch super

                    1 Antwort Letzte Antwort
                    0
                    • LocosL Offline
                      LocosL Offline
                      Locos
                      schrieb am zuletzt editiert von Locos
                      #185

                      Hi zusammen,

                      ich habe mir ein eigenes schlankes Skript gebaut, weil ich Alexa einfach nur als reines Spracheingabe-Mikrofon für meine Bring!-Liste nutzen will. Nach intensiven Tests im Alltag wurde der Code noch einmal überarbeitet und optimiert.

                      UPDATE (02.07.2026):

                      Was ist neu?

                      • 🚀 Multi-Item-Support (Großeinkäufe):
                        Sagt ihr z. B. "Alexa, füge Äpfel und Bananen hinzu", zerschneidet das Skript den Satz ab sofort automatisch bei jedem "und". Es schickt getrennte Artikel an Bring! und löscht auch alle einzeln bei Alexa.

                      • 🧮 Intelligenter Mengen-Addierer:
                        Wenn ihr schon "2 Flaschen Bier" auf der Liste habt und später "Schreibe zwei Flaschen Bier auf die Liste" sagt, liest das Skript die aktuelle Bring-Liste aus und rechnet zusammen ➔ Es landen "Bier, 4 Flaschen" in der App!

                      • 📦 Riesiges Verpackungs-Wörterbuch:
                        Der Filter erkennt jetzt auch unzählige Haushalts-Einheiten wie Packungen, Dosen, Gläser, Rollen, Tuben, Kisten und Kartons.

                      • ⏱️ Anti-Verschluck-Garantie (Staggering):
                        Werden mehrere Artikel gleichzeitig angesagt, schickt das Skript diese nun mit 1,5 Sekunden Abstand an den Adapter. So geht garantiert nichts mehr verloren.

                      • 🛠️ Feinschliff & Bugfixes:
                        Das Skript reagiert nun auch auf wiederholte, identische Sätze (change: "any"), erkennt Trigger-Wörter wie "hinzu" und löscht hängende Cloud-Warteschleifen beim Skript-Neustart sauber auf.

                      Prinzip:

                      • Erkennen:
                        Ihr sagt: "Alexa, pack drei Bananen auf die Einkaufsliste". Das Skript reagiert auf den neuen Text in eurem Alexa-Verlauf (alexa2.0.History.summary).

                      • Eintragen:
                        Das Skript schnappt sich den Artikel, formatiert ihn und schickt ihn sofort an euren Bring!-Adapter.

                      • Löschen (Hier kommt die Alexa-Liste ins Spiel):
                        Da Alexa den Artikel parallel natürlich trotzdem auf ihre eigene Liste setzt, wartet das Skript ein paar Sekunden, bis die Amazon-Cloud synchronisiert hat. Dann durchsucht es das JSON eurer echten Alexa-Einkaufsliste (alexa2.0.Lists.SHOP.json), sucht dort nach den "Bananen", schnappt sich die von Amazon vergebene Artikel-ID und löscht den Eintrag direkt von der Alexa-Liste.

                      • Ergebnis:
                        Das Produkt landet perfekt in eurer Bring!-App und eure Alexa-Liste wird im Hintergrund automatisch bereinigt.

                      Funktionen:

                      • Mathematischer Zahlen-Gesamt-Parser:
                        Macht aus gesprochenen Wörtern echte Zahlen. Die Logik basiert auf der genialen Grundidee des Users Heimweh, wurde von uns aber komplett neu aufgebaut, damit nun auch komplexe, zusammengesetzte Zahlenketten (z. B. "zwei hundert fünfzig gramm butter" ➔ 250) im selben Durchlauf fehlerfrei aufaddiert und zusammengefasst werden!

                      • Bruchzahlen-Erkennung:
                        Unterstützt jetzt auch native Bruchteile wie "ein halbes Kilo" (➔ 0.5Kg) oder "ein viertel Pfund".

                      • Automatische Groß-/Kleinschreibung:
                        Der Produktname in Bring! wird mit einem großen Anfangsbuchstaben übernommen (z. B. "frische milch" ➔ "Frische milch").

                      • Saubere Mengentrennung:
                        Zahlen und Einheiten am Anfang werden erkannt und so an den Bring!-Adapter übergeben, dass sie in der Bring!-App perfekt getrennt im Feld specification (Menge) und name (Produkt) landen. Kein Textmatsch!

                      • Cloud-Warteschleife (Dynamic Retry):
                        Weil die Amazon-Cloud manchmal pennt und ein paar Sekunden braucht, um das JSON im ioBroker zu aktualisieren, prüft mein Skript bis zu 5-mal alle 3 Sekunden nach, ob der Artikel da ist. Erst wenn er im JSON auftaucht, wird er per .#delete-Befehl weggebeamt.

                      • Error-Handling & Logging:
                        Jede Texttransformation und Regex-Filterung ist einzeln per try-catch abgesichert. Sollte die Spracherkennung von Alexa mal totalen Kauderwelsch liefern, stürzt das Skript nicht mehr ab, sondern loggt den Fehler sauber und schickt den Artikel im Notfall-Fallback einfach als Rohtext an Bring!.

                      • Schont die Ressourcen:
                        Kein permanentes JSON-Dauer-Polling im Hintergrund. Das Skript springt nur an, wenn ich auch wirklich was ansage.

                      Voraussetzungen:
                      JavaScript-Adapter, alexa2-Adapter (Listenfunktion aktiv), bring-Adapter.

                      Code (Einfach als neues JavaScript (JS) anlegen, eure eigene Bring-ID oben eintragen und fertig!):

                      /**
                       * Alexa Voice-Input to Bring! List Pushing Engine
                       * 
                       * Erkennt Sprachbefehle aus der Alexa-History, wandelt Zahlwörter um,
                       * trennt Mengen sauber ab (z.B. "3 Erdbeeren" -> "Erdbeeren, 3" für Bring!),
                       * teilt Aufzählungen ("und", ",") in einzelne Artikel auf,
                       * addiert Mengen bestehender Artikel intelligent zusammen,
                       * formatiert den Artikel und löscht ihn anschließend sofort von Alexa.
                       */
                       
                      // --- KONFIGURATION ---
                       
                      // ID des Datenpunkts für die Alexa History Zusammenfassung und Antworttext
                      const alexaHistorySummaryId = 'alexa2.0.History.summary';
                      const alexaHistoryAnswerId = 'alexa2.0.History.answerText';
                       
                      // Basis-ID deiner Bring! Adapter-Instanz (HIER DEINE EIGENE ID EINTRAGEN)
                      const bringBaseId = 'bring.0.deine-bring-liste-id-hier'; 
                      const bringAddItemStateId = bringBaseId + '.saveItem';
                       
                      // Pfad zur Alexa-Einkaufsliste (Basis und JSON)
                      const alexa2BaseId = 'alexa2.0.Lists.SHOP'; 
                      const alexaShoppingListJsonId = `${alexa2BaseId}.json`;
                       
                      // Schalter für Debug-Ausgaben im ioBroker-Log
                      const printDebug = true;
                      
                      // Array für laufende Timeouts (verhindert Fehler bei Skript-Neustart)
                      let activeTimeouts = [];
                       
                       
                      // --- HILFSFUNKTIONEN ---
                       
                      function debug(msg) {
                          if (printDebug) {
                              log('[Alexa->Bring Push] ' + msg);
                          }
                      }
                       
                      // Schneidet führende Alexa-Befehlswörter ab
                      function stripPrefixes(text) {
                          if (!text) return '';
                          const prefixesToRemove = [
                              /^(?:hey\s+)?alexa,\s*/i,
                              /^(?:hey\s+)?alexa\s+/i,
                              /^füge\s+/i, /^setz\s+/i, /^setze\s+/i, /^packe\s+/i, /^pack\s+/i, 
                              /^schreib\s+/i, /^schreibe\s+/i, /^notiere\s+/i
                          ];
                          let result = String(text).trim();
                          let changed = true;
                          
                          while (changed) {
                              let startLen = result.length;
                              for (const prefixRegex of prefixesToRemove) {
                                  result = result.replace(prefixRegex, '').trim();
                              }
                              changed = result.length !== startLen;
                          }
                          return result;
                      }
                      
                      // Prüft alte und neue Mengen und addiert sie, falls die Einheiten passen
                      function mergeQuantities(oldSpec, newSpec) {
                          if (!oldSpec) return newSpec;
                          if (!newSpec) return oldSpec;
                      
                          const regex = /^(\d+(?:[.,]\d+)?)\s*(.*)$/i;
                          const matchOld = String(oldSpec).match(regex);
                          const matchNew = String(newSpec).match(regex);
                      
                          if (matchOld && matchNew) {
                              let numOld = parseFloat(matchOld[1].replace(',', '.'));
                              let numNew = parseFloat(matchNew[1].replace(',', '.'));
                              let unitOld = matchOld[2].trim().toLowerCase();
                              let unitNew = matchNew[2].trim().toLowerCase();
                      
                              // Wenn die Einheiten gleich sind (oder eine fehlt), wird addiert
                              if (unitOld === unitNew || unitOld === '' || unitNew === '') {
                                  let total = numOld + numNew;
                                  let unitToUse = matchNew[2].trim() || matchOld[2].trim();
                                  return unitToUse ? `${total} ${unitToUse}` : `${total}`;
                              } else {
                                  // Wenn Einheiten unterschiedlich sind (z.B. "500 g" und "1 Packung"),
                                  // hängen wir sie einfach mit einem "+" zusammen
                                  return `${oldSpec} + ${newSpec}`;
                              }
                          }
                          return newSpec; // Fallback, falls keine Zahlen gefunden wurden
                      }
                       
                      // Zuverlässiger Parser für deutsche Zahlwörter
                      function wordsToNumbersSmart(text) {
                          if (!text) return '';
                          try {
                              let t = String(text).toLowerCase().replace(/\s+/g, ' ');
                       
                              const ones = {
                                  "null": 0, "eins": 1, "eine": 1, "einen": 1, "ein": 1, "milli": 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"];
                       
                              const words = t.split(/\s+/).filter(Boolean);
                              const out = [];
                              
                              let currentNumber = 0;
                              let hasNumber = false;
                       
                              for (let i = 0; i < words.length; i++) {
                                  const w = words[i];
                       
                                  if (ones[w] !== undefined) {
                                      currentNumber += ones[w];
                                      hasNumber = true;
                                  } else if (tens[w] !== undefined) {
                                      currentNumber += tens[w];
                                      hasNumber = true;
                                  } else if (multipliers[w] !== undefined) {
                                      if (currentNumber === 0) currentNumber = 1;
                                      currentNumber *= multipliers[w];
                                      hasNumber = true;
                                  } else if (!isNaN(Number(w))) {
                                      if (hasNumber) {
                                          out.push(String(currentNumber));
                                          currentNumber = 0;
                                          hasNumber = false;
                                      }
                                      out.push(w);
                                  } else if (skipWords.includes(w) && hasNumber) {
                                      continue;
                                  } else {
                                      if (hasNumber) {
                                          out.push(String(currentNumber));
                                          currentNumber = 0;
                                          hasNumber = false;
                                      }
                                      if (out.length === 0 || !isNaN(Number(out[out.length - 1]))) {
                                          out.push(w.charAt(0).toUpperCase() + w.slice(1));
                                      } else {
                                          out.push(w);
                                      }
                                  }
                              }
                       
                              if (hasNumber) {
                                  out.push(String(currentNumber));
                              }
                       
                              let resultString = out.join(" ");
                       
                              // Brüche umwandeln
                              resultString = resultString.replace(/\b(1\s+)?halbe?s?\b/gi, '0.5');
                              resultString = resultString.replace(/\b(1\s+)?viertel\b/gi, '0.25');
                       
                              return resultString;
                          } catch (err) {
                              log(`Fehler in wordsToNumbersSmart bei Text "${text}": ${err.message}`, 'warn');
                              return String(text);
                          }
                      }
                       
                      // Bereinigt den Text, trennt Mengen ab und sorgt für korrekte Groß-/Kleinschreibung
                      function ListCleaner(Eintrag = '') {
                          if (!Eintrag) return { formatted: '', product: '', quantity: '' };
                          
                          try {
                              let textWithNumbers = wordsToNumbersSmart(Eintrag);
                              if (!textWithNumbers || typeof textWithNumbers !== 'string') {
                                  textWithNumbers = String(Eintrag);
                              }
                              
                              textWithNumbers = textWithNumbers.trim();
                      
                              const quantityRegex = /^(\d+(?:[.,]\d+)?)(?:\s*(mal|x|stck|stück|l|ltr|liter|kg|kilo|g|gramm|pfund|flasche|flaschen|packung|packungen|päckchen|dose|dosen|becher|glas|gläser|rolle|rollen|tube|tuben|tafel|tafeln|kiste|kisten|karton|kartons))?\s+(.+)$/i;
                              const match = textWithNumbers.match(quantityRegex);
                              
                              if (match && match[1] && match[3]) {
                                  let quantity = String(match[1]).trim();
                                  let unit = match[2] ? String(match[2]).trim().toLowerCase() : '';
                                  let product = String(match[3]).trim();
                                  
                                  if (product.length > 0) {
                                      product = product.charAt(0).toUpperCase() + product.slice(1);
                                      
                                      if (['kilo', 'kg'].includes(unit)) {
                                          unit = 'kg';
                                      } else if (['gramm', 'g'].includes(unit)) {
                                          unit = 'g';
                                      } else if (['liter', 'l', 'ltr'].includes(unit)) {
                                          unit = 'l';
                                      } else if (['stck', 'stück'].includes(unit)) {
                                          unit = 'Stück';
                                      } else if (['mal', 'x'].includes(unit)) {
                                          unit = ''; 
                                      } else if (unit) {
                                          unit = unit.charAt(0).toUpperCase() + unit.slice(1);
                                      }
                                      
                                      let finalQuantity = unit ? `${quantity} ${unit}` : quantity;
                                      
                                      debug(`Menge erkannt: "${finalQuantity}", Produkt: "${product}" -> Formatiere für Bring als "${product}, ${finalQuantity}"`);
                                      return { formatted: `${product}, ${finalQuantity}`, product: product, quantity: finalQuantity };
                                  }
                              }
                              
                              let normProduct = textWithNumbers.charAt(0).toUpperCase() + textWithNumbers.slice(1);
                              return { formatted: normProduct, product: normProduct, quantity: '' };
                          } catch (err) {
                              log(`Fehler in ListCleaner bei Eintrag "${Eintrag}": ${err.message}`, 'error');
                              let fallbackProduct = String(Eintrag).charAt(0).toUpperCase() + String(Eintrag).slice(1);
                              return { formatted: fallbackProduct, product: fallbackProduct, quantity: '' };
                          }
                      }
                       
                      // Intelligente Löschfunktion mit Cloud-Warteschleife und Multi-Match-Logik
                      function removeItemFromAlexaList(itemName, extractedProduct, attempt = 1) {
                          const maxAttempts = 5; 
                          const retryDelay = 3000; 
                       
                          let cleanedAlexaName = stripPrefixes(itemName);
                          debug(`Versuche, "${cleanedAlexaName}" (Produkt-Match: "${extractedProduct}") von der Alexa-Liste zu entfernen (Versuch ${attempt}/${maxAttempts}).`);
                          
                          try {
                              const listState = getState(alexaShoppingListJsonId);
                              
                              if (!listState || !listState.val) {
                                  debug("Alexa Einkaufslisten-JSON ist leer. Warte auf Adapter-Update...");
                                  if (attempt < maxAttempts) {
                                      let timer = setTimeout(() => removeItemFromAlexaList(itemName, extractedProduct, attempt + 1), retryDelay);
                                      activeTimeouts.push(timer);
                                  }
                                  return;
                              }
                       
                              const shoppingList = JSON.parse(listState.val);
                              let itemIdToDelete = null;
                       
                              const search1 = cleanedAlexaName.toLowerCase();
                              const search2 = extractedProduct.toLowerCase();
                       
                              for (const item of shoppingList) {
                                  if (item && item.value) {
                                      const valLower = item.value.toLowerCase();
                                      if (valLower === search1 || valLower === search2 || search1.includes(valLower) || valLower.includes(search2)) {
                                          itemIdToDelete = item.id;
                                          cleanedAlexaName = item.value; 
                                          break;
                                      }
                                  }
                              }
                       
                              if (itemIdToDelete) {
                                  const deleteStateId = `${alexa2BaseId}.items.${itemIdToDelete}.#delete`;
                                  debug(`Artikel-ID gefunden. Sende Löschbefehl an: ${deleteStateId}`);
                                  setState(deleteStateId, true);
                                  debug(`Löschbefehl für "${cleanedAlexaName}" erfolgreich an Amazon übermittelt.`);
                              } else {
                                  if (attempt < maxAttempts) {
                                      debug(`Artikel "${cleanedAlexaName}" / "${extractedProduct}" noch nicht im JSON. Cloud synchronisiert noch... Erneuter Versuch in 3 Sek.`);
                                      let timer = setTimeout(() => removeItemFromAlexaList(itemName, extractedProduct, attempt + 1), retryDelay);
                                      activeTimeouts.push(timer);
                                  } else {
                                      debug(`Artikel wurde nach ${maxAttempts} Versuchen nicht im JSON gefunden.`);
                                  }
                              }
                       
                          } catch (e) {
                              log(`Fehler beim Entfernen des Artikels von der Alexa-Liste: ${e}`, 'error');
                          }
                      }
                       
                       
                      // --- HAUPTLOGIK ---
                       
                      on({ id: alexaHistorySummaryId, change: "any", ack: true }, function (obj) {
                          try {
                              if (!obj.state || !obj.state.val || typeof obj.state.val !== 'string') return;
                       
                              const summaryTextFull = obj.state.val;
                              const summaryText = summaryTextFull.toLowerCase();
                              
                              const alexaAnswer = getState(alexaHistoryAnswerId)?.val || 'Keine Antwort aufgezeichnet';
                              debug(`Neuer History Summary Eintrag: "${summaryTextFull}" | Alexa Antwort: "${alexaAnswer}"`);
                       
                              // Trigger-Phrasen inklusive "hinzu" aus dem Forum
                              const triggerPhrases = [' auf die einkaufsliste', ' zur einkaufsliste', ' hinzu', ' hinzufügen'];
                              let itemFound = false;
                              let itemNameRaw = '';
                       
                              for (const phrase of triggerPhrases) {
                                  const index = summaryText.lastIndexOf(phrase);
                                  if (index !== -1 && index > 0) {
                                      itemNameRaw = summaryTextFull.substring(0, index).trim();
                                      itemNameRaw = stripPrefixes(itemNameRaw);
                                      itemFound = true;
                                      break;
                                  }
                              }
                       
                              if (itemFound && itemNameRaw) {
                                  // 1. Text bei Komma oder " und " aufteilen für Multi-Item-Support
                                  const itemsToProcess = itemNameRaw.split(/,\s*|\s+und\s+/i).filter(Boolean);
                                  
                                  debug(`Multi-Item-Erkennung: Satz wurde in ${itemsToProcess.length} Artikel geteilt.`);
                      
                                  // 2. Jeden Artikel einzeln verarbeiten
                                  itemsToProcess.forEach((singleItemRaw, index) => {
                                      const cleanResult = ListCleaner(singleItemRaw);
                                      const cleanedItemName = cleanResult.formatted; 
                                      const originalItemName = singleItemRaw.trim();            
                      
                                      if (cleanedItemName) {
                                          
                                          // --- NEU: Bring! JSON prüfen und Mengen addieren ---
                                          let finalItemNameToBring = cleanedItemName;
                                          
                                          try {
                                              const bringContentState = getState(bringBaseId + '.content');
                                              if (bringContentState && bringContentState.val) {
                                                  let bringList = typeof bringContentState.val === 'string' ? JSON.parse(bringContentState.val) : bringContentState.val;
                                                  
                                                  // Sucht, ob das Produkt schon auf der Bring!-Liste steht
                                                  let existingItem = bringList.find(i => i.name.toLowerCase() === cleanResult.product.toLowerCase());
                                                  
                                                  if (existingItem && existingItem.specification) {
                                                      let mergedSpec = mergeQuantities(existingItem.specification, cleanResult.quantity);
                                                      finalItemNameToBring = `${cleanResult.product}, ${mergedSpec}`;
                                                      debug(`Artikel "${cleanResult.product}" existiert bereits. Addiere Menge: "${existingItem.specification}" + "${cleanResult.quantity}" -> "${mergedSpec}"`);
                                                  }
                                              }
                                          } catch (e) {
                                              debug(`Fehler beim Auslesen der bestehenden Bring-Liste: ${e.message}`);
                                          }
                                          // ---------------------------------------------------
                      
                                          // 3. Gestaffelte Ausführung (1.5 Sekunden Abstand pro Artikel),
                                          // damit der Bring! Adapter sich nicht verschluckt.
                                          let processingDelay = index * 1500; 
                      
                                          let processTimer = setTimeout(() => {
                                              debug(`[Artikel ${index + 1}/${itemsToProcess.length}] Roh: "${originalItemName}" -> Bring!: "${finalItemNameToBring}".`);
                                              
                                              // Schritt 1: Zu Bring! hinzufügen (mit eventuell addierter Menge)
                                              setState(bringAddItemStateId, finalItemNameToBring, false);
                                              
                                              // Schritt 2: Verzögertes Löschen auf Alexa-Seite triggern
                                              let deleteTimer = setTimeout(() => {
                                                  removeItemFromAlexaList(originalItemName, cleanResult.product);
                                              }, 4000);
                                              activeTimeouts.push(deleteTimer);
                      
                                          }, processingDelay);
                                          activeTimeouts.push(processTimer);
                       
                                      } else {
                                          debug(`Artikel "${originalItemName}" ist nach Bereinigung leer. Ignoriere.`);
                                      }
                                  });
                              }
                          } catch (globalErr) {
                              log(`Unerwarteter globaler Fehler in der Hauptschleife: ${globalErr.message}`, 'error');
                          }
                      });
                      
                      
                      // --- CLEANUP BEIM BEENDEN ---
                      onStop(function () {
                          activeTimeouts.forEach(timer => clearTimeout(timer));
                          activeTimeouts = [];
                          debug("Skript gestoppt. Alle laufenden Timeouts wurden sicher beendet.");
                      }, 10);
                       
                       
                      // --- SKRIPTSTART ---
                      debug("Skript gestartet. Multi-Item-Parser, Mengen-Addierer und erweiterte Triggerwörter aktiv.");
                      
                      1 Antwort Letzte Antwort
                      0
                      • mcBirneM Offline
                        mcBirneM Offline
                        mcBirne
                        schrieb am zuletzt editiert von mcBirne
                        #186

                        Super, vielen Dank! Klappt super bei mir.
                        Aber mir ist aufgefallen, dass es nur funktioniert wenn ich sage:
                        "packe 3 Äpfel auf die Eingaufsliste". Wenn ich sage: "füge 3 Äpel hinzu" passiert leider nichts. Alexa packt sie auf die internen Einkaufsliste aber das Skript reagiert nicht. Gibt es eine Möglichgeit auch das noch abzugreifen?
                        Habe es gefunden.
                        Zeile 252 sieht bei mir jetzt so aus:

                        const triggerPhrases = [' auf die einkaufsliste', ' zur einkaufsliste',' hinzu',' hinzufügen'];
                        

                        Werde es weiter testen.

                        LocosL 1 Antwort Letzte Antwort
                        1
                        • mcBirneM mcBirne

                          Super, vielen Dank! Klappt super bei mir.
                          Aber mir ist aufgefallen, dass es nur funktioniert wenn ich sage:
                          "packe 3 Äpfel auf die Eingaufsliste". Wenn ich sage: "füge 3 Äpel hinzu" passiert leider nichts. Alexa packt sie auf die internen Einkaufsliste aber das Skript reagiert nicht. Gibt es eine Möglichgeit auch das noch abzugreifen?
                          Habe es gefunden.
                          Zeile 252 sieht bei mir jetzt so aus:

                          const triggerPhrases = [' auf die einkaufsliste', ' zur einkaufsliste',' hinzu',' hinzufügen'];
                          

                          Werde es weiter testen.

                          LocosL Offline
                          LocosL Offline
                          Locos
                          schrieb am zuletzt editiert von
                          #187

                          @mcBirne danke für dein Feedback. habe das Skript nochmal etwas überarbeitet. Siehe vorherigen Post.

                          Liebe Grüße

                          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

                          260

                          Online

                          33.0k

                          Benutzer

                          83.4k

                          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