// Shelly 3EM Pro with Switch Add-on Script: // Smart Load Switch based on PV Power Flow and Consumption // (c) 2025 Frank Erdorf, Aachen, Germany // Version 1.0.0 // Last change 30.05.2025 // // Features: // + Switches ON when feed-in exceeds configured load power // + Switches OFF when grid consumption is detected // + Uses an exponential running average (focus on recent readings) // + Ignores invalid or extreme readings // + Cooldown mechanism to prevent frequent toggling // + Applies a margin (tolerance) around switching thresholds to avoid rapid toggling near edges // + Cooldown resets when power is significantly beyond the threshold // + Faster startup: begins switching earlier after reboot (half cooldown) // + Starts with the load switched ON at boot // + Works without internet/time sync by using internal elapsed timer // + Respects manual overrides temporarily, at least one cooldown cycle // + To get full manual control you will need to stop the script // + To reset a manual override you will need to restart the script // // Validation Checklist (verify expected switch behavior): // // ✔ Script startup: // - Start the script manually // - Confirm the script is still running // - Check the Activity log: the switch should be ON // // ✔ After device reboot: // - Reboot the device // - The switch should automatically return to ON (see Activity log) // // ✔ Night-Time Anomalies: // - The switch should remain OFF during the night // - No unexpected switch ON events should appear in the log // // ✔ During clear daylight: // - In the morning or at high noon, the switch should turn ON // - It should stay ON for several hours // - In the evening, the switch should turn OFF again // // ✔ Cooldown Enforcement: // - During changeable weather, the switch should toggle ON and OFF // - The log must not show toggles occurring more frequently than the configured cooldown period // // ✔ Manual override: // - Manually change the switch state via app or UI // - Confirm it remains in the manual state for at least one cooldown cycle const DEBUG = false; const SWITCHED_POWER = 2300; // Load wattage to match feed-in (W) const MAX_POWER = 15000; // Maximum valid power reading (W) const COOLDOWN_S = 15 * 60; // Cooldown duration in seconds (15 minutes) const AVG_WINDOW_S = COOLDOWN_S / 2; // Running average window const CHECK_INTERVAL_S = 5; // Power check interval (seconds) const ALPHA = CHECK_INTERVAL_S / AVG_WINDOW_S; const MARGIN_WATTS = 100; // margin in watts, depends on battery reaction times and inverter behavior if (DEBUG) { print("ALPHA =", ALPHA.toFixed(5)); print("MARGIN =", MARGIN_WATTS.toFixed(0), "W"); } let avgPower = 0.0; let currentState = true; // half cooldown at startup let elapsedSeconds = COOLDOWN_S / 2; let lastStateChangeSeconds = 0; // Initialize switch state ON at boot Shelly.call("Switch.Set", { id: 100, on: currentState }); function checkPower() { elapsedSeconds += CHECK_INTERVAL_S; const em = Shelly.getComponentStatus("em:0"); if (!em || typeof em.total_act_power !== "number") { if (DEBUG) print("Skipping: invalid or missing power reading"); return; } const power = em.total_act_power; if (Math.abs(power) > MAX_POWER) { if (DEBUG) print("Skipping: power reading out of range:", power); return; } // Update running average power avgPower = ALPHA * power + (1 - ALPHA) * avgPower; if (DEBUG) { print("P_now =", power.toFixed(1), "W, P_avg =", avgPower.toFixed(1), "W"); print("State =", currentState ? "ON" : "OFF"); } // Determine desired switch state with updated MARGIN logic const desiredState = currentState ? avgPower < -MARGIN_WATTS // Keep ON if still feeding : avgPower < -SWITCHED_POWER - MARGIN_WATTS; // Turn ON if feed-in sufficient const timeDiff = elapsedSeconds - lastStateChangeSeconds; if (desiredState === currentState) { // Reset cooldown if safely far from threshold const farFromThreshold = currentState ? avgPower < -MARGIN_WATTS * 2 : avgPower < -SWITCHED_POWER - MARGIN_WATTS * 2; if (farFromThreshold) { lastStateChangeSeconds = elapsedSeconds; if (DEBUG) print("Cooldown reset (far from threshold)"); } else if (DEBUG) { print("Near threshold — waiting", ((COOLDOWN_S - timeDiff) / 60).toFixed(1), "min"); } } else { // Switch state if cooldown expired if (timeDiff >= COOLDOWN_S) { currentState = desiredState; lastStateChangeSeconds = elapsedSeconds; Shelly.call("Switch.Set", { id: 100, on: currentState }, function (res, err_code, err_msg) { if (DEBUG) { print("Switched", currentState ? "ON" : "OFF", "err_code =", err_code, "err_msg =", err_msg); } }); } else if (DEBUG) { print("Cooldown active —", ((COOLDOWN_S - timeDiff) / 60).toFixed(1), "min left"); } } } Timer.set(CHECK_INTERVAL_S * 1000, true, checkPower);