Skip to content

Visualisierung

Hilfe zu Visualisierungen

9.4k Themen 103.9k Beiträge

NEWS

  • VIS / VIS-2 Übersicht Widgetkompatibilität

    Angeheftet vis
    41
    15 Stimmen
    41 Beiträge
    12k Aufrufe
    OliverIOO
    @bongo Jeder view hat eine url. Ob die von vis1 oder vis2 ausgeliefert wird ist dem Browser egal
  • Liste mit Geräten und deren Auflösung für VIS

    Angeheftet Verschoben vis
    100
    3
    1 Stimmen
    100 Beiträge
    44k Aufrufe
    B
    Ich bin ehrlich, ich habe alles versucht habe ein Samsung TabA 10.1 Zoll mit Fully Browser habe auch alles gemacht wie beschrieben aber im Vis kann ich die Größe ändern wie ich will es sind auf dem Tablet rechts und unten immer 5mm die nicht ausgefüllt bzw. schwarz sind. Die muss ich beim Neustart jedes Mal auseinander ziehen. Die Screen-Resolution zeigt mir 1333x799 an. Ich habe es schon fast aufgegeben. Vielleicht liegt es auch an Einstellungen vom Fully Browser. Falls jemand einen Tipp hat sehr gerne. Danke [image: 1768406307278-79cb9ff7-e6e6-4eb6-b8d6-f39e1c2b0d05-grafik.png]
  • iQontrol - Kachel nur als Anzeige ob etwas an oder aus ist?

    1
    0 Stimmen
    1 Beiträge
    15 Aufrufe
    Niemand hat geantwortet
  • Hilfe zu Material-Design View Dialog

    11
    5
    0 Stimmen
    11 Beiträge
    858 Aufrufe
    L
    Ich habe lange herumprobiert und des Rätsels Lösung gefunden. Dieses RELATIVE fehlte in den Dialog Views: [image: 1772004618145-1c3cb739-63e1-4255-830f-0794c3fa3871-image.png] [image: 1772007890411-wetter_view.gif] Hier das fertige Projekt 2026-02-25-Wetter2.zip
  • VIS(1): Ein PopUp für mehrere Lampen

    3
    0 Stimmen
    3 Beiträge
    108 Aufrufe
    S
    Hallo, vielen Dank. Muss ich mir mal in Ruhe durschauen
  • iQontrol Vis Support Thread

    android app vis how-to ios app iqontrol mobile ui vis
    2k
    5
    7 Stimmen
    2k Beiträge
    689k Aufrufe
    souly75S
    @marcelinho scroll ein paar Einträge hoch, dort ist die Lösung zum Problem (1.2.26)
  • Zeigt her eure iQontrol Visualisierung

    iqontrol
    354
    2 Stimmen
    354 Beiträge
    98k Aufrufe
    M
    Moin zusammen, die regnerischen Tage habe ich damit verbracht, mein iQontrol (mal wieder) umzugestalten. Hier mein neues Menü (ich habe nur zwei Unterseiten, die Animation ist als GIF etwas träge… 🥴): [image: 1771760384828-screenrecording_02-22-2026-12-34-35_1.gif] Haben sich eure Dashboards verändert?
  • VIS-2 Material Widgets Signalbilder

    16
    0 Stimmen
    16 Beiträge
    1k Aufrufe
    R
    Hi zusammen, ist der Bug behoben worden? Bei den Widgets Universal & Multi aus "vis-iventwo" und universal aus "iventwo-design" sind weiterhin "nur" 3 Signalbilder möglich, obwohl zB 4 ausgewählt wurden
  • Reolink über go2rtc in VIS einbinden

    23
    1
    0 Stimmen
    23 Beiträge
    203 Aufrufe
    inviI
    Hallo Hans. http://Meine IP:1984/stream.html?src=Hauseingang-1&mode=webrtc läuft mit W-Lan auf einem Echo Show 8 ohne ruckeln. Grüße invi
  • Iqontrol - wie bindet man ein iframe ein?

    3
    0 Stimmen
    3 Beiträge
    54 Aufrufe
    JLegJ
    @weimaraner für eine "Digitaluhr" braucht's keinen iframe - ich habe z.B. ein "Widget" genommen, und bei "background_html" einen eigenen Datenpunkt vom Typ "string" eingetragen. Dazu läuft dann ein Script, das bei mir alle 10min bissel HTML in diesen Datenpunkt schreibt...
  • VIS 2: WLED Effekt (seg.0.fx) als Dropdown anzeigen

    32
    0 Stimmen
    32 Beiträge
    202 Aufrufe
    D
    @skvarel bin gespannt
  • Problem mit "Vis 2 inventwo Widgets" Update > v0.2.2

    18
    2
    0 Stimmen
    18 Beiträge
    814 Aufrufe
    jkvarelJ
    Immerhin ein Fortschritt. Dann scheint für die raspberrypi5 Adresse evtl. noch was blockiert zu werden. Ich kenne mich da leider auch zu wenig aus, um Tipps zu geben was das Problem sein könnte, tut mir leid. Vielleicht hat noch jemand anderes eine Idee
  • Lovelace: "Unknown request for /lovelace/default_view"

    7
    0 Stimmen
    7 Beiträge
    54 Aufrufe
    David G.D
    @Rico-Sander Aber warum denkst du ist das der korrekte link? Steht das wirklich unter link auf der Seite wie in meinem Screenshot? [image: 1771325850400-1000061304.jpg] da könnte man zum testen ja mal default_view rein schreiben.
  • iobroker Visu App - Lädt immer via pro.cloud

    5
    9
    0 Stimmen
    5 Beiträge
    104 Aufrufe
    CyberraphC
    @bahnuhr Jap, der admin mit Passwort ist überflüssig. Hab ich schon gelernt, aber bislang nicht raus getan. Jetzt hab ich es mal überall raus getan. Aber abseits davon: Auch nochmal bei meinem anderen Handy, wo alles sauber läuft herumprobiert. Die SSID "FRITZ!Box 7530 UO" mal anders eingegeben. Und zack, dann wird über .pro verbunden. Geb ich sie korrekt ein, läuft es korrekt offline / direkt über das lokale Netzwerk. Bleibt mir ein Rätsel, warum es auf einmal beim anderen Handy nicht mehr funktioniert (obwohl nix geändert wurde). Es trat auf einmal auf nach vermutlich einem Stromausfall -> Serverneustart zuletzt. Aber warum sich da auf einmal was querstellt. Naja ich belasse es mal dabei -> ohne .pro das andere Handy zu belassen, damit es lokal sich verbindet.
  • iQontrol Größe der Geräte-Kacheln anpassen ?

    1
    3
    0 Stimmen
    1 Beiträge
    50 Aufrufe
    Niemand hat geantwortet
  • Support Adapter Energiefluss-erweitert v0.7.7

    Verschoben vis
    5k
    9
    20 Stimmen
    5k Beiträge
    6m Aufrufe
    hotspot_2H
    Hallo zusammen, kurze Frage zur Animation der Elemente oder der Funktion "Elemente nach Wert füllen": Kann man die Animation beim Start irgendwie deaktivieren so das der Wert sofort korrekt angezeigt wird?
  • Zeigt her eure Visu

    vis
    124
    2 Stimmen
    124 Beiträge
    29k Aufrufe
    schaefersklausS
    @michl75 sagte in Zeigt her eure Visu: hier mal meine... ... Stark! Gefällt mir sehr gut! Besonders, wie aufwendig Du alleine die Teich-Steuerung umgesetzt/visualisiert hast! Wie sind diese Halbkreis-förmigen Stromverbrauchs-Diagramme realisiert worden? Welches Widget? Finde ich auch für einen Überblick richtig gut gelöst! Geht etwas in die Richtung "des Balkens" bei evcc, das finde ich auch gut gelöst. Danke und viele Grüße, Klaus.
  • Widget import klappt nicht in VIS-2

    8
    0 Stimmen
    8 Beiträge
    108 Aufrufe
    OliverIOO
    @Longbow So wie es aussieht, ist das eine Gruppe. D.h. es besteht aus mehreren einzelnen Widgets. Versuche mal in vis1 die Gruppe aufzulösen und dir anzuschauen, welche einzelnen Widgets es sind. Eventuell liegt es auch an der Gruppe, dass es nicht in vis2 zu importieren geht, dann ebenfalls die umgruppierten Widgets markieren, exportieren, und dann noch mal probieren. Dann kannst du in der Kompatibilitätsliste nachschauen, ob es da drin steht. Gegebenfalls musst du es in vis2 selber nachbauen, das dürfte nicht so schwer sein.
  • LovelaceUI Wettervorhersage -> wie weather Endität erzeugen?

    Verschoben
    3
    4
    0 Stimmen
    3 Beiträge
    59 Aufrufe
    AtifanA
    Ah sorry bin im falschen Forum gelandet, war keine Absicht. Kannst du das ins deutsche Forum verschieben? Wenn nicht kopiere ich alles neu und lösche hier.
  • Vis2 Tabs Widget - Tab per URL anspringen

    9
    0 Stimmen
    9 Beiträge
    87 Aufrufe
    M
    @Sesamstrasse Einfach ist relativ. DPs anlegen und den Code in Skripte austauschen. DPs in ioBroker anlegen mit dem Skript Beispiele für DP setzen sind ausgeklammert, da die Werte angepasst werden müssen ioBroker javascript -> DPs anlegen // ************************ // VIS2TabControl v1.0.0 // Copyright ©MCU // ************************ // ioBroker javascript.0 - DPs für VIS2 Tabs anlegen const DP_MAIN = '0_userdata.0.vis2' const DP_TAB_EVENT = DP_MAIN + ".tabEvent"; // Client -> ioBroker const DP_TAB_CMD = DP_MAIN + ".tabCmd"; // ioBroker -> Client const DP_DEVICE_MAP = DP_MAIN + ".deviceMap"; // clientId -> clientName createState(DP_TAB_CMD, '', {name: 'VIS2 Tab Command',type: 'string', role:'', def: '', read: true, write: true, desc: ''}); createState(DP_TAB_EVENT, '', {name: 'VIS2 Tab Event',type: 'string', role:'', def: '', read: true, write: true, desc: ''}); createState(DP_DEVICE_MAP, '', {name: 'VIS2 Client Map',type: 'string', role:'', def: '', read: true, write: true, desc: ''}); // DEVICE_MAP - Aufbau /* { "d787264857bc0d13c393d37336f43dc9": "PC", "d7282dhf83hf832f944mhn4345n345n3": "Tablet" } */ // Beispiele /* setState(DP_TAB_CMD, JSON.stringify({ cmdId: `${Date.now()}-${Math.random().toString(16).slice(2)}`, target: ["d787264857bc0d13c393d37336f43dc9"], view: "datum", tab: "Licht", force: true }), false); */ // Wenn eine Zuordnung im Device_MAP vorhanden ist, kann man auch die Name nutzen /* setState(DP_TAB_CMD, JSON.stringify({ cmdId: `${Date.now()}-${Math.random().toString(16).slice(2)}`, target: ["PC"], view: "datum", tab: 2, force: true }), false); */ Script in VIS2 unter Skripte austauschen Script für VIS-2 Skripte /******************************************************************** * VIS-2 Global Script: Tabs steuern + Sync + Multi-Client Commands * * Fixes: * - Commands funktionieren mehrfach (Dedup nur über cmdId, wenn vorhanden) * - force:true klickt Tab auch wenn bereits selektiert * - periodisches resubscribe gegen verlorene Subscriptions ********************************************************************/ // =================== KONFIG =================== const DEBUG = false; // Tabs-Widget ID (aus deinem DOM) const TABS_WIDGET_ID = "w000016"; // URL Parameter const URL_PARAM_TAB = "tab"; // ?main&tab=Licht#datum // Datenpunkte (string) const DP_TAB_EVENT = "0_userdata.0.vis2.tabEvent"; // Client -> ioBroker const DP_TAB_CMD = "0_userdata.0.vis2.tabCmd"; // ioBroker -> Client const DP_DEVICE_MAP = "0_userdata.0.vis2.deviceMap"; // ioBroker -> Client (DeviceId -> Name) // Index-Format const INDEX_ONE_BASED = true; // true => 1..N, false => 0..N-1 // History const USE_PUSHSTATE = false; // false = replaceState (empfohlen) // Timing const OBSERVE_TIMEOUT_MS = 15000; // Re-Subscribe (gegen „geht nur einmal“) const RESUBSCRIBE_EVERY_MS = 30000; // localStorage Keys const LS_KEYS = { deviceId: "vis2.deviceId", deviceName: "vis2.deviceName", }; // ============================================== // =================== Helpers =================== function dlog(...a) { if (DEBUG) console.log("[VIS2Tabs]", ...a); } function getVis() { return (typeof window !== "undefined" && (window.vis || window.VIS)) || (typeof vis !== "undefined" ? vis : null); } let suppressUntil = 0; function suppress(ms = 350) { suppressUntil = Date.now() + ms; } function isSuppressed() { return Date.now() < suppressUntil; } function norm(s) { return String(s ?? "").trim().toLowerCase(); } function safeJsonParse(s) { try { return JSON.parse(s); } catch { return null; } } function getSearchParams() { return new URLSearchParams(window.location.search); } function getCurrentViewFromHash() { const h = (window.location.hash || "").replace(/^#/, ""); return (h.split("?")[0] || "").trim(); } function gotoView(view) { if (!view) return; const cur = getCurrentViewFromHash(); if (cur === view) return; suppress(600); window.location.hash = `#${view}`; } function getOrCreateDeviceId() { let id = localStorage.getItem(LS_KEYS.deviceId); if (id) return id; const buf = new Uint8Array(16); if (crypto?.getRandomValues) crypto.getRandomValues(buf); else for (let i = 0; i < buf.length; i++) buf[i] = Math.floor(Math.random() * 256); id = Array.from(buf).map(b => b.toString(16).padStart(2, "0")).join(""); localStorage.setItem(LS_KEYS.deviceId, id); return id; } // robustes Lesen aus vis.states Cache (DP oder DP.val / ggf. State-Objekt) function readVisValue(id) { const v = getVis(); if (!v?.states) return null; const candidates = [id + ".val", id]; for (const key of candidates) { try { let val = (v.states.attr && v.states.attr(key)) ?? v.states[key]; if (typeof val === "object" && val && "val" in val) val = val.val; // unwrap if (val !== undefined && val !== null) return val; } catch {} } return null; } function writeVisValue(id, value) { const v = getVis(); if (!v) return false; if (typeof v.setValue === "function") { v.setValue(id, value); return true; } if (v.conn && typeof v.conn.setState === "function") { v.conn.setState(id, value); return true; } return false; } // ============================================== // =================== Device Map =================== function applyDeviceNameFromMap() { const myId = getOrCreateDeviceId(); const raw = readVisValue(DP_DEVICE_MAP); const map = (typeof raw === "string" && raw.trim()) ? safeJsonParse(raw) : null; const name = map && typeof map === "object" ? map[myId] : null; if (name && String(name).trim()) { const n = String(name).trim(); localStorage.setItem(LS_KEYS.deviceName, n); return n; } return (localStorage.getItem(LS_KEYS.deviceName) || "").trim(); } // ============================================== // =================== URL: tab aus Query =================== function getTabParamFromSearch() { return getSearchParams().get(URL_PARAM_TAB) || null; } // NICHT URLSearchParams serialisieren (sonst wird aus ?main -> ?main=) // Wir ersetzen/appendieren nur "&tab=..." function setTabParamInSearch(tabValue) { if (!tabValue) return; const path = window.location.pathname; const hash = window.location.hash || ""; const raw = window.location.search || ""; const current = getTabParamFromSearch(); if (current === tabValue) return; let next = raw; if (/[?&]tab=/.test(next)) { next = next.replace(/([?&]tab=)[^&]*/i, `$1${encodeURIComponent(tabValue)}`); } else { next += (next.includes("?") ? "&" : "?") + `tab=${encodeURIComponent(tabValue)}`; } const newUrl = `${path}${next}${hash}`; suppress(450); if (USE_PUSHSTATE) history.pushState(null, "", newUrl); else history.replaceState(null, "", newUrl); } // ============================================== // =================== Tabs DOM Zugriff =================== function getTabButtons() { const root = document.getElementById(TABS_WIDGET_ID); if (!root) return []; return Array.from(root.querySelectorAll('button[role="tab"]')); } function isSelected(btn) { return ( btn?.getAttribute("aria-selected") === "true" || btn?.classList?.contains("Mui-selected") ); } function getSelectedTab() { const tabs = getTabButtons(); return tabs.find(isSelected) || null; } function getSelectedTabName() { const sel = getSelectedTab(); return sel ? (sel.textContent || "").trim() : ""; } function getSelectedTabIndex() { const tabs = getTabButtons(); const sel = getSelectedTab(); if (!sel) return null; const idx0 = tabs.indexOf(sel); if (idx0 < 0) return null; return INDEX_ONE_BASED ? (idx0 + 1) : idx0; } // force=true: klickt auch wenn schon selected function trySelectTab(tabParam, force = false) { if (tabParam == null) return false; const tabs = getTabButtons(); if (!tabs.length) return false; let target = null; const s = String(tabParam).trim(); if (/^\d+$/.test(s)) { const n = parseInt(s, 10); const idx = (n === 0) ? 0 : (n - 1); target = tabs[idx]; } else { const needle = norm(s); target = tabs.find(b => norm(b.textContent) === needle); } if (!target) return false; if (force || !isSelected(target)) { suppress(450); target.click(); } return true; } function forceSelectTab(tabParam, force = false) { if (trySelectTab(tabParam, force)) return; const obs = new MutationObserver(() => { if (trySelectTab(tabParam, force)) obs.disconnect(); }); obs.observe(document.body, { childList: true, subtree: true }); setTimeout(() => obs.disconnect(), OBSERVE_TIMEOUT_MS); } // ============================================== // =================== TabEvent -> DP =================== let lastSentSig = ""; function sendTabEventToDP() { const idx = getSelectedTabIndex(); const name = getSelectedTabName(); if (idx == null || !name) return; const sig = `${idx}|${name}`; if (sig === lastSentSig) return; lastSentSig = sig; const deviceId = getOrCreateDeviceId(); const deviceName = applyDeviceNameFromMap(); const view = getCurrentViewFromHash(); const payload = { deviceId, deviceName: deviceName || undefined, view: view || undefined, tabIndex: idx, tabName: name, ts: Date.now() }; writeVisValue(DP_TAB_EVENT, JSON.stringify(payload)); } // ============================================== // =================== URL -> Tab (Load/Navi) =================== function applyTabFromUrl() { if (isSuppressed()) return; const tabParam = getTabParamFromSearch(); if (!tabParam) return; forceSelectTab(tabParam, false); } // ============================================== // =================== Tab -> URL (bei Wechsel) =================== let lastWrittenName = ""; function writeUrlFromSelection() { if (isSuppressed()) return; const name = getSelectedTabName(); if (!name) return; if (name === lastWrittenName) return; lastWrittenName = name; setTabParamInSearch(name); } function wireTabHandlers() { const tabs = getTabButtons(); if (!tabs.length) return false; tabs.forEach(btn => { if (btn.__vis2TabWired) return; btn.__vis2TabWired = true; const handler = () => setTimeout(() => { writeUrlFromSelection(); sendTabEventToDP(); }, 0); btn.addEventListener("click", handler); btn.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") handler(); }); }); return true; } function observeTabChanges() { const root = document.getElementById(TABS_WIDGET_ID); if (!root) return false; const obs = new MutationObserver(() => { wireTabHandlers(); writeUrlFromSelection(); sendTabEventToDP(); }); obs.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ["aria-selected", "class"], }); wireTabHandlers(); writeUrlFromSelection(); sendTabEventToDP(); return true; } // ============================================== // =================== Command-DP -> Tab setzen =================== let lastCmdToken = ""; function isTargetForMe(cmd) { const myId = getOrCreateDeviceId(); const myName = applyDeviceNameFromMap(); if (!cmd || cmd.target == null) return false; const t = cmd.target; if (t === "*") return true; if (Array.isArray(t)) { return t.includes(myId) || (myName && t.includes(myName)); } if (typeof t === "string") { return t === myId || (myName && t === myName); } if (typeof t === "object") { if (t.deviceId && t.deviceId === myId) return true; if (t.deviceName && myName && t.deviceName === myName) return true; } return false; } function handleTabCmd(rawVal) { if (rawVal == null) return; // unwrap State-Objekt if (typeof rawVal === "object" && rawVal && "val" in rawVal) { rawVal = rawVal.val; } if (rawVal == null) return; let cmd = null; if (typeof rawVal === "string") { const s = rawVal.trim(); if (!s) return; if (s.startsWith("{") || s.startsWith("[")) cmd = safeJsonParse(s); if (!cmd) cmd = { target: "*", tab: s, ts: Date.now() }; } else if (typeof rawVal === "number") { cmd = { target: "*", tab: rawVal, ts: Date.now() }; } else if (typeof rawVal === "object") { cmd = rawVal; } if (!cmd) return; // Kompatibilität: event-artiges Format akzeptieren if (cmd.target == null && (cmd.deviceId || cmd.deviceName)) { cmd.target = { deviceId: cmd.deviceId, deviceName: cmd.deviceName }; } if (cmd.tab == null) { if (cmd.tabName != null) cmd.tab = cmd.tabName; else if (cmd.tabIndex != null) cmd.tab = cmd.tabIndex; } // ✅ Dedup NUR über cmdId (wenn vorhanden). Ohne cmdId wird NICHT deduped. if (cmd.cmdId != null) { const token = String(cmd.cmdId); if (token === lastCmdToken) return; lastCmdToken = token; } // Zielprüfung applyDeviceNameFromMap(); if (!isTargetForMe(cmd)) return; dlog("CMD accepted:", cmd); if (cmd.view) gotoView(String(cmd.view)); if (cmd.tab != null) forceSelectTab(cmd.tab, !!cmd.force); } // ============================================== // =================== Subscribe/Bind Setup =================== let subscribedIds = new Set(); function subscribeNow(ids) { const v = getVis(); if (!v?.conn?.subscribe) return; try { v.conn.subscribe(ids); ids.forEach(id => subscribedIds.add(id)); dlog("subscribed:", ids); } catch (e) { dlog("subscribe err", e); } } function ensureSubscribeAndBind(ids, onChange) { const v = getVis(); if (!v?.conn?.getStates || !v?.conn?.subscribe || !v?.states?.bind) { return false; } try { v.conn.gettingStates = 0; } catch {} v.conn.getStates(ids, (err, states) => { if (err) dlog("getStates err", err); // subscribe subscribeNow(ids); // cache füllen (hilft je nach Build) try { if (states && typeof v.updateStates === "function") v.updateStates(states); } catch {} ids.forEach(id => { const cb = (e, newVal, oldVal) => onChange(newVal, oldVal, id); // robust: DP und DP.val try { v.states.bind(id + ".val", cb); } catch {} try { v.states.bind(id, cb); } catch {} }); }); return true; } function setupCmdListener() { const ok = ensureSubscribeAndBind([DP_TAB_CMD], (newVal) => { if (isSuppressed()) return; handleTabCmd(newVal); }); if (ok) return true; // Fallback Polling let last = null; const timer = setInterval(() => { try { let v = readVisValue(DP_TAB_CMD); if (typeof v === "object" && v && "val" in v) v = v.val; if (v != null && v !== last) { last = v; if (!isSuppressed()) handleTabCmd(v); } } catch {} }, 500); window.addEventListener("beforeunload", () => clearInterval(timer)); return true; } function setupDeviceMapListener() { const ok = ensureSubscribeAndBind([DP_DEVICE_MAP], () => { if (isSuppressed()) return; const n = applyDeviceNameFromMap(); dlog("deviceName updated:", n); }); applyDeviceNameFromMap(); return ok; } function startPeriodicResubscribe() { setInterval(() => { // immer wieder subscribe, falls VIS2 nach Reconnect „vergisst“ subscribeNow([DP_TAB_CMD, DP_DEVICE_MAP]); }, RESUBSCRIBE_EVERY_MS); } // ============================================== // =================== Init =================== (function init() { const id = getOrCreateDeviceId(); dlog("deviceId:", id); const n = applyDeviceNameFromMap(); dlog("deviceName:", n); applyTabFromUrl(); if (!observeTabChanges()) { const obs = new MutationObserver(() => { if (observeTabChanges()) obs.disconnect(); }); obs.observe(document.body, { childList: true, subtree: true }); setTimeout(() => obs.disconnect(), OBSERVE_TIMEOUT_MS); } setupCmdListener(); setupDeviceMapListener(); startPeriodicResubscribe(); window.addEventListener("popstate", applyTabFromUrl); })(); [image: 1770574125750-vis2-tabcontrol.gif]

616

Online

32.7k

Benutzer

82.4k

Themen

1.3m

Beiträge