Skip to content
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • 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

  • Monatsrückblick Januar/Februar 2026 ist online!
    BluefoxB
    Bluefox
    15
    1
    213

  • Jahresrückblick 2025 – unser neuer Blogbeitrag ist online! ✨
    BluefoxB
    Bluefox
    17
    1
    4.4k

  • Neuer Blogbeitrag: Monatsrückblick - Dezember 2025 🎄
    BluefoxB
    Bluefox
    13
    1
    1.3k

Alexa Shopping List mit Bring synchronisieren

Geplant Angeheftet Gesperrt Verschoben Skripten / Logik
182 Beiträge 31 Kommentatoren 38.1k Aufrufe 32 Watching
  • Ä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 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
      Antworten
      • In einem neuen Thema antworten
      Anmelden zum Antworten
      • Älteste zuerst
      • Neuste zuerst
      • Meiste Stimmen


      Support us

      ioBroker
      Community Adapters
      Donate

      561

      Online

      32.7k

      Benutzer

      82.4k

      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