Add bedroom 1 sensors (temp/humidity/CO2) to night display
This commit is contained in:
parent
4d78baa0f1
commit
569963c02d
47
index.html
47
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; }
|
|||
<div id="clock-wrap"><canvas id="clock" width="440" height="440"></canvas></div>
|
||||
<div id="digital-time"></div>
|
||||
<div id="digital-date"></div>
|
||||
<div id="room-sensors" style="display:none">
|
||||
<div class="sensor"><div class="sensor-val" id="s-temp">--</div><div class="sensor-label">Temp</div></div>
|
||||
<div class="sensor"><div class="sensor-val" id="s-hum">--</div><div class="sensor-label">Humidity</div></div>
|
||||
<div class="sensor"><div class="sensor-val" id="s-co2">--</div><div class="sensor-label">CO₂</div></div>
|
||||
</div>
|
||||
<div id="calendar">
|
||||
<div id="cal-nav">
|
||||
<button id="cal-prev">‹</button>
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
37
server.js
37
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`, {
|
||||
|
|
|
|||
Loading…
Reference in New Issue