diff --git a/.gitignore b/.gitignore
index 84376b6..30dbac5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
app/node_modules/
+.env
diff --git a/app/public/app.js b/app/public/app.js
index 16ed945..cf856e8 100644
--- a/app/public/app.js
+++ b/app/public/app.js
@@ -300,6 +300,8 @@ const STORAGE_KEYS = {
alertThresholds: 'weather.alertThresholds',
digestHour: 'weather.digestHour',
pwsUrl: 'weather.pwsUrl',
+ ambientApiKey: 'weather.ambientApiKey',
+ ambientAppKey: 'weather.ambientAppKey',
alertIdsSeen: 'weather.alertIdsSeen',
};
@@ -344,6 +346,8 @@ const state = {
alertThresholds: JSON.parse(localStorage.getItem(STORAGE_KEYS.alertThresholds) || '{}'),
digestHour: Number(localStorage.getItem(STORAGE_KEYS.digestHour) ?? 7),
pwsUrl: localStorage.getItem(STORAGE_KEYS.pwsUrl) || '',
+ ambientApiKey: localStorage.getItem(STORAGE_KEYS.ambientApiKey) || '',
+ ambientAppKey: localStorage.getItem(STORAGE_KEYS.ambientAppKey) || '',
pushSub: null,
hourlyExtended: false,
hourlyFeelsLike: false,
@@ -1957,6 +1961,9 @@ async function loadAll(city, source) {
// Fetch discussion non-blocking — US only, silently skip if unavailable
fetchDiscussion(city.latitude, city.longitude);
+
+ // Fetch personal station non-blocking
+ fetchAmbientStation();
try {
if (state.showLightning || state.unifiedMapLayers.lightning) renderLightningStrikes(city);
@@ -2653,6 +2660,44 @@ if (digestHourInput) {
}
// ── PWS URL ────────────────────────────────────────────────────────────────────
+// ── Ambient Weather key settings ──────────────────────────────────────────────
+const ambientApiKeyInput = document.getElementById('ambient-api-key-input');
+const ambientAppKeyInput = document.getElementById('ambient-app-key-input');
+const ambientSaveBtn = document.getElementById('ambient-save-btn');
+const ambientStatusEl = document.getElementById('ambient-status');
+if (ambientApiKeyInput) ambientApiKeyInput.value = state.ambientApiKey;
+if (ambientAppKeyInput) ambientAppKeyInput.value = state.ambientAppKey;
+if (ambientSaveBtn) {
+ ambientSaveBtn.addEventListener('click', async () => {
+ const apiKey = ambientApiKeyInput?.value?.trim();
+ const appKey = ambientAppKeyInput?.value?.trim();
+ if (!apiKey || !appKey) {
+ if (ambientStatusEl) { ambientStatusEl.textContent = 'Both keys are required.'; show(ambientStatusEl); }
+ return;
+ }
+ ambientSaveBtn.disabled = true;
+ try {
+ const res = await fetch(`/api/ambient?apiKey=${encodeURIComponent(apiKey)}&appKey=${encodeURIComponent(appKey)}`);
+ if (res.ok) {
+ const devices = await res.json();
+ state.ambientApiKey = apiKey;
+ state.ambientAppKey = appKey;
+ localStorage.setItem(STORAGE_KEYS.ambientApiKey, apiKey);
+ localStorage.setItem(STORAGE_KEYS.ambientAppKey, appKey);
+ const stationName = devices?.[0]?.info?.name || devices?.[0]?.macAddress || 'station';
+ if (ambientStatusEl) { ambientStatusEl.textContent = `✓ Connected to ${stationName}`; show(ambientStatusEl); }
+ fetchAmbientStation();
+ } else {
+ const { error } = await res.json().catch(() => ({}));
+ if (ambientStatusEl) { ambientStatusEl.textContent = `✗ ${error || 'Connection failed'}`; show(ambientStatusEl); }
+ }
+ } catch {
+ if (ambientStatusEl) { ambientStatusEl.textContent = '✗ Request failed.'; show(ambientStatusEl); }
+ }
+ ambientSaveBtn.disabled = false;
+ });
+}
+
const pwsUrlInput = document.getElementById('pws-url-input');
const pwsSaveBtn = document.getElementById('pws-save-btn');
const pwsStatusEl = document.getElementById('pws-status');
@@ -2758,3 +2803,135 @@ if (pwsSaveBtn) {
// silent startup fallback
}
})();
+
+// ── Ambient Weather personal station ─────────────────────────────────────────
+let stationRefreshTimer = null;
+
+async function fetchAmbientStation() {
+ const loadingEl = document.getElementById('station-loading');
+ const contentEl = document.getElementById('station-content');
+ const errorEl = document.getElementById('station-error');
+ const peekEl = document.getElementById('station-drawer-peek');
+
+ const apiKey = state.ambientApiKey;
+ const appKey = state.ambientAppKey;
+ const params = new URLSearchParams();
+ if (apiKey) params.set('apiKey', apiKey);
+ if (appKey) params.set('appKey', appKey);
+ const qs = params.toString() ? `?${params}` : '';
+
+ try {
+ const r = await fetch(`/api/ambient${qs}`);
+ if (!r.ok) {
+ const { error } = await r.json().catch(() => ({}));
+ if (errorEl) { errorEl.textContent = error || 'Station unavailable'; show(errorEl); }
+ if (loadingEl) hide(loadingEl);
+ return;
+ }
+ const devices = await r.json();
+ if (!Array.isArray(devices) || !devices.length) {
+ if (errorEl) { errorEl.textContent = 'No devices found on this account.'; show(errorEl); }
+ if (loadingEl) hide(loadingEl);
+ return;
+ }
+ if (loadingEl) hide(loadingEl);
+ if (errorEl) hide(errorEl);
+ renderAmbientStation(devices[0]);
+
+ // Auto-refresh every 60 s
+ clearInterval(stationRefreshTimer);
+ stationRefreshTimer = setInterval(async () => {
+ try {
+ const p2 = new URLSearchParams();
+ if (state.ambientApiKey) p2.set('apiKey', state.ambientApiKey);
+ if (state.ambientAppKey) p2.set('appKey', state.ambientAppKey);
+ const q2 = p2.toString() ? `?${p2}` : '';
+ const r2 = await fetch(`/api/ambient${q2}`);
+ if (r2.ok) {
+ const d2 = await r2.json();
+ if (Array.isArray(d2) && d2.length) renderAmbientStation(d2[0]);
+ }
+ } catch {}
+ }, 60000);
+ } catch (e) {
+ if (errorEl) { errorEl.textContent = 'Could not reach station.'; show(errorEl); }
+ if (loadingEl) hide(loadingEl);
+ }
+}
+
+function renderAmbientStation(device) {
+ const data = device?.lastData || {};
+ const info = device?.info || {};
+ const contentEl = document.getElementById('station-content');
+ const peekEl = document.getElementById('station-drawer-peek');
+
+ const stName = info.name || device?.macAddress || 'My Station';
+ const nameEl = document.getElementById('station-name');
+ if (nameEl) nameEl.textContent = stName;
+
+ const updatedEl = document.getElementById('station-updated');
+ if (updatedEl && data.dateutc) {
+ const ago = Math.round((Date.now() - data.dateutc) / 60000);
+ updatedEl.textContent = ago <= 1 ? 'just now' : `${ago} min ago`;
+ }
+
+ const isMetric = state.units !== 'us';
+
+ function fmtTemp(f) {
+ if (f == null) return '--';
+ return isMetric ? Math.round((f - 32) * 5 / 9) : Math.round(f);
+ }
+ function fmtSpeed(mph) {
+ if (mph == null) return '--';
+ return isMetric ? Math.round(mph * 1.60934) : Math.round(mph);
+ }
+ function fmtRain(inches) {
+ if (inches == null) return '--';
+ return isMetric ? (inches * 25.4).toFixed(1) : inches.toFixed(2);
+ }
+
+ const tempUnit = isMetric ? 'C' : 'F';
+ const speedUnit = isMetric ? 'km/h' : 'mph';
+ const rainUnit = isMetric ? 'mm' : 'in';
+
+ const setEl = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
+
+ setEl('st-temp', fmtTemp(data.tempf));
+ setEl('st-temp-unit', tempUnit);
+ setEl('st-feels', data.feelsLike != null ? `Feels ${fmtTemp(data.feelsLike)}°` : '');
+ setEl('st-hum', data.humidity != null ? `${data.humidity}` : '--');
+
+ if (data.tempf != null && data.humidity != null) {
+ const tc = (data.tempf - 32) * 5 / 9;
+ const alpha = Math.log(data.humidity / 100) + (17.62 * tc) / (243.12 + tc);
+ const dewC = (243.12 * alpha) / (17.62 - alpha);
+ const dewDisplay = isMetric ? Math.round(dewC) : Math.round(dewC * 9 / 5 + 32);
+ setEl('st-dew', `Dew ${dewDisplay}°`);
+ }
+
+ setEl('st-wind', fmtSpeed(data.windspeedmph));
+ setEl('st-wind-unit', speedUnit);
+ setEl('st-wind-dir', data.winddir != null ? compassDirFull(data.winddir) : '');
+ setEl('st-gust', fmtSpeed(data.windgustmph));
+ setEl('st-gust-unit', speedUnit);
+ setEl('st-rain-day', fmtRain(data.dailyrainin));
+ setEl('st-rain-unit', rainUnit);
+ const rrDisplay = data.hourlyrainin != null ? `${fmtRain(data.hourlyrainin)} ${rainUnit}/hr` : '';
+ setEl('st-rain-rate', rrDisplay);
+ setEl('st-pressure', data.baromrelin != null ? data.baromrelin.toFixed(2) : '--');
+ setEl('st-uv', data.uv != null ? data.uv : '--');
+ setEl('st-solar', data.solarradiation != null ? Math.round(data.solarradiation) : '--');
+
+ const indoorCard = document.getElementById('st-indoor-card');
+ if (data.tempinf != null) {
+ setEl('st-tempin', fmtTemp(data.tempinf));
+ setEl('st-humin', data.humidityin != null ? `${data.humidityin}% RH` : '');
+ if (indoorCard) indoorCard.classList.remove('hidden');
+ }
+
+ if (peekEl && data.tempf != null) {
+ peekEl.textContent = `· ${fmtTemp(data.tempf)}°${tempUnit} ${data.humidity != null ? data.humidity + '% RH' : ''}`;
+ }
+
+ if (contentEl) show(contentEl);
+}
diff --git a/app/public/index.html b/app/public/index.html
index 5b4ab45..794c42e 100644
--- a/app/public/index.html
+++ b/app/public/index.html
@@ -237,6 +237,68 @@
+
+
+ My Station
+
+ ›
+
+
From ambientweather.net → Account → API Keys. Keys are stored locally in your browser.
+ +