/* Station tab: live readings from the personal weather station, in the former /conditions look. app.js calls renderStationTab(device) whenever new station data arrives. */ (() => { 'use strict'; const root = document.getElementById('station-live'); if (!root) return; const cardinal = (deg) => (deg == null ? '—' : ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'][Math.round(+deg / 22.5) % 16]); const metric = () => typeof state !== 'undefined' && state.units !== 'us'; const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null); const fTemp = (f) => (num(f) == null ? '—' : String(Math.round(metric() ? (f - 32) * 5 / 9 : f))); const fSpeed = (m) => (num(m) == null ? '—' : String(Math.round(metric() ? m * 1.60934 : m))); const fRain = (i) => (num(i) == null ? '—' : metric() ? (i * 25.4).toFixed(1) : i.toFixed(2)); const fPres = (i) => (num(i) == null ? '—' : metric() ? String(Math.round(i * 33.8639)) : i.toFixed(2)); const units = () => ({ t: metric() ? '°C' : '°F', s: metric() ? 'km/h' : 'mph', r: metric() ? 'mm' : 'in', p: metric() ? 'hPa' : 'inHg' }); function tempColor(f) { if (f == null) return 'var(--sl-text)'; if (f <= 32) return '#93c5fd'; if (f <= 50) return '#60a5fa'; if (f <= 65) return 'var(--sl-text)'; if (f <= 80) return '#f59e0b'; if (f <= 90) return '#f97316'; return '#ef4444'; } function uvInfo(uv) { if (uv == null) return ['', 'var(--sl-text)']; if (uv < 3) return ['Low', '#4ade80']; if (uv < 6) return ['Moderate', '#facc15']; if (uv < 8) return ['High', '#f97316']; if (uv < 11) return ['Very High', '#ef4444']; return ['Extreme', '#d946ef']; } function conditionText(d) { if (!d) return '—'; if (d.rainratein > 0.05) return '🌧 Raining'; const s = d.solarradiation; if (s != null) { if (s > 700) return '☀️ Sunny'; if (s > 250) return '🌤 Partly Cloudy'; if (s > 60) return '⛅ Mostly Cloudy'; if (s > 5) return '☁️ Overcast'; } return '🌙 Night'; } const SHELL = `
My Station Live
ⓘWhat is this station?›

This station is a personal weather station: real instruments on the owner's property that measure the weather right here, right now. These numbers are measurements, not a forecast or a computer model.

What it measures

How it fits with the rest of the app

History, records and charts, plus a side-by-side with the official National Weather Service station, are on the Station Dashboard (sign-in required).

—
Loading…
Atmosphere
Humidity
—
%
Dew Point
—
°F
Pressure
—
inHg
Wind
Speed
—
mph
Gust
—
mph
From
—
Max Today
—
mph
Precipitation & Solar
Rain Today
—
in
Rain Rate
—
in / hr
UV Index
—
Solar Rad.
—
W/m²
Refreshes automatically every minute · Dashboard →
`; const EMPTY = (title, text) => `
${title}${text}
`; const $ = (id) => document.getElementById(id); const set = (id, v) => { const el = $(id); if (el) el.textContent = v; }; const setAll = (sel, v) => root.querySelectorAll(sel).forEach((el) => { el.textContent = v; }); let shellReady = false; let lastDir = null; // "What is this station?" starts open for new visitors and stays closed once dismissed function initAbout() { const d = $('sl-about'); if (!d) return; let closed = false; try { closed = localStorage.getItem('stationAboutClosed') === '1'; } catch {} d.open = !closed; d.addEventListener('toggle', () => { try { localStorage.setItem('stationAboutClosed', d.open ? '0' : '1'); } catch {} }); } function ensureShell() { if (shellReady) return; root.innerHTML = SHELL; shellReady = true; initAbout(); drawCompass(null, null); } function showMessage(title, text) { shellReady = false; root.innerHTML = EMPTY(title, text); $('sl-goto-settings')?.addEventListener('click', () => { if (typeof activateTab === 'function') activateTab('settings'); }); } function drawCompass(dir, speedText) { const canvas = $('sl-compass'); if (!canvas) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); const size = 152; canvas.width = size * dpr; canvas.height = size * dpr; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); const cs = getComputedStyle(root); const v = (n, f) => cs.getPropertyValue(n).trim() || f; const cCard = v('--sl-surface', '#fff'), cRing = v('--sl-border', '#ccd6e2'), cLabel = v('--sl-muted', '#5a7089'); const cAccent = v('--sl-accent', '#2563eb'), cNorth = v('--sl-north', '#dc2626'); const cx = size / 2, cy = size / 2, r = size / 2 - 7; ctx.fillStyle = cCard; ctx.beginPath(); ctx.arc(cx, cy, r + 5, 0, Math.PI * 2); ctx.fill(); [1 / 3, 2 / 3, 1].forEach((f) => { ctx.beginPath(); ctx.arc(cx, cy, r * f, 0, Math.PI * 2); ctx.strokeStyle = cRing; ctx.lineWidth = 0.7; ctx.stroke(); }); for (let i = 0; i < 8; i++) { const a = ((i * 45 - 90) * Math.PI) / 180, major = i % 2 === 0, len = major ? 9 : 5; ctx.beginPath(); ctx.moveTo(cx + r * Math.cos(a), cy + r * Math.sin(a)); ctx.lineTo(cx + (r - len) * Math.cos(a), cy + (r - len) * Math.sin(a)); ctx.strokeStyle = major ? cLabel : cRing; ctx.lineWidth = major ? 1.5 : 0.7; ctx.stroke(); } [['N', -90, cNorth], ['E', 0, cLabel], ['S', 90, cLabel], ['W', 180, cLabel]].forEach(([l, deg, color]) => { const a = (deg * Math.PI) / 180; ctx.font = `700 ${l === 'N' ? 11 : 10}px 'Barlow', sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = color; ctx.fillText(l, cx + (r - 18) * Math.cos(a), cy + (r - 18) * Math.sin(a)); }); if (dir != null) { const a = ((+dir - 90) * Math.PI) / 180, tip = r * 0.6; const tx = cx + tip * Math.cos(a), ty = cy + tip * Math.sin(a); ctx.strokeStyle = cAccent; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(tx, ty); ctx.stroke(); const ha = Math.atan2(ty - cy, tx - cx), hl = 13; ctx.fillStyle = cAccent; ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(tx - hl * Math.cos(ha - 0.38), ty - hl * Math.sin(ha - 0.38)); ctx.lineTo(tx - hl * 0.55 * Math.cos(ha), ty - hl * 0.55 * Math.sin(ha)); ctx.lineTo(tx - hl * Math.cos(ha + 0.38), ty - hl * Math.sin(ha + 0.38)); ctx.closePath(); ctx.fill(); const ta = a + Math.PI; ctx.globalAlpha = 0.3; ctx.lineWidth = 1.5; ctx.strokeStyle = cAccent; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + r * 0.22 * Math.cos(ta), cy + r * 0.22 * Math.sin(ta)); ctx.stroke(); ctx.globalAlpha = 1; ctx.fillStyle = cAccent; ctx.beginPath(); ctx.arc(cx, cy, 3, 0, Math.PI * 2); ctx.fill(); } } window.renderStationTab = function renderStationTab(device) { const d = device?.lastData; if (!d) return; ensureShell(); const u = units(); const name = (typeof state !== 'undefined' && state.stationName) || device.info?.name || 'My Station'; set('sl-name', name); // Describe the instruments this particular station really has set('sl-about-name', name); const has = (...k) => k.some((f) => d[f] != null); const sensors = [ has('tempf', 'humidity') && '🌡 Temperature & humidity: a shaded outdoor sensor', has('windspeedmph', 'winddir', 'windgustmph') && '💨 Wind: speed, gusts and direction (anemometer and wind vane)', has('dailyrainin', 'rainratein') && '🌧 Rain: a rain gauge that tips with each bit of rain', has('baromrelin') && '🧭 Pressure: a barometer (falling pressure often means weather is coming)', has('uv', 'solarradiation') && '☀️ Sun: UV and solar-radiation sensors', has('tempinf', 'humidityin') && '🏠 Indoor: a sensor inside the house', ].filter(Boolean); if (sensors.length) $('sl-sensors').innerHTML = sensors.map((x) => `
  • ${x}
  • `).join(''); const fresh = d.dateutc && Date.now() - d.dateutc < 15 * 60 * 1000; const live = $('sl-live'); live.classList.toggle('stale', !fresh); set('sl-live-text', fresh ? 'Live' : 'Offline'); if (d.dateutc) { const t = new Date(d.dateutc).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); set('sl-time', t); set('sl-foot', `Updated ${t} · refreshes automatically`); } const tempEl = $('sl-temp'), degEl = $('sl-deg'); tempEl.textContent = fTemp(d.tempf); degEl.textContent = d.tempf != null ? u.t : ''; const col = tempColor(d.tempf); tempEl.style.color = col; degEl.style.color = col; const fl = d.feelsLike ?? d.tempf; set('sl-sub', fl != null ? `Feels like ${fTemp(fl)}${u.t}` : ''); set('sl-pill', fresh ? conditionText(d) : 'Station offline'); set('sl-hum', d.humidity != null ? String(Math.round(d.humidity)) : '—'); set('sl-dew', fTemp(d.dewPoint)); set('sl-dew-u', u.t); set('sl-pres', fPres(d.baromrelin)); set('sl-pres-u', u.p); set('sl-ws', fSpeed(d.windspeedmph)); set('sl-wg', fSpeed(d.windgustmph)); set('sl-wm', fSpeed(d.maxdailygust)); set('sl-wd', cardinal(d.winddir)); set('sl-wdeg', d.winddir != null ? Math.round(+d.winddir) + '°' : ''); setAll('.sl-su', u.s); lastDir = d.winddir ?? null; drawCompass(lastDir); set('sl-rain', fRain(d.dailyrainin)); set('sl-rate', fRain(d.rainratein)); setAll('.sl-ru', u.r); setAll('.sl-rru', `${u.r} / hr`); const [uvLabel, uvColor] = uvInfo(d.uv); set('sl-uv', d.uv != null ? String(Math.round(d.uv)) : '—'); $('sl-uv').style.color = uvColor; set('sl-uvl', uvLabel); set('sl-solar', d.solarradiation != null ? String(Math.round(d.solarradiation)) : '—'); const sec = $('sl-indoor-sec'); if (d.tempinf != null) { sec.hidden = false; set('sl-tin', fTemp(d.tempinf)); setAll('.sl-tu', u.t); set('sl-hin', d.humidityin != null ? String(Math.round(d.humidityin)) : '—'); } else sec.hidden = true; }; // Called by app.js when the station can't be reached / isn't configured window.stationTabMessage = function (kind, detail) { if (shellReady) { // Keep showing the last readings but flag them as stale const live = $('sl-live'); if (live) { live.classList.add('stale'); set('sl-live-text', 'Offline'); } return; } if (kind === 'unconfigured') { showMessage('No station connected', 'This tab shows live readings from a personal weather station (a real device on someone\'s property that measures conditions on the spot, not a forecast). Add your Ambient Weather API keys in Settings to connect yours.'); } else { showMessage('Station unavailable', detail || 'Could not reach your station right now. It will keep retrying.'); } }; window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => shellReady && drawCompass(lastDir)); new MutationObserver(() => shellReady && drawCompass(lastDir)).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); root.innerHTML = '
    Loading…Fetching your station readings.
    '; })();