// ============================================================================= // Shelly 2PM Detached Script v3.5 // ============================================================================= // Telt alleen btn_down events om valse meldingen van Gira Systeem 55 // (of andere wipknoppen) te verminderen. // Crosstalk-beveiliging voorkomt dat één knop van een dubbele schakelaar // de andere knop triggert. // Geschikt voor: Gen4 2PM, Gen3 2PM // ============================================================================= var CONFIG = { channels: [ { inputId: 0, outputId: 0 }, { inputId: 1, outputId: 1 } ], SETTLE_MS: 300, CROSSTALK_MS: 300, COUNT_SIGNAL: "btn_down", 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" } }; 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); } 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 }); } 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 }); 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 }); } } } 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; } 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); } function onBurstEnd(chIndex) { var s = chState[chIndex]; s.settling = false; s.settleTimer = null; s.lastBurstEnd = Date.now(); var changes = s.stateChanges; s.stateChanges = 0; // v3.5: één btn_down = één fysieke toggle var toggles = changes; if (toggles < 1) toggles = 1; print("[Burst] " + CONFIG.channels[chIndex].name + ": " + JSON.stringify(changes) + " counted events -> " + 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); } 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]; // Crosstalk-beveiliging: blokkeer het eerste event van dit kanaal als het // andere kanaal net actief was. Voorkomt vals triggeren bij Gira Systeem 55 // dubbele schakelaars. if (s.stateChanges === 0 && !s.settling) { if (other.settling) return; if (now - other.lastBurstEnd < CONFIG.CROSSTALK_MS) return; } // Tel alleen het ingestelde signaal. Standaard: btn_down. if (signal === CONFIG.COUNT_SIGNAL || 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; } if (signal === "btn_up") { print("[Ignored] " + CONFIG.channels[chIndex].name + " = " + JSON.stringify(signal)); return; } if (signal === "single_push" || signal === "double_push" || signal === "triple_push" || signal === "long_push") { print("[Native] " + CONFIG.channels[chIndex].name + " = " + JSON.stringify(signal)); } }); print("=== Shelly 2PM Detached v3.5 ==="); print("Geschikt voor: Gen4 2PM, Gen3 2PM"); print("Device: " + deviceName); print("Timing: settle=" + JSON.stringify(CONFIG.SETTLE_MS) + "ms crosstalk=" + JSON.stringify(CONFIG.CROSSTALK_MS) + "ms"); print("Counting only: " + CONFIG.COUNT_SIGNAL); 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");