Skip to content

Instantly share code, notes, and snippets.

@pacioc193
Last active January 15, 2026 08:36
Show Gist options
  • Select an option

  • Save pacioc193/19fac478c58f06d127cd5307be85904d to your computer and use it in GitHub Desktop.

Select an option

Save pacioc193/19fac478c58f06d127cd5307be85904d to your computer and use it in GitHub Desktop.
Blue TRV with XIAOMI LYWSD03MMC as external sensor
/**
* 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