import { requireAuth, requireCap } from '../auth.js' import { pool, getConfig, DEFAULT_HVAC_TEMPLATE } from '../db.js' import { isConnected as mqttConnected } from '../lib/mqtt.js' import { getPeakTempC } from '../lib/weather.js' export async function statusRoutes(app) { app.addHook('preHandler', requireAuth) // GET /api/status — live status per zone: room state, target temp, device health/battery app.get('/api/status', { preHandler: requireCap('view') }, async () => { const hvacDefaults = await getConfig('hvac_defaults', DEFAULT_HVAC_TEMPLATE) // Today's forecast peak — informational only here (what a NEW occupied transition // would use right now); doesn't retroactively affect already-applied AC targets. // getPeakTempC() is TTL-cached internally so this is cheap to call on every status poll. const weatherPeakC = await getPeakTempC() const tiers = hvacDefaults.ac_occupied_tiers || [] let weatherTier = null if (weatherPeakC != null) { for (const tier of tiers) { if (weatherPeakC >= Number(tier.peak_above_c)) weatherTier = tier.name } } const { rows: zones } = await pool.query(` SELECT z.*, zs.room_state, zs.last_transition_at, zs.last_booking_status, zs.booking_kind, zs.current_night, zs.total_nights, zs.has_next_booking FROM zones z LEFT JOIN zone_state zs ON zs.zone_id = z.id WHERE z.active = TRUE ORDER BY z.zone_type, z.name `) const { rows: devices } = await pool.query(` SELECT id, zone_id, device_type, external_ref, discovered_name, location, health_state, battery_pct, wifi_rssi, current_target_temp, target_origin, last_seen, current_room_temp, heating_active, power_state, current_mode, fan_speed, remote_locked, status_updated_at FROM zone_devices WHERE zone_id IS NOT NULL `) const devicesByZone = new Map() for (const d of devices) { if (!devicesByZone.has(d.zone_id)) devicesByZone.set(d.zone_id, []) devicesByZone.get(d.zone_id).push(d) } return { mqtt_connected: mqttConnected(), weather_peak_c: weatherPeakC, weather_tier: weatherTier, zones: zones.map(z => ({ ...z, room_state: z.room_state || 'vacant', has_next_booking: z.has_next_booking ?? false, target_temp: ['heating_up', 'occupied'].includes(z.room_state) ? (z.occupied_temp ?? hvacDefaults.trv_occupied_temp) : (z.vacant_temp ?? hvacDefaults.trv_vacant_temp), devices: devicesByZone.get(z.id) || [], })), } }) // GET /api/status/activity — recent activity log, optionally filtered by zone app.get('/api/status/activity', { preHandler: requireCap('view') }, async (req) => { const { zone_id, limit } = req.query || {} const lim = Math.min(parseInt(limit) || 100, 500) if (zone_id) { const { rows } = await pool.query( `SELECT al.*, z.name AS zone_name FROM activity_log al LEFT JOIN zones z ON z.id = al.zone_id WHERE al.zone_id = $1 ORDER BY al.created_at DESC LIMIT $2`, [zone_id, lim] ) return rows } const { rows } = await pool.query( `SELECT al.*, z.name AS zone_name FROM activity_log al LEFT JOIN zones z ON z.id = al.zone_id ORDER BY al.created_at DESC LIMIT $1`, [lim] ) return rows }) }