Daves-Windsor

Window controller

Oct 5th, 2025 (edited)
68
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Arduino 19.53 KB | Source Code | 0 0
  1. #include <ESP8266WiFi.h>
  2. #include <ESP8266WebServer.h>
  3. #include <DHT.h>
  4. #include <EEPROM.h>
  5.  
  6. // —— Calibration ——
  7. // Adjust after testing your motor + microstepping
  8. const int FULL_STEPS = 800;  
  9.  
  10. // —— Wi-Fi & HTTP Server ——
  11. const char* ssid = "******";
  12. const char* password = "*****";
  13. ESP8266WebServer server(80);
  14. // —— Static IP Config ——
  15. IPAddress local_IP(192,168,11,101);   // pick an unused address on your LAN
  16. IPAddress gateway(192,168,11,1);      // your router
  17. IPAddress subnet(255,255,255,0);      // standard mask
  18.  
  19. // —— Sensors & Stepper ——
  20. #define DHTPIN      D5
  21. #define DHTTYPE     DHT11
  22. #define RAIN_PIN    A0
  23. #define RAIN_THRESH 500
  24. DHT dht(DHTPIN, DHTTYPE);
  25.  
  26. // Stepper driver pins (HW-135 / A4988 / DRV8825)
  27. #define STEP_PIN D1
  28. #define DIR_PIN  D2
  29.  
  30. // Motion state (non-blocking)
  31. volatile bool motorActive = false;
  32. volatile int motorDir = 0;          // 0 = close, 1 = open
  33. volatile long motorStepsRemaining = 0;
  34. unsigned long lastStepMicros = 0;
  35. const int stepDelay = 800;          // µs between steps
  36.  
  37. // —— App State ——
  38. bool  autoMode      = false;
  39. bool  windowLocked  = true;
  40. float thresholdTemp = 25.0;
  41. float currTemp      = NAN;
  42. bool  isRaining     = false;
  43. int   windowPos     = 0;        // 0–100%
  44. const String unlockPin = "1972";
  45.  
  46. // —— EEPROM Addresses ——
  47. const int ADDR_THRESHOLD = 0;
  48. const int ADDR_WINDOWPOS = ADDR_THRESHOLD + sizeof(float);
  49. const int ADDR_AUTOMODE  = ADDR_WINDOWPOS + sizeof(int);
  50. void setup() {
  51.   Serial.begin(115200);
  52.   delay(100);if (!WiFi.config(local_IP, gateway, subnet)) {
  53.   Serial.println("⚠️  Failed to configure static IP");
  54. }
  55.   WiFi.begin(ssid, password);
  56.   Serial.print("Connecting");
  57.   while (WiFi.status() != WL_CONNECTED) {
  58.     delay(500);
  59.     Serial.print('.');
  60.   }
  61.   Serial.println("\nIP = " + WiFi.localIP().toString());
  62.  
  63.   dht.begin();
  64.   EEPROM.begin(512);
  65.  
  66.   // Load threshold
  67.   EEPROM.get(ADDR_THRESHOLD, thresholdTemp);
  68.   if (isnan(thresholdTemp) || thresholdTemp < 10 || thresholdTemp > 50) {
  69.     thresholdTemp = 25.0;
  70.   }
  71.  
  72.   // Load saved window position if valid
  73.   EEPROM.get(ADDR_WINDOWPOS, windowPos);
  74.   if (windowPos < 0 || windowPos > 100) {
  75.     windowPos = 0;
  76.   }
  77.  
  78.   // Load autoMode
  79.   byte modeByte = 0;
  80.   EEPROM.get(ADDR_AUTOMODE, modeByte);
  81.   autoMode = (modeByte == 1);
  82.  
  83.   // Stepper pins
  84.   pinMode(STEP_PIN, OUTPUT);
  85.   pinMode(DIR_PIN, OUTPUT);
  86.  
  87.   // HTTP endpoints
  88.   server.on("/",             HTTP_GET, handleRoot);
  89.   server.on("/status",       HTTP_GET, handleStatus);
  90.   server.on("/open",         HTTP_GET, handleOpen);
  91.   server.on("/close",        HTTP_GET, handleClose);
  92.   server.on("/vent",         HTTP_GET, handleVent);
  93.   server.on("/toggleAuto",   HTTP_GET, handleToggleAuto);
  94.   server.on("/setThreshold", HTTP_GET, handleSetThreshold);
  95.   server.on("/toggleLock",   HTTP_GET, handleToggleLock);
  96.  
  97.   server.begin();
  98. }
  99.  
  100. void loop() {
  101.   server.handleClient();
  102.   handleMotor();   // keep motor moving in background
  103.  
  104.   static unsigned long last = 0;
  105.   if (millis() - last < 2000) return;
  106.   last = millis();
  107.  
  108.   currTemp  = dht.readTemperature();
  109.   isRaining = (analogRead(RAIN_PIN) < RAIN_THRESH);
  110.  
  111.   // Force close on rain
  112.   if (isRaining && !windowLocked && windowPos > 0) {
  113.     startMoveByPercent(0, windowPos);
  114.     windowPos = 0;
  115.     saveWindowPos();
  116.     Serial.println("Rain detected → auto-closing window");
  117.   }
  118.  
  119.   // auto-mode window control
  120.   if (autoMode && !windowLocked) {
  121.     if (currTemp > thresholdTemp && !isRaining && windowPos < 100) {
  122.       startMoveByPercent(1, 100 - windowPos);
  123.       windowPos = 100;
  124.       saveWindowPos();
  125.     }
  126.     else if ((currTemp < thresholdTemp || isRaining) && windowPos > 0) {
  127.       startMoveByPercent(0, windowPos);
  128.       windowPos = 0;
  129.       saveWindowPos();
  130.     }
  131.   }
  132. }
  133. // Start a move by percent (non-blocking)
  134. void startMoveByPercent(int direction, int percentDelta) {
  135.   percentDelta = constrain(percentDelta, 0, 100);
  136.   long steps = (FULL_STEPS * percentDelta) / 100;
  137.   if (steps <= 0) return;
  138.  
  139.   motorDir = direction;
  140.   digitalWrite(DIR_PIN, motorDir ? HIGH : LOW);
  141.   motorStepsRemaining = steps;
  142.   motorActive = true;
  143.   lastStepMicros = micros();
  144. }
  145.  
  146. // Called in loop() to advance motor
  147. void handleMotor() {
  148.   if (!motorActive) return;
  149.  
  150.   unsigned long now = micros();
  151.   if (now - lastStepMicros >= stepDelay) {
  152.     digitalWrite(STEP_PIN, HIGH);
  153.     delayMicroseconds(2);
  154.     digitalWrite(STEP_PIN, LOW);
  155.  
  156.     motorStepsRemaining--;
  157.     lastStepMicros = now;
  158.  
  159.     if (motorStepsRemaining <= 0) {
  160.       motorActive = false;
  161.     }
  162.   }
  163. }
  164.  
  165. // EEPROM helper
  166. void saveWindowPos() {
  167.   static int lastPos = -1;
  168.   if (windowPos != lastPos) {
  169.     EEPROM.put(ADDR_WINDOWPOS, windowPos);
  170.     EEPROM.commit();
  171.     lastPos = windowPos;
  172.   }
  173. }
  174. // ——— Handlers ———
  175.  
  176. void handleStatus() {
  177.   String j = "{";
  178.   j += "\"temp\":"      + String(isnan(currTemp) ? -1 : currTemp, 1) + ",";
  179.   j += "\"threshold\":" + String(thresholdTemp, 1) + ",";
  180.   j += "\"rain\":";     j += (isRaining ? "true" : "false"); j += ",";
  181.   j += "\"windowPos\":" + String(windowPos) + ",";
  182.   j += "\"auto\":";     j += (autoMode ? "true" : "false"); j += ",";
  183.   j += "\"locked\":";   j += (windowLocked ? "true" : "false");
  184.   j += "}";
  185.  
  186.   server.sendHeader("Content-Type",            "application/json; charset=utf-8");
  187.   server.sendHeader("Cache-Control",           "no-store, must-revalidate");
  188.   server.sendHeader("X-Content-Type-Options", "nosniff");
  189.   server.send(200, "application/json", j);
  190. }
  191.  
  192. void handleOpen() {
  193.   if (windowLocked) { server.send(403, "text/plain", "Locked"); return; }
  194.   int delta = 100 - windowPos;
  195.   if (delta > 0) {
  196.     startMoveByPercent(1, delta);
  197.     windowPos = 100;
  198.     saveWindowPos();
  199.   }
  200.   server.send(200, "text/plain", "Open");
  201. }
  202.  
  203. void handleClose() {
  204.   if (windowLocked) { server.send(403, "text/plain", "Locked"); return; }
  205.   int delta = windowPos;
  206.   if (delta > 0) {
  207.     startMoveByPercent(0, delta);
  208.     windowPos = 0;
  209.     saveWindowPos();
  210.   }
  211.   server.send(200, "text/plain", "Close");
  212. }
  213.  
  214. void handleVent() {
  215.   if (windowLocked) { server.send(403, "text/plain", "Locked"); return; }
  216.   int newPos = min(windowPos + 25, 100);
  217.   int delta  = newPos - windowPos;
  218.   if (delta > 0) {
  219.     startMoveByPercent(1, delta);
  220.     windowPos = newPos;
  221.     saveWindowPos();
  222.   }
  223.   server.send(200, "text/plain", "Vent");
  224. }
  225.  
  226. void handleToggleAuto() {
  227.   autoMode = !autoMode;
  228.   byte modeByte = autoMode ? 1 : 0;
  229.   EEPROM.put(ADDR_AUTOMODE, modeByte);
  230.   EEPROM.commit();
  231.   Serial.println(autoMode ? "Auto mode enabled" : "Manual mode enabled");
  232.   server.send(200, "text/plain", autoMode ? "Auto" : "Manual");
  233. }
  234.  
  235. void handleSetThreshold() {
  236.   if (!server.hasArg("value")) { server.send(400, "text/plain", "No value"); return; }
  237.   float v = server.arg("value").toFloat();
  238.   if (v < 10 || v > 50) { server.send(400, "text/plain", "Out of range"); return; }
  239.   thresholdTemp = v;
  240.   EEPROM.put(ADDR_THRESHOLD, thresholdTemp);
  241.   EEPROM.commit();
  242.   server.send(200, "text/plain", "Threshold set");
  243. }
  244.  
  245. void handleToggleLock() {
  246.   if (!server.hasArg("pin")) { server.send(400, "text/plain", "No PIN"); return; }
  247.   if (server.arg("pin") == unlockPin) {
  248.     windowLocked = !windowLocked;
  249.     server.send(200, "text/plain", windowLocked ? "Locked" : "Unlocked");
  250.   } else {
  251.     server.send(403, "text/plain", "Bad PIN");
  252.   }
  253. }
  254.  
  255. void handleRoot() {
  256.   // … your existing HTML/JS payload here …
  257.   // (unchanged from your original sketch)
  258.  
  259.  
  260.   String html = R"rawliteral(
  261. <!DOCTYPE html>
  262. <html lang="en">
  263. <head>
  264. <meta charset="utf-8">
  265. <title>LCARS Window Ctrl</title>
  266. <meta name="viewport" content="width=device-width, initial-scale=1">
  267. <style>
  268.  html, body { margin:0; padding:0; height:100%; overflow:hidden;
  269.               background:#000; color:#0FA; font-family:Segoe UI, sans-serif; }
  270.  #starfield { position:fixed; top:0; left:0; width:100%; height:100%; z-index:0; }
  271.  #clock { position:absolute; top:8px; right:12px; color:#0FA; font-size:0.9em; z-index:2; }
  272.  #weatherCurrent {
  273.    position:absolute; top:8px; left:12px; display:flex; align-items:center;
  274.    color:#0FA; font-size:0.9em; z-index:2;
  275.  }
  276.  #weatherCurrent img { width:24px; height:24px; margin-right:6px; }
  277.  #panel {
  278.    position:absolute; top:40px; left:50%; transform:translateX(-50%);
  279.    display:flex; flex-direction:column; align-items:center; z-index:1;
  280.  }
  281.  #dashboard {
  282.    display:flex; justify-content:space-around;
  283.    width:100%; max-width:540px; margin-bottom:16px;
  284.  }
  285.  .gauge-container {
  286.    position:relative; width:140px; border:2px solid #0FA;
  287.    border-radius:6px; padding:8px; background:rgba(0,0,0,0.5);
  288.    cursor:pointer;
  289.  }
  290.  .gauge-container .threshold-text {
  291.    margin-top:4px; font-size:0.85em; color:#0FA; text-align:center;
  292.  }
  293.  .lock-text {
  294.    position:absolute; top:50%; left:50%;
  295.    transform:translate(-50%,-50%);
  296.    color:rgba(255,0,0,0.8); font-size:14px;
  297.    font-weight:bold; display:none;
  298.  }
  299.  #controls {
  300.    display:flex; flex-wrap:wrap; justify-content:center;
  301.    gap:8px; background:rgba(0,0,0,0.5); padding:8px 0;
  302.  }
  303.  #controls button, #controls input {
  304.    margin:4px; padding:6px 12px; border:none; border-radius:4px;
  305.    background:#0FA; color:#000; font-size:0.9em; cursor:pointer;
  306.    transition: box-shadow 0.2s, transform 0.1s;
  307.  }
  308.  #controls button:hover { background:#0CF; }
  309.  #controls button:active {
  310.    box-shadow:0 0 12px #0FA, 0 0 24px #0FA; transform:scale(0.95);
  311.  }
  312.  #modal {
  313.    display:none; position:fixed; inset:0;
  314.    background:rgba(0,0,0,0.8); justify-content:center; align-items:center;
  315.    z-index:2;
  316.  }
  317.  #modal.show { display:flex; }
  318.  #modal-content {
  319.    background:#111; border:2px solid #0FA; border-radius:8px;
  320.    padding:16px; max-height:80vh; width:90%; max-width:400px;
  321.    overflow-y:auto; position:relative;
  322.  }
  323.  #modal-close {
  324.    position:absolute; top:8px; right:8px; background:#0FA; color:#000;
  325.    border:none; width:28px; height:28px; border-radius:50%; cursor:pointer;
  326.    font-weight:bold;
  327.  }
  328.  #historyChart { width:100%!important; height:200px!important; }
  329. </style>
  330. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  331. </head>
  332. <body>
  333.  
  334. <canvas id="starfield"></canvas>
  335. <div id="clock"></div>
  336. <div id="weatherCurrent">
  337.   <img id="weatherIcon" src="" alt="">
  338.   <span id="weatherText"></span>
  339. </div>
  340.  
  341. <div id="panel">
  342.   <div id="dashboard">
  343.     <div class="gauge-container">
  344.       <canvas id="gaugeTemp" width="120" height="120"></canvas>
  345.       <div>Temp</div>
  346.       <div id="tempThresholdDisplay" class="threshold-text"></div>
  347.     </div>
  348.     <div class="gauge-container">
  349.       <canvas id="gaugeRain" width="120" height="120"></canvas>
  350.       <div>Rain</div>
  351.       <div id="rainForecast" class="threshold-text"></div>
  352.     </div>
  353.     <div class="gauge-container">
  354.       <canvas id="gaugeWin" width="120" height="120"></canvas>
  355.       <div>Window</div>
  356.       <div id="lockOverlay" class="lock-text">LOCKED</div>
  357.     </div>
  358.   </div>
  359.   <div id="controls">
  360.     <button onclick="openWindow()">Open</button>
  361.     <button onclick="closeWindow()">Close</button>
  362.     <button onclick="ventWindow()">Vent</button>
  363.     <button onclick="toggleAuto()">Mode: <span id="modeBtn"></span></button>
  364.     <input id="thresholdInput" type="number" step="0.1" min="10" max="50" placeholder="Temp Threshold">
  365.     <button onclick="updateThreshold()">Set</button>
  366.     <input id="pinInput" type="password" placeholder="PIN">
  367.     <button onclick="toggleLock()">Lock/Unlock</button>
  368.   </div>
  369. </div>
  370.  
  371. <div id="modal">
  372.   <div id="modal-content">
  373.     <button id="modal-close">×</button>
  374.     <h3 id="modal-title">History</h3>
  375.     <canvas id="historyChart"></canvas>
  376.   </div>
  377. </div>
  378.  
  379. <script>
  380.   // Starfield & dynamic star color
  381.   const sf = document.getElementById('starfield'),
  382.         sfc = sf.getContext('2d');
  383.   let stars = [], starColor = '#FFF';
  384.   function resizeSF(){ sf.width = innerWidth; sf.height = innerHeight; }
  385.   window.addEventListener('resize', resizeSF);
  386.   resizeSF();
  387.   class Star {
  388.     constructor(){ this.reset(); }
  389.     reset(){
  390.       this.x = Math.random()*sf.width;
  391.       this.y = Math.random()*sf.height;
  392.       this.z = Math.random()*sf.width;
  393.     }
  394.     update(){
  395.       this.z -= 4;
  396.       if (this.z <= 0) this.reset();
  397.     }
  398.     draw(){
  399.       const x = (this.x - sf.width/2)*(sf.width/this.z) + sf.width/2,
  400.             y = (this.y - sf.height/2)*(sf.width/this.z) + sf.height/2,
  401.             sz= 1.5*(sf.width/this.z);
  402.       sfc.fillStyle = starColor;
  403.       sfc.beginPath(); sfc.arc(x,y,sz,0,2*Math.PI); sfc.fill();
  404.     }
  405.   }
  406.   for(let i=0; i<150; i++) stars.push(new Star());
  407.   (function anim(){
  408.     sfc.fillStyle = 'rgba(0,0,0,0.3)';
  409.     sfc.fillRect(0,0,sf.width,sf.height);
  410.     stars.forEach(s=>{ s.update(); s.draw(); });
  411.     requestAnimationFrame(anim);
  412.   })();
  413.  
  414.   // Gauge class with bottom gap
  415.   class Gauge {
  416.     constructor(id, min, max, yellowThreshold, unit) {
  417.       this.c      = document.getElementById(id);
  418.       this.ctx    = this.c.getContext('2d');
  419.       this.min    = min;
  420.       this.max    = max;
  421.       this.yellow = yellowThreshold;
  422.       this.unit   = unit;
  423.       this.val    = min;
  424.     }
  425.     draw() {
  426.       const ctx    = this.ctx,
  427.             w      = this.c.width,
  428.             h      = this.c.height,
  429.             gap    = Math.PI/6,
  430.             start  = Math.PI*1.5 + gap/2,
  431.             range  = 2*Math.PI - gap,
  432.             pct    = (this.val - this.min)/(this.max - this.min),
  433.             theta  = start + range*pct;
  434.       ctx.clearRect(0,0,w,h);
  435.       // background
  436.       ctx.lineWidth   = 6;
  437.       ctx.strokeStyle = '#333';
  438.       ctx.beginPath(); ctx.arc(w/2,h/2,w/2-8,start,start+range,false); ctx.stroke();
  439.       // value arc
  440.       ctx.strokeStyle = this.val > this.yellow ? '#F00' : '#0FA';
  441.       ctx.beginPath(); ctx.arc(w/2,h/2,w/2-8,start,theta,false); ctx.stroke();
  442.       // center text
  443.       ctx.fillStyle = '#FFF';
  444.       ctx.font      = '18px Segoe UI';
  445.       ctx.textAlign = 'center';
  446.       ctx.fillText(this.val.toFixed(1) + this.unit, w/2, h/2 + 8);
  447.     }
  448.     update(v) {
  449.       this.val = v;
  450.       this.draw();
  451.     }
  452.   }
  453.  
  454.   // Instantiate & initial draw
  455.   const gT = new Gauge('gaugeTemp',  0, 50, 30, '°C'),
  456.         gR = new Gauge('gaugeRain',  0,100, 50, '%'),
  457.         gW = new Gauge('gaugeWin',   0,100, 50, '%');
  458.   gT.draw(); gR.draw(); gW.draw();
  459.  
  460.   // Element references
  461.   const clockEl          = document.getElementById('clock'),
  462.         rainForecastEl   = document.getElementById('rainForecast'),
  463.         tempThresholdEl  = document.getElementById('tempThresholdDisplay'),
  464.         lockOverlay      = document.getElementById('lockOverlay'),
  465.         modeBtn          = document.getElementById('modeBtn'),
  466.         weatherIconEl    = document.getElementById('weatherIcon'),
  467.         weatherTextEl    = document.getElementById('weatherText');
  468.        
  469.  
  470.       // History buffers
  471.   const hx = [], hT = [], hR = [], hW = [], MAX = 20;
  472.  
  473.   async function refreshStatus() {
  474.     try {
  475.       const res = await fetch('/status'),
  476.             st  = await res.json();
  477.       // update gauges & UI
  478.       gT.update(Math.max(0, st.temp));
  479.       gR.update(st.rain ? 100 : 0);
  480.       gW.update(st.windowPos);
  481.       modeBtn.textContent = st.auto ? 'Auto' : 'Manual';
  482.       tempThresholdEl.textContent = thresholdTemp.toFixed(1) + '°C';
  483.       lockOverlay.style.display = st.locked ? 'block' : 'none';
  484.       // dynamic star color from blue→red over 0–40°C
  485.       let ratio = Math.min(Math.max(st.temp/40,0),1);
  486.       let r = Math.round(255*ratio), b = Math.round(255*(1-ratio));
  487.       starColor = `rgb(${r},0,${b})`;
  488.       // history
  489.       const t = new Date().toLocaleTimeString();
  490.       hx.push(t); hT.push(Math.round(st.temp));
  491.       hR.push(st.rain?100:0); hW.push(st.windowPos);
  492.       if (hx.length > MAX) { hx.shift(); hT.shift(); hR.shift(); hW.shift(); }
  493.     } catch(e) {
  494.       console.warn('refreshStatus error', e);
  495.     }
  496.   }
  497.   setInterval(refreshStatus, 2000);
  498.   refreshStatus();
  499.  
  500.   // Chart.js stepped-bar history
  501.   const modal      = document.getElementById('modal'),
  502.         modalClose = document.getElementById('modal-close'),
  503.         titleEl    = document.getElementById('modal-title'),
  504.         ctxChart   = document.getElementById('historyChart').getContext('2d');
  505.   const historyChart = new Chart(ctxChart, {
  506.     type:'bar',
  507.     data:{ labels:[], datasets:[{ label:'', data:[], backgroundColor:'#0FA' }] },
  508.     options:{
  509.       responsive:true,
  510.       maintainAspectRatio:false,
  511.       scales:{ y:{ beginAtZero:true, max:100 } }
  512.     }
  513.   });
  514.   document.querySelectorAll('.gauge-container').forEach(box => {
  515.     box.addEventListener('click', () => {
  516.       const cid = box.querySelector('canvas').id;
  517.       let dataArr, label;
  518.       if (cid === 'gaugeTemp')  { dataArr = hT; label = 'Temp (°C)'; }
  519.       else if (cid === 'gaugeRain'){ dataArr = hR; label = 'Rain (%)'; }
  520.       else                        { dataArr = hW; label = 'Window (%)'; }
  521.       historyChart.data.labels = hx;
  522.       historyChart.data.datasets[0].data = dataArr;
  523.       historyChart.data.datasets[0].label = label;
  524.       historyChart.update();
  525.       titleEl.textContent = label + ' History';
  526.       modal.classList.add('show');
  527.     });
  528.   });
  529.   modalClose.addEventListener('click', () => modal.classList.remove('show'));
  530.  
  531.   // WeatherAPI.com integration
  532.   async function fetchWeather() {
  533.     try {
  534.       const key = '934ffa93bb054c47a97121247251109';      // ← your WeatherAPI.com key
  535.       const lat = 51.307, lon = 0.26;
  536.       const url = `https://api.weatherapi.com/v1/forecast.json`
  537.                 + `?key=${key}&q=${lat},${lon}&days=2&aqi=no&alerts=no`;
  538.  
  539.       const res  = await fetch(url);
  540.       const data = await res.json();
  541.  
  542.       // tomorrow’s rain chance
  543.       const pop = data.forecast.forecastday[1].day.daily_chance_of_rain;
  544.       rainForecastEl.textContent = `${pop}% Chance Of Rain`;
  545.  
  546.       // current weather
  547.       const curr = data.current;
  548.       weatherIconEl.src = 'https:' + curr.condition.icon;
  549.       weatherIconEl.alt = curr.condition.text;
  550.       weatherTextEl.textContent = `${Math.round(curr.temp_c)}°C – ${curr.condition.text}`;
  551.     } catch(e) {
  552.       console.warn('fetchWeather error', e);
  553.       rainForecastEl.textContent  = 'Forecast error';
  554.       weatherIconEl.style.display = 'none';
  555.       weatherTextEl.textContent   = 'No weather';
  556.     }
  557.   }
  558.   fetchWeather();
  559.   setInterval(fetchWeather, 60*60*1000);
  560.  
  561.   // Real-time clock
  562.   function updateClock() {
  563.     clockEl.textContent = new Date().toLocaleTimeString();
  564.   }
  565.   setInterval(updateClock, 1000);
  566.   updateClock();
  567.  
  568.   // Control handlers
  569.   function openWindow()  { fetch('/open'); }
  570.   function closeWindow() { fetch('/close'); }
  571.   function ventWindow()  { fetch('/vent'); }
  572.   function toggleAuto()  { fetch('/toggleAuto'); }
  573.   function updateThreshold(){
  574.     const v = parseFloat(document.getElementById('thresholdInput').value);
  575.     if (isNaN(v)) return;
  576.     fetch('/setThreshold?value=' + v).then(_=>{
  577.       thresholdTemp = v;
  578.       tempThresholdEl.textContent = v.toFixed(1) + '°C';
  579.     });
  580.   }
  581.   function toggleLock() {
  582.     fetch('/toggleLock?pin=' + document.getElementById('pinInput').value);
  583.   }
  584.    setInterval(refresh, 2000);
  585. </script>
  586. </body>
  587. </html>
  588. )rawliteral";
  589.  
  590.  server.sendHeader("Content-Type",            "text/html; charset=utf-8");
  591.  server.sendHeader("Cache-Control",           "no-store, must-revalidate");
  592.  server.sendHeader("X-Content-Type-Options", "nosniff");
  593.  server.send(200, "text/html; charset=utf-8", html);
  594. }
  595.  
Advertisement
Add Comment
Please, Sign In to add comment