From 8d8bbbc476d8b93905a2746ee2a5adac0f1d7f07 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 14 Feb 2026 03:25:26 -0500 Subject: [PATCH] Add weather line: temp range + NWS severe alerts (hurricane/tornado/etc) --- index.html | 33 +++++++++++++++++++++--------- server.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index 1c4ff89..981c10d 100644 --- a/index.html +++ b/index.html @@ -22,12 +22,14 @@ body { background: #0a0a0a; color: #e0ddd5; font-family: 'Sora', sans-serif; ove 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; } +#weather-line { text-align: center; font-size: 13px; font-weight: 300; color: #888; margin-top: 4px; letter-spacing: 1px; } +#weather-line .wx-alert { color: #ff2222; font-weight: 600; } /* Cam + sensors row */ -#cam-row { display: flex; align-items: center; gap: 14px; } +#cam-row { display: flex; align-items: stretch; gap: 14px; } /* Room sensors */ -#room-sensors { display: flex; flex-direction: column; gap: 12px; padding: 8px 16px; border-left: 1px solid #2a2a1a; } +#room-sensors { display: flex; flex-direction: column; gap: 12px; padding: 12px 16px; border: 1px solid #222; border-radius: 8px; align-self: stretch; justify-content: center; } #room-sensors .sensor { text-align: center; } #room-sensors .sensor-val { font-size: 26px; font-weight: 300; color: #e0ddd5; font-variant-numeric: tabular-nums; line-height: 1.1; } #room-sensors .sensor-label { font-size: 9px; color: #c8b273; text-transform: uppercase; letter-spacing: 2px; font-weight: 600; } @@ -97,6 +99,7 @@ canvas#clock { width: 180px; height: 180px; }
+
@@ -358,6 +361,21 @@ evtSource.onerror = () => { setTimeout(() => location.reload(), 5000); }; // Resume audio on first interaction document.addEventListener('click', () => { if (audioCtx.state === 'suspended') audioCtx.resume(); }, { once: true }); +// === WEATHER === +async function updateWeather() { + try { + const r = await fetch('/api/weather'); + const d = await r.json(); + let html = `${d.low}–${d.high}°F`; + if (d.alerts && d.alerts.length) { + html += ' ⚠️ ' + d.alerts.join(', ') + ''; + } + document.getElementById('weather-line').innerHTML = html; + } catch(e) {} +} +updateWeather(); +setInterval(updateWeather, 1800000); + // === ROOM SENSORS (7pm - 8am only) === const sensorEl = document.getElementById('room-sensors'); @@ -375,7 +393,7 @@ async function updateSensors() { const tempF = parseFloat(d.temperature); const tempC = ((tempF - 32) * 5/9).toFixed(1); const tempEl = document.getElementById('s-temp'); - tempEl.textContent = Math.round(tempF) + '°'; + tempEl.textContent = tempF.toFixed(1) + '°'; tempEl.title = tempC + '°C'; tempEl.className = 'sensor-val' + (tempF < 73 ? ' cold' : tempF > 76 ? ' crit' : ''); @@ -392,19 +410,14 @@ async function updateSensors() { } updateSensors(); -setInterval(updateSensors, 30000); +setInterval(updateSensors, 5000); // === PULSE-OX CAMERA (7pm - 8am only) === const camImg = document.getElementById('pulse-cam'); const camWrap = document.getElementById('cam-wrap'); -function isCamTime() { - const h = new Date().getHours(); - return h >= 19 || h < 8; -} - function updateCam() { - if (isCamTime()) { + if (isNightTime()) { camWrap.style.display = ''; if (!camImg.src.includes('/stream')) { camImg.src = '/api/cam/pulse-ox/stream'; diff --git a/server.js b/server.js index 4dc60ea..f728564 100644 --- a/server.js +++ b/server.js @@ -61,8 +61,8 @@ app.post('/api/alerts', (req, res) => { // Bedroom 1 sensors proxy from HA const SENSOR_ENTITIES = { - temperature: 'sensor.bed1_temperature', - humidity: 'sensor.bed1_humidity', + temperature: 'sensor.athom_tem_hum_sensor_b3882c_temperature', + humidity: 'sensor.athom_tem_hum_sensor_b3882c_humidity', co2: 'sensor.athom_co2_sensor_b34780_co2' }; @@ -70,7 +70,7 @@ 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) { + if (sensorCache.data && Date.now() - sensorCache.ts < 4000) { return res.json(sensorCache.data); } try { @@ -96,6 +96,60 @@ app.get('/api/sensors/bed1', async (req, res) => { } }); +// Weather: temp range + severe alerts +let weatherCache = { data: null, ts: 0 }; +const SEVERE_EVENTS = ['hurricane', 'tropical storm', 'tornado', 'severe thunderstorm', 'flash flood', 'storm surge', 'tsunami', 'extreme wind']; + +app.get('/api/weather', async (req, res) => { + if (weatherCache.data && Date.now() - weatherCache.ts < 1800000) { + return res.json(weatherCache.data); + } + try { + const result = { high: null, low: null, alerts: [] }; + + // Temp range from wttr.in + const wttr = await new Promise((resolve, reject) => { + const r = require('https').get('https://wttr.in/St+Petersburg+FL?format=j1', { + headers: { 'User-Agent': 'james-dashboard' } + }, (resp) => { + let body = ''; + resp.on('data', c => body += c); + resp.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(e); } }); + }); + r.on('error', reject); + r.setTimeout(10000, () => { r.destroy(); reject(new Error('timeout')); }); + }); + const today = wttr.weather[0]; + result.high = parseInt(today.maxtempF); + result.low = parseInt(today.mintempF); + + // NWS severe alerts + const nws = await new Promise((resolve, reject) => { + const r = require('https').get('https://api.weather.gov/alerts/active?point=27.7676,-82.6403', { + headers: { 'User-Agent': 'james-dashboard' } + }, (resp) => { + let body = ''; + resp.on('data', c => body += c); + resp.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(e); } }); + }); + r.on('error', reject); + r.setTimeout(10000, () => { r.destroy(); reject(new Error('timeout')); }); + }); + for (const f of (nws.features || [])) { + const event = (f.properties.event || '').toLowerCase(); + if (SEVERE_EVENTS.some(s => event.includes(s))) { + result.alerts.push(f.properties.event); + } + } + + weatherCache = { data: result, ts: Date.now() }; + res.json(result); + } catch(e) { + console.error('Weather fetch failed:', e.message); + res.status(502).json({ error: 'weather 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`, {