Last active
January 15, 2026 08:36
-
-
Save pacioc193/19fac478c58f06d127cd5307be85904d to your computer and use it in GitHub Desktop.
Blue TRV with XIAOMI LYWSD03MMC as external sensor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * UNIFIED HEATING CONTROLLER v21 (Clean Logs & Always Visible Params) | |
| * * LOGIC: | |
| * - Cycle Time: Every ~30s. | |
| * - TRV Data: Read and Logged EVERY cycle (Always visible). | |
| * - Pushing: Happens only every N cycles (defined by CYCLES_PER_PUSH). | |
| * - Logs: "PUSHING" appears only when data is actually sent. | |
| */ | |
| // --- CONFIGURATION --- | |
| let REMOTE_IP = "192.168.1.232"; | |
| let SAFETY_TIMER = 300; | |
| let CYCLE_MS = 30000; | |
| let RESTART_DELAY = 120000; | |
| let SENSOR_TIMEOUT = 300; | |
| let MIN_BOILER_POS = 5; | |
| let MAX_WAIT_TIME = 20000; | |
| // CONFIGURATION: PUSH FREQUENCY | |
| let CYCLES_PER_PUSH = 2; // 1 = Always, 2 = Every 60s, etc. | |
| // LOGGING | |
| let DEBUG = true; | |
| let PRINT_RAW = false; | |
| // MAPPING | |
| let ZONES = [ | |
| { trvId: 200, sensorId: 204 }, | |
| { trvId: 201, sensorId: 212 }, | |
| { trvId: 202, sensorId: 207 } | |
| ]; | |
| // --- STATE --- | |
| let state = { | |
| lastOff: 0, | |
| lastSent: null, | |
| currentMax: 0, | |
| cycleStartTime: 0, | |
| pendingZones: 0, | |
| watchdogTimer: null, | |
| cycleCount: 0 | |
| }; | |
| function log(msg, isInfo) { | |
| if (isInfo || DEBUG) print((isInfo ? "[INFO] " : "[DEBUG] ") + msg); | |
| } | |
| // --- BOILER CONTROL --- | |
| function setBoiler(turnOn) { | |
| if (state.watchdogTimer) { Timer.clear(state.watchdogTimer); state.watchdogTimer = null; } | |
| let now = Date.now(); | |
| let duration = Math.round((now - state.cycleStartTime) / 1000); | |
| // LOG TEMPI: Sempre visibile | |
| let type = (state.cycleCount % CYCLES_PER_PUSH === 0) ? "FULL" : "FAST"; | |
| log("[SYSTEM] " + type + " Cycle finished in " + duration + "s. Max Valve: " + state.currentMax + "%", true); | |
| let shouldBeOn = turnOn && state.currentMax >= MIN_BOILER_POS; | |
| // Sleep logic if OFF and already OFF | |
| if (!shouldBeOn && state.lastSent === false) { | |
| scheduleNextRun(); | |
| return; | |
| } | |
| // Safety Delay | |
| if (shouldBeOn) { | |
| let diff = now - state.lastOff; | |
| if (diff < RESTART_DELAY && state.lastSent === false) { | |
| log("[SYSTEM] ⏳ Safety Delay (" + Math.ceil((RESTART_DELAY - diff)/1000) + "s)", true); | |
| scheduleNextRun(); | |
| return; | |
| } | |
| } | |
| let url = "http://" + REMOTE_IP + "/rpc/Switch.Set?id=0&on=" + shouldBeOn; | |
| if (shouldBeOn) url += "&toggle_after=" + SAFETY_TIMER; | |
| Shelly.call("HTTP.GET", { url: url }, function(r, e, m) { | |
| if (e === 0) { | |
| let isRefresh = (shouldBeOn && state.lastSent === true); | |
| state.lastSent = shouldBeOn; | |
| if (!shouldBeOn) state.lastOff = Date.now(); | |
| if (isRefresh) { | |
| if(DEBUG) print("[DEBUG] Heartbeat sent (Boiler stays ON)"); | |
| } else { | |
| log("[SYSTEM] Boiler -> " + (shouldBeOn ? "🔥 ON" : "❄️ OFF"), true); | |
| } | |
| } else { | |
| log("[SYSTEM] 🔴 HTTP Error: " + e, true); | |
| } | |
| scheduleNextRun(); | |
| }); | |
| } | |
| // --- ZONE PROCESSING --- | |
| function processZone(zone) { | |
| let done = function() { | |
| if (state.pendingZones > 0) { | |
| state.pendingZones--; | |
| if (state.pendingZones === 0) setBoiler(state.currentMax > 0); | |
| } | |
| }; | |
| // 1. ALWAYS GET TRV STATUS (Read Only) | |
| Shelly.call("BluTrv.GetStatus", { id: zone.trvId }, function(trvRes, trvErr) { | |
| if (state.pendingZones === -1) return; | |
| if (trvErr !== 0 || !trvRes) { | |
| log("[Zone " + zone.trvId + "] 🔴 TRV OFFLINE", true); | |
| done(); | |
| return; | |
| } | |
| let trvPos = (trvRes.pos !== undefined) ? trvRes.pos : 0; | |
| let trvTemp = trvRes.current_C || "?"; | |
| let trvTarget = trvRes.target_C || "?"; | |
| if (trvPos > state.currentMax) state.currentMax = trvPos; | |
| // BASE MESSAGE (Always visible) | |
| let logMsg = "[Zone " + zone.trvId + "] 🟢 " + trvPos + "% | Set: " + trvTarget + "° | In: " + trvTemp + "°"; | |
| // DECISION: Push or Skip? | |
| let doPush = (state.cycleCount % CYCLES_PER_PUSH === 0); | |
| // CASE A: NO PUSH (Fast Cycle) | |
| if (!doPush || zone.sensorId === null) { | |
| log(logMsg + " (No Push)", false); // Shows parameters but indicates no push | |
| done(); | |
| return; | |
| } | |
| // CASE B: PUSH (Full Cycle) | |
| Shelly.call("BTHomeSensor.GetStatus", { id: zone.sensorId }, function(sensRes, sensErr) { | |
| if (state.pendingZones === -1) return; | |
| if (sensErr === 0 && sensRes && sensRes.value !== undefined) { | |
| let age = (Date.now() / 1000) - sensRes.last_updated_ts; | |
| let extTemp = sensRes.value; | |
| if (age < SENSOR_TIMEOUT) { | |
| let params = { id: 0, t_C: extTemp }; | |
| // Append "Out" and "PUSHING" only here | |
| log(logMsg + " | Out: " + extTemp + "° -> PUSHING", true); | |
| Shelly.call("BluTrv.Call", { id: zone.trvId, method: "TRV.SetExternalTemperature", params: params }, function(res, err) { | |
| if (err !== 0) log("[Zone " + zone.trvId + "] ⚠️ Push Warn", false); | |
| done(); | |
| }); | |
| } else { | |
| log(logMsg + " | Out: 🔴 OLD", true); | |
| done(); | |
| } | |
| } else { | |
| log(logMsg + " | Out: 🔴 SENS ERROR", true); | |
| done(); | |
| } | |
| }); | |
| }); | |
| } | |
| // --- MAIN LOOP --- | |
| function runParallelCycle() { | |
| state.currentMax = 0; | |
| state.cycleStartTime = Date.now(); | |
| state.pendingZones = ZONES.length; | |
| state.cycleCount++; | |
| // Watchdog | |
| state.watchdogTimer = Timer.set(MAX_WAIT_TIME, false, function() { | |
| if (state.pendingZones > 0) { | |
| log("⚠️ WATCHDOG FORCE", true); | |
| state.pendingZones = -1; | |
| setBoiler(state.currentMax > 0); | |
| } | |
| }); | |
| for (let i = 0; i < ZONES.length; i++) processZone(ZONES[i]); | |
| } | |
| // --- SCHEDULING --- | |
| function scheduleNextRun() { | |
| let now = Date.now(); | |
| let elapsed = now - state.cycleStartTime; | |
| let nextDelay = CYCLE_MS - elapsed; | |
| if (nextDelay < 1000) nextDelay = 1000; | |
| Timer.set(nextDelay, false, runParallelCycle); | |
| } | |
| // --- START --- | |
| log("🚀 Script v21 (Full Visibility). Starting...", true); | |
| Timer.set(1000, false, runParallelCycle); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment