commit ffa243810a7dc111fed549ec4b9921343227d5ea Author: bes-r Date: Wed May 20 20:14:12 2026 +0200 detached-script.js toevoegen diff --git a/detached-script.js b/detached-script.js new file mode 100644 index 0000000..d7e036f --- /dev/null +++ b/detached-script.js @@ -0,0 +1,300 @@ +// ============================================================================= +// Shelly 2PM Detached Script v3.4 +// ============================================================================= +// Based on v3.3 (stable) +// Fix: supports both state-based and event-based inputs (Gen4 compatible) +// Fully English logging & events +// ============================================================================= + +// === CONFIG ================================================================== + +var CONFIG = { + channels: [ + { inputId: 0, outputId: 0 }, + { inputId: 1, outputId: 1 } + ], + + SETTLE_MS: 300, + CROSSTALK_MS: 200, + + LONG_ENABLED: true, + LONG_MIN_MS: 800, + LONG_MAX_MS: 1800, + + EMERGENCY_COUNT: 5, + EMERGENCY_WINDOW_MS: 2500, + + names: { + single: "single", + double: "double", + triple: "triple", + long: "long", + emergency: "emergency" + } +}; + +// === DEVICE INFO ============================================================= + +var deviceInfo = Shelly.getDeviceInfo(); +var deviceName = (deviceInfo.name && deviceInfo.name !== "") + ? deviceInfo.name + : deviceInfo.model + " " + deviceInfo.id; + +for (var ci = 0; ci < CONFIG.channels.length; ci++) { + var inputCfg = Shelly.getComponentConfig("input:" + CONFIG.channels[ci].inputId); + CONFIG.channels[ci].name = (inputCfg && inputCfg.name && inputCfg.name !== "") + ? inputCfg.name + : "Channel " + (ci + 1); +} + +// === STATE =================================================================== + +var chState = []; +for (var si = 0; si < CONFIG.channels.length; si++) { + chState.push({ + stateChanges: 0, + settleTimer: null, + longTimer: null, + settling: false, + waitingForLong: false, + singleTime: 0, + lastBurstEnd: 0, + emergencyCount: 0, + emergencyTimer: null, + emergencyLocked: false + }); +} + +// === EVENT EMIT =============================================================== + +function fireEvent(chIndex, logicalType, toggles) { + var ch = CONFIG.channels[chIndex]; + var eventType = CONFIG.names[logicalType] || logicalType; + + print(">>> " + ch.name + ": " + eventType + + " (" + JSON.stringify(toggles) + " toggles)"); + + Shelly.emitEvent("toggle_event", { + input: ch.inputId, + input_name: ch.name, + output: ch.outputId, + device: deviceName, + type: eventType, + logical_type: logicalType, + toggles: toggles + }); + + // Offline fallback + if (logicalType === "single") { + var w = Shelly.getComponentStatus("wifi"); + if (w === null || w.sta_ip === null || w.sta_ip === "") { + print("[Offline] Local toggle: " + ch.name); + Shelly.call("Switch.Toggle", { id: ch.outputId }); + } + } +} + +// === EMERGENCY =============================================================== + +function resetEmergency(chIndex) { + var s = chState[chIndex]; + s.emergencyCount = 0; + s.emergencyLocked = false; + + if (s.emergencyTimer !== null) { + Timer.clear(s.emergencyTimer); + s.emergencyTimer = null; + } +} + +function registerEmergency(chIndex, toggles) { + var s = chState[chIndex]; + if (toggles < 1) return false; + if (s.emergencyLocked) return true; + + if (s.emergencyCount === 0) { + s.emergencyTimer = Timer.set(CONFIG.EMERGENCY_WINDOW_MS, false, function() { + print("[Emergency] Timeout " + CONFIG.channels[chIndex].name + + " after " + JSON.stringify(s.emergencyCount) + " toggles"); + resetEmergency(chIndex); + }); + } + + s.emergencyCount += toggles; + + print("[Emergency] " + CONFIG.channels[chIndex].name + + ": " + JSON.stringify(s.emergencyCount) + + "/" + JSON.stringify(CONFIG.EMERGENCY_COUNT)); + + if (s.emergencyCount >= CONFIG.EMERGENCY_COUNT) { + s.emergencyLocked = true; + + if (s.emergencyTimer !== null) { + Timer.clear(s.emergencyTimer); + s.emergencyTimer = null; + } + + fireEvent(chIndex, "emergency", s.emergencyCount); + + Timer.set(1000, false, function() { + resetEmergency(chIndex); + }); + + return true; + } + return false; +} + +// === TOGGLE HANDLING ========================================================== + +function emitForToggles(chIndex, toggles) { + if (registerEmergency(chIndex, toggles)) return; + + var type = "single"; + if (toggles >= 3) type = "triple"; + else if (toggles >= 2) type = "double"; + + fireEvent(chIndex, type, toggles); +} + +// === BURST =================================================================== + +function onBurstEnd(chIndex) { + var s = chState[chIndex]; + + s.settling = false; + s.settleTimer = null; + s.lastBurstEnd = Date.now(); + + var changes = s.stateChanges; + s.stateChanges = 0; + + var toggles = ((changes + 1) / 2) | 0; + if (toggles < 1) toggles = 1; + + print("[Burst] " + CONFIG.channels[chIndex].name + + ": " + JSON.stringify(changes) + " changes -> " + + JSON.stringify(toggles) + " toggles"); + + if (s.waitingForLong) { + var elapsed = Date.now() - s.singleTime; + + s.waitingForLong = false; + if (s.longTimer !== null) { + Timer.clear(s.longTimer); + s.longTimer = null; + } + + if (toggles === 1 && + elapsed >= CONFIG.LONG_MIN_MS && + elapsed <= CONFIG.LONG_MAX_MS) { + fireEvent(chIndex, "long", 1); + return; + } + + fireEvent(chIndex, "single", 1); + if (toggles > 0) emitForToggles(chIndex, toggles); + return; + } + + if (toggles === 1 && CONFIG.LONG_ENABLED) { + s.waitingForLong = true; + s.singleTime = Date.now(); + + print("[Long] Waiting for return toggle " + CONFIG.channels[chIndex].name); + + s.longTimer = Timer.set(CONFIG.LONG_MAX_MS, false, function() { + s.waitingForLong = false; + s.longTimer = null; + print("[Long] Timeout -> single"); + emitForToggles(chIndex, 1); + }); + return; + } + + emitForToggles(chIndex, toggles); +} + +// === EVENT HANDLER =========================================================== + +Shelly.addEventHandler(function(event) { + if (event.component === undefined) return; + if (event.info === undefined) return; + + var chIndex = -1; + for (var i = 0; i < CONFIG.channels.length; i++) { + if (event.component === ("input:" + CONFIG.channels[i].inputId)) { + chIndex = i; + break; + } + } + if (chIndex === -1) return; + + var signal = null; + + if (event.info.state !== undefined) { + signal = event.info.state; + } else if (event.info.event !== undefined) { + signal = event.info.event; + } else { + return; + } + + var now = Date.now(); + var s = chState[chIndex]; + var other = chState[(chIndex === 0) ? 1 : 0]; + + if (s.stateChanges === 0 && !s.settling) { + if (other.settling) return; + if (now - other.lastBurstEnd < CONFIG.CROSSTALK_MS) return; + } + + // Count only real toggle transitions + if (signal === "btn_down" || signal === "btn_up" || event.info.state !== undefined) { + + s.stateChanges += 1; + s.settling = true; + + print("[State] " + CONFIG.channels[chIndex].name + + " = " + JSON.stringify(signal) + + " (#" + JSON.stringify(s.stateChanges) + ")"); + + if (s.settleTimer !== null) { + Timer.clear(s.settleTimer); + } + + s.settleTimer = Timer.set(CONFIG.SETTLE_MS, false, function() { + onBurstEnd(chIndex); + }); + + return; + } + + // Show native events for debugging + if (signal === "single_push" || + signal === "double_push" || + signal === "triple_push" || + signal === "long_push") { + print("[Native] " + CONFIG.channels[chIndex].name + + " = " + JSON.stringify(signal)); + } +}); + +// === INIT ==================================================================== + +print("=== Shelly Detached v3.4 ==="); +print("Device: " + deviceName); +print("Timing: settle=" + JSON.stringify(CONFIG.SETTLE_MS) + + "ms crosstalk=" + JSON.stringify(CONFIG.CROSSTALK_MS) + "ms"); +print("Long: " + (CONFIG.LONG_ENABLED ? "enabled" : "disabled")); +print("Emergency: " + CONFIG.EMERGENCY_COUNT + " toggles / " + + CONFIG.EMERGENCY_WINDOW_MS + "ms"); + +for (var pi = 0; pi < CONFIG.channels.length; pi++) { + var ch = CONFIG.channels[pi]; + print(" " + ch.name + + " input:" + ch.inputId + + " -> output:" + ch.outputId); +} + +print("[Script] Active"); \ No newline at end of file