From 569963c02dbbf5f6715343add01f0a9ba203b5b6 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 14 Feb 2026 02:48:57 -0500 Subject: [PATCH] Add bedroom 1 sensors (temp/humidity/CO2) to night display --- index.html | 47 +++++++++++++++++++++++++++++++++++++++++++++++ server.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/index.html b/index.html index 5800d73..f6a15e6 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,14 @@ canvas#clock { width: 180px; height: 180px; } #digital-time { text-align: center; color: #c8b273; font-size: 21px; font-weight: 300; letter-spacing: 2px; margin-top: 6px; font-variant-numeric: tabular-nums; } #digital-date { text-align: center; color: #666; font-size: 13px; font-weight: 400; letter-spacing: 1px; margin-top: 2px; } +/* Room sensors */ +#room-sensors { display: flex; gap: 20px; justify-content: center; width: 100%; max-width: 280px; padding: 8px 0; } +#room-sensors .sensor { text-align: center; } +#room-sensors .sensor-val { font-size: 22px; font-weight: 300; color: #e0ddd5; font-variant-numeric: tabular-nums; } +#room-sensors .sensor-label { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; } +#room-sensors .sensor-val.warn { color: #d4a050; } +#room-sensors .sensor-val.crit { color: #c45; } + /* Calendar */ #calendar { width: 100%; max-width: 280px; } #cal-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } @@ -78,6 +86,11 @@ canvas#clock { width: 180px; height: 180px; }
+
@@ -339,6 +352,40 @@ evtSource.onerror = () => { setTimeout(() => location.reload(), 5000); }; // Resume audio on first interaction document.addEventListener('click', () => { if (audioCtx.state === 'suspended') audioCtx.resume(); }, { once: true }); +// === ROOM SENSORS (7pm - 8am only) === +const sensorEl = document.getElementById('room-sensors'); + +function isNightTime() { + const h = new Date().getHours(); + return h >= 19 || h < 8; +} + +async function updateSensors() { + if (!isNightTime()) { sensorEl.style.display = 'none'; return; } + sensorEl.style.display = ''; + try { + const r = await fetch('/api/sensors/bed1'); + const d = await r.json(); + const tempF = parseFloat(d.temperature); + const tempC = ((tempF - 32) * 5/9).toFixed(1); + document.getElementById('s-temp').textContent = Math.round(tempF) + '°'; + document.getElementById('s-temp').title = tempC + '°C'; + + const hum = parseFloat(d.humidity); + const humEl = document.getElementById('s-hum'); + humEl.textContent = Math.round(hum) + '%'; + humEl.className = 'sensor-val' + (hum < 30 || hum > 65 ? ' warn' : ''); + + const co2 = parseInt(d.co2); + const co2El = document.getElementById('s-co2'); + co2El.textContent = co2; + co2El.className = 'sensor-val' + (co2 > 1500 ? ' crit' : co2 > 1000 ? ' warn' : ''); + } catch(e) { console.error('Sensor fetch failed:', e); } +} + +updateSensors(); +setInterval(updateSensors, 30000); + // === PULSE-OX CAMERA (7pm - 8am only) === const camImg = document.getElementById('pulse-cam'); const camWrap = document.getElementById('cam-wrap'); diff --git a/server.js b/server.js index 0ea89e3..7e3131d 100644 --- a/server.js +++ b/server.js @@ -58,6 +58,43 @@ app.post('/api/alerts', (req, res) => { res.status(201).json(alert); }); +// Bedroom 1 sensors proxy from HA +const SENSOR_ENTITIES = { + temperature: 'sensor.bed1_temperature', + humidity: 'sensor.bed1_humidity', + co2: 'sensor.athom_co2_sensor_b34780_co2' +}; + +let sensorCache = { data: null, ts: 0 }; + +app.get('/api/sensors/bed1', async (req, res) => { + // Cache for 15s to avoid hammering HA + if (sensorCache.data && Date.now() - sensorCache.ts < 15000) { + return res.json(sensorCache.data); + } + try { + const results = {}; + for (const [key, entity] of Object.entries(SENSOR_ENTITIES)) { + const data = await new Promise((resolve, reject) => { + const r = http.get(`${HA_URL}/api/states/${entity}`, { + headers: { 'Authorization': `Bearer ${HA_TOKEN}` } + }, (haRes) => { + let body = ''; + haRes.on('data', c => body += c); + haRes.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(e); } }); + }); + r.on('error', reject); + r.setTimeout(3000, () => { r.destroy(); reject(new Error('timeout')); }); + }); + results[key] = data.state; + } + sensorCache = { data: results, ts: Date.now() }; + res.json(results); + } catch(e) { + res.status(502).json({ error: 'HA unavailable' }); + } +}); + // Camera proxy - snapshot from HA app.get('/api/cam/pulse-ox', (req, res) => { const haReq = http.get(`${HA_URL}/api/camera_proxy/camera.pulse_ox_live_view`, {