diff --git a/app/public/dashboard.html b/app/public/dashboard.html
index cf515ae..db41467 100644
--- a/app/public/dashboard.html
+++ b/app/public/dashboard.html
@@ -178,6 +178,21 @@ a { color: var(--cool); text-decoration: none; }
.today-sep { width: 1px; height: 1.1em; background: var(--border); flex-shrink: 0; }
.today-lbl { font-size: 0.75rem; color: var(--ink-light); }
+/* ── Ecobee section ── */
+.hvac-badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 20px; font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
+.hvac-heating { background: rgba(194,90,42,0.18); color: var(--accent); border: 1px solid var(--accent); }
+.hvac-cooling { background: rgba(42,107,138,0.18); color: var(--cool); border: 1px solid var(--cool); }
+.hvac-fan { background: rgba(42,107,138,0.1); color: var(--cool); border: 1px solid var(--border); }
+.hvac-idle { background: var(--paper-darker); color: var(--ink-light); border: 1px solid var(--border); }
+.ecobee-sensors { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; }
+.sensor-chip { background: var(--paper); border: 1px solid var(--border); border-radius: 8px; padding: 0.35rem 0.7rem; font-size: 0.8rem; display: flex; align-items: center; gap: 0.4rem; }
+.sensor-chip-name { color: var(--ink-light); font-size: 0.72rem; }
+.sensor-chip-val { font-weight: 700; }
+.ecobee-row { display: flex; gap: 1.5rem; align-items: flex-start; flex-wrap: wrap; }
+.ecobee-main { flex: 0 0 auto; min-width: 120px; }
+.ecobee-setpoints { display: flex; gap: 1rem; margin-top: 0.4rem; font-size: 0.85rem; color: var(--ink-light); }
+.ecobee-setpoints strong { color: var(--ink); }
+
/* ── Tile color tints ── */
.stat-hot { border-color: rgba(220,80,30,0.55) !important; background: rgba(220,80,30,0.07) !important; }
.stat-warm { border-color: rgba(200,155,0,0.5) !important; background: rgba(200,155,0,0.06) !important; }
@@ -319,6 +334,29 @@ canvas.sparkline { display: block; width: 100%; height: 22px; margin-top: 0.4rem
+
+
+
+ Thermostat
+ —
+
+
+
+
+
+
+ Heat: —°
+ Cool: —°
+
+
Humidity: —%
+
Mode: —
+
+
+
+
History:
@@ -365,6 +403,43 @@ canvas.sparkline { display: block; width: 100%; height: 22px; margin-top: 0.4rem
+
+
+ Thermostat (HomeKit Push) Setup
+
+
+
+
iOS Shortcut Push URL
+
Configure your iOS Shortcut to POST thermostat data to this URL:
+
+
+
+
+
+
Auth Token
+
Include this as the x-homekit-token header in your Shortcut's URL request:
+
+
+
+
+
+
+
+
+
+
iOS Shortcut Setup
+
+ - Open Shortcuts → tap + to create a new shortcut
+ - Add Get my HomeKit value for → select your thermostat → Current Temperature → save as variable temp
+ - Repeat for Target Temperature (save as targetTemp) and Current Relative Humidity (save as humidity)
+ - Add Get contents of URL → paste the Push URL above → Method: POST
+ - Add header:
x-homekit-token = (paste your token)
+ - Request Body: JSON → add keys:
temperature=temp, heatSetF=targetTemp, humidity=humidity
+ - Go to Automation tab → + → Time of Day → set repeat every 5 min → run this shortcut (no notification)
+
+
+
+
Station API Keys
@@ -1543,6 +1618,94 @@ document.getElementById('saveLocBtn')?.addEventListener('click', async () => {
} catch { msg.textContent = 'Error'; msg.className = 'save-msg err'; }
});
+// ── HomeKit thermostat push ───────────────────────────────────────────────────
+function hvacBadge(hvacState) {
+ if (!hvacState) return { text: 'Idle', cls: 'hvac-idle' };
+ const s = hvacState.toLowerCase();
+ if (s.includes('cool')) return { text: 'Cooling', cls: 'hvac-cooling' };
+ if (s.includes('heat')) return { text: 'Heating', cls: 'hvac-heating' };
+ if (s.includes('fan')) return { text: 'Fan', cls: 'hvac-fan' };
+ return { text: 'Idle', cls: 'hvac-idle' };
+}
+
+async function loadHomekitData() {
+ const section = document.getElementById('ecobeeSection');
+ try {
+ const r = await fetch('/api/homekit/data');
+ if (!r.ok) return;
+ const d = await r.json();
+ if (d.tempF == null) return;
+
+ section.style.display = '';
+ const set = (id, val) => { const el = document.getElementById(id); if (el && val != null) el.textContent = val; };
+ set('ecobeeTempF', d.tempF != null ? d.tempF.toFixed(1) : '—');
+ set('ecobeeHeatSet', d.heatSetF != null ? d.heatSetF.toFixed(0) : '—');
+ set('ecobeeCoolSet', d.coolSetF != null ? d.coolSetF.toFixed(0) : '—');
+ set('ecobeeHum', d.humidity ?? '—');
+ set('ecobeeMode', d.mode ?? '—');
+
+ const badge = document.getElementById('ecobeeBadge');
+ if (badge) {
+ const { text, cls } = hvacBadge(d.hvacState);
+ badge.textContent = text;
+ badge.className = `hvac-badge ${cls}`;
+ }
+
+ const upd = document.getElementById('ecobeeUpdated');
+ if (upd && d.receivedAt) {
+ upd.textContent = 'Updated ' + new Date(d.receivedAt).toLocaleTimeString();
+ }
+ } catch {}
+}
+
+async function loadHomekitConfigStatus() {
+ try {
+ const r = await fetch('/api/homekit/config');
+ if (!r.ok) return;
+ const d = await r.json();
+ const el = document.getElementById('ecobeeConfigStatus');
+ if (d.token) {
+ el.textContent = '✓ Push endpoint active — configure your iOS Shortcut below';
+ el.className = 'keys-status ok';
+ } else {
+ el.textContent = '⚠ Token not configured';
+ el.className = 'keys-status missing';
+ }
+ const urlInput = document.getElementById('homekitPushUrl');
+ if (urlInput) urlInput.value = window.location.origin + '/api/homekit/push';
+ const tokenInput = document.getElementById('homekitTokenDisplay');
+ if (tokenInput) tokenInput.value = d.token || '';
+ } catch {}
+}
+
+document.getElementById('homekitCopyUrlBtn')?.addEventListener('click', () => {
+ const val = document.getElementById('homekitPushUrl')?.value;
+ if (val) navigator.clipboard.writeText(val).catch(() => {});
+});
+
+document.getElementById('homekitCopyTokenBtn')?.addEventListener('click', () => {
+ const val = document.getElementById('homekitTokenDisplay')?.value;
+ if (val) navigator.clipboard.writeText(val).catch(() => {});
+});
+
+document.getElementById('homekitRegenBtn')?.addEventListener('click', async () => {
+ const msg = document.getElementById('homekitRegenMsg');
+ try {
+ const r = await fetch('/api/homekit/config/regenerate', { method: 'POST' });
+ const d = await r.json();
+ if (r.ok && d.token) {
+ const tokenInput = document.getElementById('homekitTokenDisplay');
+ if (tokenInput) tokenInput.value = d.token;
+ msg.textContent = 'Token regenerated — update your Shortcut'; msg.className = 'save-msg';
+ } else { msg.textContent = 'Error'; msg.className = 'save-msg err'; }
+ setTimeout(() => msg.textContent = '', 4000);
+ } catch { msg.textContent = 'Error'; msg.className = 'save-msg err'; }
+});
+
+document.getElementById('ecobeeSettingsPanel')?.addEventListener('toggle', function() {
+ if (this.open) loadHomekitConfigStatus();
+});
+
// ── Init all new features ─────────────────────────────────────────────────────
updateLatest();
updatePressureTrend();
@@ -1556,11 +1719,13 @@ loadThresholds();
loadAlerts();
loadStreak();
loadLocation();
+loadHomekitData();
setInterval(updatePressureTrend, 120000);
setInterval(updateExtStats, 300000);
setInterval(updateTrends, 120000);
setInterval(loadAlerts, 5 * 60 * 1000);
+setInterval(loadHomekitData, 3 * 60 * 1000);