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. JavaScript
  5. Kontakte (Cards)

NEWS

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    11
    1
    568

  • 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
    872

Kontakte (Cards)

Geplant Angeheftet Gesperrt Verschoben JavaScript
javascriptmultimediatemplate
4 Beiträge 2 Kommentatoren 69 Aufrufe 4 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.
  • Ro75R Online
    Ro75R Online
    Ro75
    schrieb zuletzt editiert von Ro75
    #1

    In unserem Dashboard stehen alle wichtigen Kontakte als moderne, dynamische Karten bereit – so hat jedes Familienmitglied jederzeit Zugriff auf die aktuellsten Informationen, ohne suchen zu müssen.
    Die Daten liegen zentral in einer JSON, aus der automatisch schicke SVG‑Karten erzeugt werden, die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.

    Sobald ein Eintrag geändert oder ein neuer Kontakt hinzugefügt wird, aktualisiert das System die Karten vollautomatisch.
    Keine Pflege an mehreren Stellen, keine veralteten Infos – einmal eintragen, überall aktuell.

    Das Script erzeugt beim ersten Start automatisch einen kompletten Beispieldatensatz mit drei fertigen Karten – inklusive der passenden SVG‑Grafiken.
    So sieht man sofort, wie das System funktioniert und wie die Karten später in der Visualisierung aussehen.

    card.png

    HINWEIS: Ein Teil des Codes wurde dabei mit Unterstützung einer KI (Microsoft Copilot) entwickelt, um die Erstellung noch effizienter und moderner zu gestalten.

    VORAUSSETZUNGEN:

    • JS-Adapter: 9.0.18
    • Node-JS: 22.22.3

    Hier der Code:

    //********** START KONFIGURATION **********
    const root     = "0_userdata.0.Kontakte."
    const JSON_DP  = root + "json";
    const SVG_ROOT = root + "svg.";
    
    // zentrale SVG-Größen
    const CARD_LARGE_W = 350;
    const CARD_LARGE_H = 360;
    
    const CARD_SMALL_W = 350;
    const CARD_SMALL_H = 130;
    
    const AVATAR = {
        aura: {
            inner: {color: "#6E463E", opacity: 0.22}, // stärkster innerer Aura-Kern
            mid:   {color: "#6E463E", opacity: 0.08}, // mittlere Aura
            outer: {color: "#6E463E", opacity: 0.00}  // äußerer Rand, auslaufend
        },
    
        glass: {
            core:  {color: "#ffffff", opacity: 1.00}, // hellster Kern
            glow1: {color: "#6E463E", opacity: 0.92}, // starker Glow
            glow2: {color: "#6E463E", opacity: 0.48}, // mittlerer Glow
            glow3: {color: "#6E463E", opacity: 0.12}, // schwacher Glow
            fade:  {color: "#6E463E", opacity: 0.00}  // auslaufender Rand
        },
    
        glowFactorSmall: 1.25,           // Glow‑Dunkelheits‑Faktor NUR für kleine Karte
        ring: "rgba(255,255,255,0.30)",  // äußerer Ring
        background: "#0f2647"            // Avatar-Hintergrund
    };
    
    //Beispieldaten json - als Base64 - für weniger Platz
    const SAMPLE_DATA_BASE64 =
    `ewogICJlaW50cmFlZ2UiOiBbCiAgICB7CiAgICAgICJpZCI6IDEsCiAgICAgICJrYXJ0ZSI6ICJsYXJnZSIsCiAgICAgICJ0eXAiOiAiaGF1c2FyenQiLAogICAgICAi
    bmFtZSI6ICJIYXVzYXJ6dCDigJMgRHIuIE1heCBNdXN0ZXJtYW5uIiwKICAgICAgImJpbGQiOiAiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvYXJ6dC8zMDAvMzAw
    IiwKICAgICAgImFkcmVzc2UiOiB7CiAgICAgICAgInN0cmFzc2UiOiAiTXVzdGVyc3RyYcOfZSAxMiIsCiAgICAgICAgInBseiI6ICIwMTIzNCIsCiAgICAgICAgIm9y
    dCI6ICJNdXN0ZXJzdGFkdCIKICAgICAgfSwKICAgICAgInplaXRlbiI6IFsKICAgICAgICAiTW9udGFnIDA4LjAwIOKAkyAxMi4wMCIsCiAgICAgICAgIkRpZW5zdGFn
    IDA4LjAwIOKAkyAxMi4wMCwgMTQuMDAg4oCTIDE4LjAwIiwKICAgICAgICAiTWl0d29jaCAwOC4wMCDigJMgMTIuMDAiLAogICAgICAgICJEb25uZXJzdGFnIDA4LjAw
    IOKAkyAxMi4wMCIsCiAgICAgICAgIkZyZWl0YWcgMDguMDAg4oCTIDEyLjAwIgogICAgICBdLAogICAgICAia29udGFrdCI6IHsKICAgICAgICAidGVsZWZvbiI6ICIo
    MDEyMzQpIDEyMzQ1NiIsCiAgICAgICAgImZheCI6ICIoMDEyMzQpIDY1NDMyMSIsCiAgICAgICAgImVtYWlsIjogInByYXhpc0BtdXN0ZXJtYW5uLmRlIiwKICAgICAg
    ICAid2ViIjogImh0dHBzOi8vd3d3Lm11c3Rlcm1hbm4taGF1c2FyenQuZGUvIgogICAgICB9LAogICAgICAiaGlud2VpcyI6ICIiCiAgICB9LAogICAgewogICAgICAi
    aWQiOiAyLAogICAgICAia2FydGUiOiAibGFyZ2UiLAogICAgICAidHlwIjogInphaG5hcnp0IiwKICAgICAgIm5hbWUiOiAiWmFobmFyenQg4oCTIERyLiBKdWxpYSBC
    ZWlzcGllbCIsCiAgICAgICJiaWxkIjogImh0dHBzOi8vcGljc3VtLnBob3Rvcy9zZWVkL3phaG5hcnp0LzMwMC8zMDAiLAogICAgICAiYWRyZXNzZSI6IHsKICAgICAg
    ICAic3RyYXNzZSI6ICJCZWlzcGllbHdlZyA1IiwKICAgICAgICAicGx6IjogIjU2Nzg5IiwKICAgICAgICAib3J0IjogIkJlaXNwaWVsc3RhZHQiCiAgICAgIH0sCiAg
    ICAgICJ6ZWl0ZW4iOiBbCiAgICAgICAgIk1vbnRhZyAwOS4wMCDigJMgMTMuMDAiLAogICAgICAgICJEaWVuc3RhZyAwOS4wMCDigJMgMTMuMDAsIDE0LjAwIOKAkyAx
    OC4wMCIsCiAgICAgICAgIk1pdHR3b2NoIDA5LjAwIOKAkyAxMy4wMCIsCiAgICAgICAgIkRvbm5lcnN0YWcgMDkuMDAg4oCTIDEzLjAwLCAxNC4wMCDigJMgMTguMDAi
    LAogICAgICAgICJGcmVpdGFnIDA5LjAwIOKAkyAxMi4wMCIKICAgICAgXSwKICAgICAgImtvbnRha3QiOiB7CiAgICAgICAgInRlbGVmb24iOiAiKDA1Njc4KSA5ODc2
    NTQiLAogICAgICAgICJmYXgiOiAiIiwKICAgICAgICAiZW1haWwiOiAiaW5mb0B6YWhuYXJ6dC1iZWlzcGllbC5kZSIsCiAgICAgICAgIndlYiI6ICJodHRwczovL3d3
    dy56YWhuYXJ6dC1iZWlzcGllbC5kZS8iCiAgICAgIH0sCiAgICAgICJoaW53ZWlzIjogIlRlcm1pbmUgbmFjaCBWZXJlaW5iYXJ1bmciCiAgICB9LAogICAgewogICAg
    ICAiaWQiOiAzLAogICAgICAia2FydGUiOiAic21hbGwiLAogICAgICAidHlwIjogImZyaXNldXIiLAogICAgICAibmFtZSI6ICJGcmlzZXVyIFNhbG9uIEJlaXNwaWVs
    IiwKICAgICAgImJpbGQiOiAiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvZnJpc2V1ci8zMDAvMzAwIiwKICAgICAgImFkcmVzc2UiOiB7CiAgICAgICAgInN0cmFz
    c2UiOiAiIiwKICAgICAgICAicGx6IjogIiIsCiAgICAgICAgIm9ydCI6ICIiCiAgICAgIH0sCiAgICAgICJ6ZWl0ZW4iOiBbCiAgICAgICAgIk1vbnRhZyDigJMgRnJl
    aXRhZyAwOC4wMCDigJMgMTguMDAiLAogICAgICAgICJTYW1zdGFnIG5hY2ggVmVyZWluYmFydW5nIgogICAgICBdLAogICAgICAia29udGFrdCI6IHsKICAgICAgICAi
    dGVsZWZvbiI6ICIoMDEyMzQpIDExMjIzMyIsCiAgICAgICAgImZheCI6ICIiLAogICAgICAgICJlbWFpbCI6ICIiLAogICAgICAgICJ3ZWIiOiAiIgogICAgICB9LAog
    ICAgICAiaGlud2VpcyI6ICIiCiAgICB9CiAgXQp9`;
    
    /*
        JSON‑Datensatz – Anforderungen (kurz & eindeutig)
    
        • Jeder Eintrag benötigt eine eindeutige ID und muss sich exakt an die Struktur der Beispieldaten halten.
        • Telefonnummern dürfen mehrere Werte enthalten, getrennt durch Komma oder Semikolon.
        • Öffnungszeiten müssen exakt im Format „Wochentag HH.MM – HH.MM[, HH.MM – HH.MM]“ stehen
          und dabei zwingend den echten EN‑DASH (–) verwenden — kein Minuszeichen (-).
        • Das Bildfeld („bild“) kann eine WWW‑URL oder eine ioBroker‑interne URL sein,
          z. B. http://192.168.10.99:8082/vis.0/kontakte/xyz.png
          und es werden PNG, JPG und SVG unterstützt.
    */
    
    //********** ENDE KONFIGURATION **********
    
    async function smartCreateState(id, value, options = {}) {
        if (existsState(id)) return;
        await createState(id, value, options);
    }
    
    function sleepMs(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    function base64Decode(str) {
        return Buffer.from(str, "base64").toString("utf8");
    }
    
    function decodeSampleData() {
        const decoded = base64Decode(SAMPLE_DATA_BASE64);
        return JSON.parse(decoded);
    }
    
    async function processJSON() {
        try {
            const raw = getState(JSON_DP).val;
            if (!raw) return;
    
            const data = JSON.parse(raw);
            if (!data.eintraege || !Array.isArray(data.eintraege)) return;
    
            for (const entry of data.eintraege) {
                const svg = buildSVG(entry);
                const dp = SVG_ROOT + entry.id;
    
                await smartCreateState(dp, "", { type: "string", name: "SVG Karte " + entry.id });
                await sleepMs(100);
    
                setStateIfChanged(dp, svg);
            }
        } catch (err) {
        }
    }
    
    function compressTimes(times) {
        const full    = ["Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"];
        const short   = ["Mo","Di","Mi","Do","Fr","Sa","So"];
        const special = {"Feiertage": "Feiertage", "Brückentage": "Brückentage"};
    
        const parsed = times.map(t => {
            const [day, ...rest] = t.split(" ");
            const time = rest.join(" ");
    
            if (special[day]) {
                return { day, short: special[day], time, special: true };
            }
            return {day, short: short[full.indexOf(day)], time, special: false};
        });
    
        const groups = {};
        const specials = [];
    
        parsed.forEach(p => {
            if (p.special) {
                specials.push(`${p.short} ${p.time}`);
            } else {
                if (!groups[p.time]) groups[p.time] = [];
                groups[p.time].push(p.short);
            }
        });
    
        const result = [];
    
        for (const time in groups) {
            const list = groups[time];
    
            if (list.length === 1) {
                result.push(`${list[0]} ${time}`);
            } else {
                result.push(`${list[0]}–${list[list.length - 1]} ${time}`);
            }
        }
        return result.concat(specials);
    }
    
    function splitTimeRow(row) {
        const parts = row.split(" ");
        const day = parts[0];
        const time = parts.slice(1).join(" ");
        return { day, time };
    }
    
    function wrapText(text, maxLen = 32) {
        const words = text.split(" ");
        const lines = [];
        let current = "";
    
        for (const w of words) {
            if ((current + w).length > maxLen) {
                lines.push(current.trim());
                current = w + " ";
            } else {
                current += w + " ";
            }
        }
        if (current.trim().length > 0) lines.push(current.trim());
        return lines;
    }
    
    on({ id: JSON_DP, change: "any" }, async () => {
        await processJSON();
    });
    
    function buildSVG(entry) {
        if (entry.karte === "small") {
            return buildSmallCard(entry);
        } else {
            return buildLargeCard(entry);
        }
    }
    
    async function main() {
        await smartCreateState(JSON_DP, "", { type: "string", name: "Kontakte JSON" });
    
        const current = getState(JSON_DP).val;
        if (!current || current.trim() === "") {
            const sample = decodeSampleData();
            setState(JSON_DP, JSON.stringify(sample, null, 2));
        }
        await processJSON();
    }
    
    main();
    
    function buildLargeCard(e) {
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(e.hinweis, 45) : [];
        const uid = `card${e.id}`;
    
        function splitTimeAtomic(row) {
            const idx = row.indexOf(" ");
            if (idx === -1) return { day: row, parts: [] };
    
            const day  = row.substring(0, idx);
            const rest = row.substring(idx + 1).split(",");
            const parts = [];
    
            rest.forEach(block => {
                const m = block.trim().match(/^(\S+)\s*–\s*(\S+)$/);
                if (m) parts.push({ start: m[1], end: m[2] });
            });
            return { day, parts };
        }
    
        const X = {
            day: 18,
            s1: 145, d1: 160, e1: 175,
            s2: 255, d2: 270, e2: 285
        };
    
    return `
        <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
        <style>.txt{text-shadow:3px 3px 4px #000;font-feature-settings:"tnum";}</style>
    
        <defs>
            <radialGradient id="glassGlow-${uid}" cx="50%" cy="50%" r="50%">
                <stop offset="0%" stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                <stop offset="22%" stop-color="${AVATAR.glow1?.color ?? AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity}"/>
                <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity}"/>
                <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity}"/>
                <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity}"/>
            </radialGradient>
    
            <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                <stop offset="0%" stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity}"/>
                <stop offset="60%" stop-color="${AVATAR.aura.mid.color}" stop-opacity="${AVATAR.aura.mid.opacity}"/>
                <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity}"/>
            </radialGradient>
    
            <filter id="glassBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%">
                <feGaussianBlur stdDeviation="12" result="blur"/>
                <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
            </filter>
    
            <filter id="glassShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%">
                <feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/>
            </filter>
    
            <mask id="avatarMask-${uid}">
                <circle cx="280" cy="115" r="40" fill="white"/>
            </mask>
        </defs>
    
        <circle cx="280" cy="115" r="90" fill="url(#avatarAuraGlow-${uid})" filter="url(#glassBloom-${uid})" opacity="0.78"/>
        <circle cx="280" cy="115" r="62" fill="url(#glassGlow-${uid})" filter="url(#glassBloom-${uid})"/>
        <circle cx="280" cy="115" r="46" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/>
        <ellipse cx="270" cy="102" rx="12" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 270 102)"/>
        <circle cx="280" cy="115" r="42" fill="${AVATAR.background}" filter="url(#glassShadow-${uid})"/>
        <image href="${e.bild}" x="240" y="75" width="80" height="80" mask="url(#avatarMask-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
        <text class="txt" x="18" y="40" fill="#fff" font-size="15" font-weight="600">${e.name}</text>
        <text class="txt" x="18" y="75" fill="#d0d7e2" font-size="13">${e.adresse.strasse}</text>
        <text class="txt" x="18" y="93" fill="#d0d7e2" font-size="13">${e.adresse.plz} ${e.adresse.ort}</text>
    
        ${e.kontakt.telefon.split(/[,;]\s*/).map((tel,i)=>`
            <text class="txt" x="18" y="${130+i*18}" fill="#9fb3d9" font-size="12">Tel.: ${tel}</text>
        `).join("")}
        ${e.kontakt.fax   ? `<text class="txt" x="18" y="148" fill="#9fb3d9" font-size="12">Fax: ${e.kontakt.fax}</text>` : ""}
        ${e.kontakt.email ? `<text class="txt" x="18" y="166" fill="#9fb3d9" font-size="12">${e.kontakt.email}</text>` : ""}
        ${e.kontakt.web   ? `<text class="txt" x="18" y="184" fill="#6fa8ff" font-size="12">${e.kontakt.web}</text>` : ""}
    
        <text class="txt" x="18" y="225" fill="#fff" font-size="14" font-weight="600">Öffnungszeiten</text>
    
        ${hasValidTimes ? zeiten.map((z,i)=>{
            const {day,parts}=splitTimeAtomic(z);
            const b1=parts[0]||null, b2=parts[1]||null;
            return `
            <text class="txt" y="${250+i*20}" fill="#d0d7e2" font-size="12">
                <tspan x="${X.day}">${day}</tspan>
                ${b1?`<tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>`:""}
                ${b2?`<tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>`:""}
            </text>`;
        }).join("") : `
            <text class="txt" x="18" y="250" fill="#d0d7e2" font-size="12">${e.hinweis}</text>
        `}
    
        ${e.hinweis && hasValidTimes ? hinweisLines.map((line,i)=>`
            <text class="txt" x="18" y="${250+zeiten.length*20+10+i*14}" fill="#ffcc66" font-size="12" font-weight="600">${line}</text>
        `).join("") : ""}
    
        </svg>
    `.trim();
    }
    
    function buildSmallCard(e) {
        const uid = `card${e.id}`;
        const gf = AVATAR.glowFactorSmall;
    
        function normalizeDays(str) {
            const map = {
                "Montag": "Mo",
                "Dienstag": "Di",
                "Mittwoch": "Mi",
                "Donnerstag": "Do",
                "Freitag": "Fr",
                "Samstag": "Sa",
                "Sonntag": "So"
            };
    
            const rangeRegex = /(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)\s*[–-]\s*(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)/g;
    
            str = str.replace(rangeRegex, (m, a, b) => `${map[a]}–${map[b]}`);
            for (const full in map) {
                str = str.replace(new RegExp(full, "g"), map[full]);
            }
            return str;
        }
        const zeiten = e.zeiten.map(z => normalizeDays(z));
    
        function splitTimeRow(row) {
            const idx = row.indexOf(" ");
            if (idx === -1) return { day: row, time: "" };
            return {
                day: row.substring(0, idx),
                time: row.substring(idx + 1)
            };
        }
    
    return `
        <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
        <style>.txt{text-shadow:3px 3px 4px #000;font-family:InterVariable;}</style>
    
        <defs>
    
            <radialGradient id="avatarCoreGlow-${uid}" cx="50%" cy="50%" r="50%">
                <stop offset="0%"  stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                <stop offset="22%" stop-color="${AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity * gf}"/>
                <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity * gf}"/>
                <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity * gf}"/>
                <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity * gf}"/>
            </radialGradient>
    
            <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                <stop offset="0%" stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity}"/>
                <stop offset="60%" stop-color="${AVATAR.aura.mid.color}" stop-opacity="${AVATAR.aura.mid.opacity}"/>
                <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity}"/>
            </radialGradient>
    
            <filter id="avatarBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%">
                <feGaussianBlur stdDeviation="8" result="blur"/>
                <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
            </filter>
    
            <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%">
                <feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/>
            </filter>
    
            <clipPath id="avatarClipSmall-${uid}">
                <circle cx="300" cy="80" r="28"/>
            </clipPath>
        </defs>
    
        <circle cx="300" cy="80" r="48" fill="url(#avatarAuraGlow-${uid})" filter="url(#avatarBloom-${uid})" opacity="0.88"/>
        <circle cx="300" cy="80" r="44" fill="url(#avatarCoreGlow-${uid})" filter="url(#avatarBloom-${uid})"/>
        <circle cx="300" cy="80" r="32" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/>
        <ellipse cx="289" cy="67" rx="11" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 289 67)"/>
        <circle cx="300" cy="80" r="22.4" fill="${AVATAR.background}" filter="url(#avatarShadow-${uid})"/>
        <image href="${e.bild}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
        <text class="txt" x="15" y="32" fill="#fff" font-size="15" font-weight="600">${e.name}</text>
        <text class="txt" x="15" y="55" fill="#9fb3d9" font-size="13">Tel.: ${e.kontakt.telefon}</text>
        <text class="txt" x="15" y="78" fill="#fff" font-size="13" font-weight="600">Öffnungszeiten</text>
    
        ${zeiten.map((z,i)=>{
            const row = splitTimeRow(z);
            return `
            <text class="txt" x="15" y="${95+i*15}" fill="#d0d7e2" font-size="12">
                <tspan x="15">${row.day}</tspan>
                <tspan x="120">${row.time}</tspan>
            </text>`;
        }).join("")}
    
        </svg>
    `.trim();
    }
    
    function setStateIfChanged(id, value) {
        const state = getState(id);
        if (!state) return;
    
        const old = state.val;
        const isObject = v => v !== null && typeof v === "object";
    
        if (isObject(value)) {
            const newStr = JSON.stringify(value);
            let oldStr = null;
    
            if (typeof old === "string") oldStr = old;
            else if (isObject(old)) oldStr = JSON.stringify(old);
    
            if (newStr === oldStr) return;
            return setState(id, newStr, true);
        }
    
        if (old === value) return;
        setState(id, value, true);
    }
    

    Wünsche Euch viel Spaß bei der Umsetzung.

    Ro75.

    SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

    sigi234S 1 Antwort Letzte Antwort
    2
    • Ro75R Ro75

      In unserem Dashboard stehen alle wichtigen Kontakte als moderne, dynamische Karten bereit – so hat jedes Familienmitglied jederzeit Zugriff auf die aktuellsten Informationen, ohne suchen zu müssen.
      Die Daten liegen zentral in einer JSON, aus der automatisch schicke SVG‑Karten erzeugt werden, die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.

      Sobald ein Eintrag geändert oder ein neuer Kontakt hinzugefügt wird, aktualisiert das System die Karten vollautomatisch.
      Keine Pflege an mehreren Stellen, keine veralteten Infos – einmal eintragen, überall aktuell.

      Das Script erzeugt beim ersten Start automatisch einen kompletten Beispieldatensatz mit drei fertigen Karten – inklusive der passenden SVG‑Grafiken.
      So sieht man sofort, wie das System funktioniert und wie die Karten später in der Visualisierung aussehen.

      card.png

      HINWEIS: Ein Teil des Codes wurde dabei mit Unterstützung einer KI (Microsoft Copilot) entwickelt, um die Erstellung noch effizienter und moderner zu gestalten.

      VORAUSSETZUNGEN:

      • JS-Adapter: 9.0.18
      • Node-JS: 22.22.3

      Hier der Code:

      //********** START KONFIGURATION **********
      const root     = "0_userdata.0.Kontakte."
      const JSON_DP  = root + "json";
      const SVG_ROOT = root + "svg.";
      
      // zentrale SVG-Größen
      const CARD_LARGE_W = 350;
      const CARD_LARGE_H = 360;
      
      const CARD_SMALL_W = 350;
      const CARD_SMALL_H = 130;
      
      const AVATAR = {
          aura: {
              inner: {color: "#6E463E", opacity: 0.22}, // stärkster innerer Aura-Kern
              mid:   {color: "#6E463E", opacity: 0.08}, // mittlere Aura
              outer: {color: "#6E463E", opacity: 0.00}  // äußerer Rand, auslaufend
          },
      
          glass: {
              core:  {color: "#ffffff", opacity: 1.00}, // hellster Kern
              glow1: {color: "#6E463E", opacity: 0.92}, // starker Glow
              glow2: {color: "#6E463E", opacity: 0.48}, // mittlerer Glow
              glow3: {color: "#6E463E", opacity: 0.12}, // schwacher Glow
              fade:  {color: "#6E463E", opacity: 0.00}  // auslaufender Rand
          },
      
          glowFactorSmall: 1.25,           // Glow‑Dunkelheits‑Faktor NUR für kleine Karte
          ring: "rgba(255,255,255,0.30)",  // äußerer Ring
          background: "#0f2647"            // Avatar-Hintergrund
      };
      
      //Beispieldaten json - als Base64 - für weniger Platz
      const SAMPLE_DATA_BASE64 =
      `ewogICJlaW50cmFlZ2UiOiBbCiAgICB7CiAgICAgICJpZCI6IDEsCiAgICAgICJrYXJ0ZSI6ICJsYXJnZSIsCiAgICAgICJ0eXAiOiAiaGF1c2FyenQiLAogICAgICAi
      bmFtZSI6ICJIYXVzYXJ6dCDigJMgRHIuIE1heCBNdXN0ZXJtYW5uIiwKICAgICAgImJpbGQiOiAiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvYXJ6dC8zMDAvMzAw
      IiwKICAgICAgImFkcmVzc2UiOiB7CiAgICAgICAgInN0cmFzc2UiOiAiTXVzdGVyc3RyYcOfZSAxMiIsCiAgICAgICAgInBseiI6ICIwMTIzNCIsCiAgICAgICAgIm9y
      dCI6ICJNdXN0ZXJzdGFkdCIKICAgICAgfSwKICAgICAgInplaXRlbiI6IFsKICAgICAgICAiTW9udGFnIDA4LjAwIOKAkyAxMi4wMCIsCiAgICAgICAgIkRpZW5zdGFn
      IDA4LjAwIOKAkyAxMi4wMCwgMTQuMDAg4oCTIDE4LjAwIiwKICAgICAgICAiTWl0d29jaCAwOC4wMCDigJMgMTIuMDAiLAogICAgICAgICJEb25uZXJzdGFnIDA4LjAw
      IOKAkyAxMi4wMCIsCiAgICAgICAgIkZyZWl0YWcgMDguMDAg4oCTIDEyLjAwIgogICAgICBdLAogICAgICAia29udGFrdCI6IHsKICAgICAgICAidGVsZWZvbiI6ICIo
      MDEyMzQpIDEyMzQ1NiIsCiAgICAgICAgImZheCI6ICIoMDEyMzQpIDY1NDMyMSIsCiAgICAgICAgImVtYWlsIjogInByYXhpc0BtdXN0ZXJtYW5uLmRlIiwKICAgICAg
      ICAid2ViIjogImh0dHBzOi8vd3d3Lm11c3Rlcm1hbm4taGF1c2FyenQuZGUvIgogICAgICB9LAogICAgICAiaGlud2VpcyI6ICIiCiAgICB9LAogICAgewogICAgICAi
      aWQiOiAyLAogICAgICAia2FydGUiOiAibGFyZ2UiLAogICAgICAidHlwIjogInphaG5hcnp0IiwKICAgICAgIm5hbWUiOiAiWmFobmFyenQg4oCTIERyLiBKdWxpYSBC
      ZWlzcGllbCIsCiAgICAgICJiaWxkIjogImh0dHBzOi8vcGljc3VtLnBob3Rvcy9zZWVkL3phaG5hcnp0LzMwMC8zMDAiLAogICAgICAiYWRyZXNzZSI6IHsKICAgICAg
      ICAic3RyYXNzZSI6ICJCZWlzcGllbHdlZyA1IiwKICAgICAgICAicGx6IjogIjU2Nzg5IiwKICAgICAgICAib3J0IjogIkJlaXNwaWVsc3RhZHQiCiAgICAgIH0sCiAg
      ICAgICJ6ZWl0ZW4iOiBbCiAgICAgICAgIk1vbnRhZyAwOS4wMCDigJMgMTMuMDAiLAogICAgICAgICJEaWVuc3RhZyAwOS4wMCDigJMgMTMuMDAsIDE0LjAwIOKAkyAx
      OC4wMCIsCiAgICAgICAgIk1pdHR3b2NoIDA5LjAwIOKAkyAxMy4wMCIsCiAgICAgICAgIkRvbm5lcnN0YWcgMDkuMDAg4oCTIDEzLjAwLCAxNC4wMCDigJMgMTguMDAi
      LAogICAgICAgICJGcmVpdGFnIDA5LjAwIOKAkyAxMi4wMCIKICAgICAgXSwKICAgICAgImtvbnRha3QiOiB7CiAgICAgICAgInRlbGVmb24iOiAiKDA1Njc4KSA5ODc2
      NTQiLAogICAgICAgICJmYXgiOiAiIiwKICAgICAgICAiZW1haWwiOiAiaW5mb0B6YWhuYXJ6dC1iZWlzcGllbC5kZSIsCiAgICAgICAgIndlYiI6ICJodHRwczovL3d3
      dy56YWhuYXJ6dC1iZWlzcGllbC5kZS8iCiAgICAgIH0sCiAgICAgICJoaW53ZWlzIjogIlRlcm1pbmUgbmFjaCBWZXJlaW5iYXJ1bmciCiAgICB9LAogICAgewogICAg
      ICAiaWQiOiAzLAogICAgICAia2FydGUiOiAic21hbGwiLAogICAgICAidHlwIjogImZyaXNldXIiLAogICAgICAibmFtZSI6ICJGcmlzZXVyIFNhbG9uIEJlaXNwaWVs
      IiwKICAgICAgImJpbGQiOiAiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvZnJpc2V1ci8zMDAvMzAwIiwKICAgICAgImFkcmVzc2UiOiB7CiAgICAgICAgInN0cmFz
      c2UiOiAiIiwKICAgICAgICAicGx6IjogIiIsCiAgICAgICAgIm9ydCI6ICIiCiAgICAgIH0sCiAgICAgICJ6ZWl0ZW4iOiBbCiAgICAgICAgIk1vbnRhZyDigJMgRnJl
      aXRhZyAwOC4wMCDigJMgMTguMDAiLAogICAgICAgICJTYW1zdGFnIG5hY2ggVmVyZWluYmFydW5nIgogICAgICBdLAogICAgICAia29udGFrdCI6IHsKICAgICAgICAi
      dGVsZWZvbiI6ICIoMDEyMzQpIDExMjIzMyIsCiAgICAgICAgImZheCI6ICIiLAogICAgICAgICJlbWFpbCI6ICIiLAogICAgICAgICJ3ZWIiOiAiIgogICAgICB9LAog
      ICAgICAiaGlud2VpcyI6ICIiCiAgICB9CiAgXQp9`;
      
      /*
          JSON‑Datensatz – Anforderungen (kurz & eindeutig)
      
          • Jeder Eintrag benötigt eine eindeutige ID und muss sich exakt an die Struktur der Beispieldaten halten.
          • Telefonnummern dürfen mehrere Werte enthalten, getrennt durch Komma oder Semikolon.
          • Öffnungszeiten müssen exakt im Format „Wochentag HH.MM – HH.MM[, HH.MM – HH.MM]“ stehen
            und dabei zwingend den echten EN‑DASH (–) verwenden — kein Minuszeichen (-).
          • Das Bildfeld („bild“) kann eine WWW‑URL oder eine ioBroker‑interne URL sein,
            z. B. http://192.168.10.99:8082/vis.0/kontakte/xyz.png
            und es werden PNG, JPG und SVG unterstützt.
      */
      
      //********** ENDE KONFIGURATION **********
      
      async function smartCreateState(id, value, options = {}) {
          if (existsState(id)) return;
          await createState(id, value, options);
      }
      
      function sleepMs(ms) {
          return new Promise(resolve => setTimeout(resolve, ms));
      }
      
      function base64Decode(str) {
          return Buffer.from(str, "base64").toString("utf8");
      }
      
      function decodeSampleData() {
          const decoded = base64Decode(SAMPLE_DATA_BASE64);
          return JSON.parse(decoded);
      }
      
      async function processJSON() {
          try {
              const raw = getState(JSON_DP).val;
              if (!raw) return;
      
              const data = JSON.parse(raw);
              if (!data.eintraege || !Array.isArray(data.eintraege)) return;
      
              for (const entry of data.eintraege) {
                  const svg = buildSVG(entry);
                  const dp = SVG_ROOT + entry.id;
      
                  await smartCreateState(dp, "", { type: "string", name: "SVG Karte " + entry.id });
                  await sleepMs(100);
      
                  setStateIfChanged(dp, svg);
              }
          } catch (err) {
          }
      }
      
      function compressTimes(times) {
          const full    = ["Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"];
          const short   = ["Mo","Di","Mi","Do","Fr","Sa","So"];
          const special = {"Feiertage": "Feiertage", "Brückentage": "Brückentage"};
      
          const parsed = times.map(t => {
              const [day, ...rest] = t.split(" ");
              const time = rest.join(" ");
      
              if (special[day]) {
                  return { day, short: special[day], time, special: true };
              }
              return {day, short: short[full.indexOf(day)], time, special: false};
          });
      
          const groups = {};
          const specials = [];
      
          parsed.forEach(p => {
              if (p.special) {
                  specials.push(`${p.short} ${p.time}`);
              } else {
                  if (!groups[p.time]) groups[p.time] = [];
                  groups[p.time].push(p.short);
              }
          });
      
          const result = [];
      
          for (const time in groups) {
              const list = groups[time];
      
              if (list.length === 1) {
                  result.push(`${list[0]} ${time}`);
              } else {
                  result.push(`${list[0]}–${list[list.length - 1]} ${time}`);
              }
          }
          return result.concat(specials);
      }
      
      function splitTimeRow(row) {
          const parts = row.split(" ");
          const day = parts[0];
          const time = parts.slice(1).join(" ");
          return { day, time };
      }
      
      function wrapText(text, maxLen = 32) {
          const words = text.split(" ");
          const lines = [];
          let current = "";
      
          for (const w of words) {
              if ((current + w).length > maxLen) {
                  lines.push(current.trim());
                  current = w + " ";
              } else {
                  current += w + " ";
              }
          }
          if (current.trim().length > 0) lines.push(current.trim());
          return lines;
      }
      
      on({ id: JSON_DP, change: "any" }, async () => {
          await processJSON();
      });
      
      function buildSVG(entry) {
          if (entry.karte === "small") {
              return buildSmallCard(entry);
          } else {
              return buildLargeCard(entry);
          }
      }
      
      async function main() {
          await smartCreateState(JSON_DP, "", { type: "string", name: "Kontakte JSON" });
      
          const current = getState(JSON_DP).val;
          if (!current || current.trim() === "") {
              const sample = decodeSampleData();
              setState(JSON_DP, JSON.stringify(sample, null, 2));
          }
          await processJSON();
      }
      
      main();
      
      function buildLargeCard(e) {
          const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
          const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
          const hinweisLines  = e.hinweis ? wrapText(e.hinweis, 45) : [];
          const uid = `card${e.id}`;
      
          function splitTimeAtomic(row) {
              const idx = row.indexOf(" ");
              if (idx === -1) return { day: row, parts: [] };
      
              const day  = row.substring(0, idx);
              const rest = row.substring(idx + 1).split(",");
              const parts = [];
      
              rest.forEach(block => {
                  const m = block.trim().match(/^(\S+)\s*–\s*(\S+)$/);
                  if (m) parts.push({ start: m[1], end: m[2] });
              });
              return { day, parts };
          }
      
          const X = {
              day: 18,
              s1: 145, d1: 160, e1: 175,
              s2: 255, d2: 270, e2: 285
          };
      
      return `
          <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
          <style>.txt{text-shadow:3px 3px 4px #000;font-feature-settings:"tnum";}</style>
      
          <defs>
              <radialGradient id="glassGlow-${uid}" cx="50%" cy="50%" r="50%">
                  <stop offset="0%" stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                  <stop offset="22%" stop-color="${AVATAR.glow1?.color ?? AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity}"/>
                  <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity}"/>
                  <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity}"/>
                  <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity}"/>
              </radialGradient>
      
              <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                  <stop offset="0%" stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity}"/>
                  <stop offset="60%" stop-color="${AVATAR.aura.mid.color}" stop-opacity="${AVATAR.aura.mid.opacity}"/>
                  <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity}"/>
              </radialGradient>
      
              <filter id="glassBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%">
                  <feGaussianBlur stdDeviation="12" result="blur"/>
                  <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
              </filter>
      
              <filter id="glassShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%">
                  <feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/>
              </filter>
      
              <mask id="avatarMask-${uid}">
                  <circle cx="280" cy="115" r="40" fill="white"/>
              </mask>
          </defs>
      
          <circle cx="280" cy="115" r="90" fill="url(#avatarAuraGlow-${uid})" filter="url(#glassBloom-${uid})" opacity="0.78"/>
          <circle cx="280" cy="115" r="62" fill="url(#glassGlow-${uid})" filter="url(#glassBloom-${uid})"/>
          <circle cx="280" cy="115" r="46" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/>
          <ellipse cx="270" cy="102" rx="12" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 270 102)"/>
          <circle cx="280" cy="115" r="42" fill="${AVATAR.background}" filter="url(#glassShadow-${uid})"/>
          <image href="${e.bild}" x="240" y="75" width="80" height="80" mask="url(#avatarMask-${uid})" preserveAspectRatio="xMidYMid slice"/>
      
          <text class="txt" x="18" y="40" fill="#fff" font-size="15" font-weight="600">${e.name}</text>
          <text class="txt" x="18" y="75" fill="#d0d7e2" font-size="13">${e.adresse.strasse}</text>
          <text class="txt" x="18" y="93" fill="#d0d7e2" font-size="13">${e.adresse.plz} ${e.adresse.ort}</text>
      
          ${e.kontakt.telefon.split(/[,;]\s*/).map((tel,i)=>`
              <text class="txt" x="18" y="${130+i*18}" fill="#9fb3d9" font-size="12">Tel.: ${tel}</text>
          `).join("")}
          ${e.kontakt.fax   ? `<text class="txt" x="18" y="148" fill="#9fb3d9" font-size="12">Fax: ${e.kontakt.fax}</text>` : ""}
          ${e.kontakt.email ? `<text class="txt" x="18" y="166" fill="#9fb3d9" font-size="12">${e.kontakt.email}</text>` : ""}
          ${e.kontakt.web   ? `<text class="txt" x="18" y="184" fill="#6fa8ff" font-size="12">${e.kontakt.web}</text>` : ""}
      
          <text class="txt" x="18" y="225" fill="#fff" font-size="14" font-weight="600">Öffnungszeiten</text>
      
          ${hasValidTimes ? zeiten.map((z,i)=>{
              const {day,parts}=splitTimeAtomic(z);
              const b1=parts[0]||null, b2=parts[1]||null;
              return `
              <text class="txt" y="${250+i*20}" fill="#d0d7e2" font-size="12">
                  <tspan x="${X.day}">${day}</tspan>
                  ${b1?`<tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>`:""}
                  ${b2?`<tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>`:""}
              </text>`;
          }).join("") : `
              <text class="txt" x="18" y="250" fill="#d0d7e2" font-size="12">${e.hinweis}</text>
          `}
      
          ${e.hinweis && hasValidTimes ? hinweisLines.map((line,i)=>`
              <text class="txt" x="18" y="${250+zeiten.length*20+10+i*14}" fill="#ffcc66" font-size="12" font-weight="600">${line}</text>
          `).join("") : ""}
      
          </svg>
      `.trim();
      }
      
      function buildSmallCard(e) {
          const uid = `card${e.id}`;
          const gf = AVATAR.glowFactorSmall;
      
          function normalizeDays(str) {
              const map = {
                  "Montag": "Mo",
                  "Dienstag": "Di",
                  "Mittwoch": "Mi",
                  "Donnerstag": "Do",
                  "Freitag": "Fr",
                  "Samstag": "Sa",
                  "Sonntag": "So"
              };
      
              const rangeRegex = /(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)\s*[–-]\s*(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)/g;
      
              str = str.replace(rangeRegex, (m, a, b) => `${map[a]}–${map[b]}`);
              for (const full in map) {
                  str = str.replace(new RegExp(full, "g"), map[full]);
              }
              return str;
          }
          const zeiten = e.zeiten.map(z => normalizeDays(z));
      
          function splitTimeRow(row) {
              const idx = row.indexOf(" ");
              if (idx === -1) return { day: row, time: "" };
              return {
                  day: row.substring(0, idx),
                  time: row.substring(idx + 1)
              };
          }
      
      return `
          <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
          <style>.txt{text-shadow:3px 3px 4px #000;font-family:InterVariable;}</style>
      
          <defs>
      
              <radialGradient id="avatarCoreGlow-${uid}" cx="50%" cy="50%" r="50%">
                  <stop offset="0%"  stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                  <stop offset="22%" stop-color="${AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity * gf}"/>
                  <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity * gf}"/>
                  <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity * gf}"/>
                  <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity * gf}"/>
              </radialGradient>
      
              <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                  <stop offset="0%" stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity}"/>
                  <stop offset="60%" stop-color="${AVATAR.aura.mid.color}" stop-opacity="${AVATAR.aura.mid.opacity}"/>
                  <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity}"/>
              </radialGradient>
      
              <filter id="avatarBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%">
                  <feGaussianBlur stdDeviation="8" result="blur"/>
                  <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
              </filter>
      
              <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%">
                  <feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/>
              </filter>
      
              <clipPath id="avatarClipSmall-${uid}">
                  <circle cx="300" cy="80" r="28"/>
              </clipPath>
          </defs>
      
          <circle cx="300" cy="80" r="48" fill="url(#avatarAuraGlow-${uid})" filter="url(#avatarBloom-${uid})" opacity="0.88"/>
          <circle cx="300" cy="80" r="44" fill="url(#avatarCoreGlow-${uid})" filter="url(#avatarBloom-${uid})"/>
          <circle cx="300" cy="80" r="32" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/>
          <ellipse cx="289" cy="67" rx="11" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 289 67)"/>
          <circle cx="300" cy="80" r="22.4" fill="${AVATAR.background}" filter="url(#avatarShadow-${uid})"/>
          <image href="${e.bild}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
      
          <text class="txt" x="15" y="32" fill="#fff" font-size="15" font-weight="600">${e.name}</text>
          <text class="txt" x="15" y="55" fill="#9fb3d9" font-size="13">Tel.: ${e.kontakt.telefon}</text>
          <text class="txt" x="15" y="78" fill="#fff" font-size="13" font-weight="600">Öffnungszeiten</text>
      
          ${zeiten.map((z,i)=>{
              const row = splitTimeRow(z);
              return `
              <text class="txt" x="15" y="${95+i*15}" fill="#d0d7e2" font-size="12">
                  <tspan x="15">${row.day}</tspan>
                  <tspan x="120">${row.time}</tspan>
              </text>`;
          }).join("")}
      
          </svg>
      `.trim();
      }
      
      function setStateIfChanged(id, value) {
          const state = getState(id);
          if (!state) return;
      
          const old = state.val;
          const isObject = v => v !== null && typeof v === "object";
      
          if (isObject(value)) {
              const newStr = JSON.stringify(value);
              let oldStr = null;
      
              if (typeof old === "string") oldStr = old;
              else if (isObject(old)) oldStr = JSON.stringify(old);
      
              if (newStr === oldStr) return;
              return setState(id, newStr, true);
          }
      
          if (old === value) return;
          setState(id, value, true);
      }
      

      Wünsche Euch viel Spaß bei der Umsetzung.

      Ro75.

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

      @Ro75 sagte:

      die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.

      und wie?

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

      1 Antwort Letzte Antwort
      0
      • Ro75R Online
        Ro75R Online
        Ro75
        schrieb zuletzt editiert von Ro75
        #3

        mit einem Basic - String (unescaped). Hier VIS1.
        76ae27c0-3e71-4d7d-b2de-9f15c0dff723-image.jpeg

        Ro75.

        SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

        1 Antwort Letzte Antwort
        0
        • Ro75R Online
          Ro75R Online
          Ro75
          schrieb zuletzt editiert von
          #4

          Habe den Code im Eingangspost aktualisiert. Da fehlte eine Funktion.

          Ro75.

          SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

          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

          440

          Online

          32.9k

          Benutzer

          83.0k

          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