hvac/backend/src/routes/status.js
jtricerolph 276c04f8c6 Scaffold Phase 1 hvac app — NewBook-driven room TRV heating scheduler
Ports the state machine, retry/backoff, and guest-override detection from
the retired homeassistant-newbook-heating-component, without depending on
Home Assistant. Backend (Fastify/pg) + frontend (React/Vite/TS) following
standard stack conventions; LXC 128 (127 was already taken by utilities).

MHI/Midea/Daikin/boiler drivers and the shared MQTT broker (LXC 104) are
later phases/infra, not included here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 18:00:42 +00:00

59 lines
2.2 KiB
JavaScript

import { requireAuth, requireCap } from '../auth.js'
import { pool } from '../db.js'
import { isConnected as mqttConnected } from '../lib/mqtt.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 { rows: zones } = await pool.query(`
SELECT z.*, zs.room_state, zs.last_transition_at, zs.last_booking_status
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
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(),
zones: zones.map(z => ({
...z,
room_state: z.room_state || 'vacant',
target_temp: ['heating_up', 'occupied'].includes(z.room_state) ? z.occupied_temp : z.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
})
}