Accept GET query params on HomeKit push endpoint (#6)

* Add Ecobee thermostat integration to dashboard

- Server: PIN-based OAuth flow to authorize Ecobee access without
  exposing redirect URIs; tokens stored server-side and auto-refreshed
- Server: /api/ecobee/config, /api/ecobee/auth/pin,
  /api/ecobee/auth/complete, /api/ecobee/data, /api/ecobee/runtime
  endpoints behind dashboard auth
- Dashboard: Ecobee section card showing indoor temp, humidity,
  heat/cool setpoints, HVAC mode, equipment status badge, and
  per-sensor temperature readings from remote sensors
- Dashboard: stacked bar chart of daily heating/cooling runtime hours
  for the past 7 days
- Dashboard: Ecobee setup panel with API key entry and step-by-step
  PIN authorization flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMhuFaecxraQzUPN4h9nsB

* Replace ecobee API integration with HomeKit push endpoint

Ecobee removed public developer API access, so pivot to HomeKit:
- Add POST /api/homekit/push accepting thermostat data from iOS Shortcuts
- Add GET /api/homekit/data returning latest reading to dashboard
- Add GET /api/homekit/config and POST /api/homekit/config/regenerate for token management
- Auto-generate auth token on first run, persist to data/homekit-config.json
- Auto-convert Celsius→Fahrenheit if temp < 50 (HomeKit always returns Celsius)
- Update dashboard section card to show temp, setpoints, humidity, mode, HVAC state
- Replace ecobee settings panel with push URL, token copy, and Shortcut setup instructions
- Remove runtime chart (not available without API access)

* Support GET query params on homekit push endpoint for iOS Shortcuts compatibility

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude authored and GitHub committed 2026-08-23 12:40:52 -04:00
1 parent e8bb117568
commit 5eae872773
1 file changed
+20 -19
+20 -19
View File
@@ -636,35 +636,36 @@ app.post('/api/homekit/config/regenerate', requireDashboardAuth, (_req, res) =>
res.json({ token: homekitConfig.token });
});
// Receive thermostat data from iOS Shortcut — token header auth
app.post('/api/homekit/push', (req, res) => {
const token = req.headers['x-homekit-token'] || req.query.token;
// Receive thermostat data from iOS Shortcut — token in query string or header
// Accepts both GET (query params) and POST (JSON body or query params)
function handleHomekitPush(req, res) {
const token = req.query.token || req.headers['x-homekit-token'] || (req.body || {}).token;
if (!token || token !== homekitConfig.token) {
return res.status(401).json({ error: 'Unauthorized' });
}
const body = req.body || {};
// HomeKit reports temperatures in Celsius; auto-detect and convert
let tempF = parseFloat(body.tempF ?? body.temperature);
if (!isNaN(tempF) && tempF < 50) tempF = +(tempF * 9 / 5 + 32).toFixed(1);
const p = { ...(req.body || {}), ...req.query };
let heatSetF = body.heatSetF != null ? parseFloat(body.heatSetF) : null;
if (heatSetF != null && heatSetF < 50) heatSetF = +(heatSetF * 9 / 5 + 32).toFixed(1);
let coolSetF = body.coolSetF != null ? parseFloat(body.coolSetF) : null;
if (coolSetF != null && coolSetF < 50) coolSetF = +(coolSetF * 9 / 5 + 32).toFixed(1);
function parseTemp(val) {
const n = parseFloat(val);
if (isNaN(n)) return null;
return n < 50 ? +(n * 9 / 5 + 32).toFixed(1) : +n.toFixed(1);
}
homekitLatest = {
tempF: isNaN(tempF) ? null : tempF,
heatSetF: heatSetF != null && !isNaN(heatSetF) ? +parseFloat(heatSetF).toFixed(1) : null,
coolSetF: coolSetF != null && !isNaN(coolSetF) ? +parseFloat(coolSetF).toFixed(1) : null,
humidity: body.humidity != null ? +parseFloat(body.humidity).toFixed(0) : null,
mode: body.mode || null,
hvacState: body.hvacState || null,
tempF: parseTemp(p.temperature ?? p.tempF),
heatSetF: parseTemp(p.heatSetF),
coolSetF: parseTemp(p.coolSetF),
humidity: p.humidity != null ? +parseFloat(p.humidity).toFixed(0) : null,
mode: p.mode || null,
hvacState: p.hvacState || null,
receivedAt: Date.now(),
};
try { fs.writeFileSync(HOMEKIT_LATEST_FILE, JSON.stringify(homekitLatest)); } catch {}
res.json({ ok: true });
});
}
app.get('/api/homekit/push', handleHomekitPush);
app.post('/api/homekit/push', handleHomekitPush);
// Latest thermostat reading — dashboard auth
app.get('/api/homekit/data', requireDashboardAuth, (_req, res) => {