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`, {