@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]