/* 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
`;
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;
function ensureShell() {
if (shellReady) return;
root.innerHTML = SHELL;
shellReady = true;
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);
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', 'Add your Ambient Weather API keys in Settings to see live readings from your own station here.');
} 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.
';
})();