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>
This commit is contained in:
commit
276c04f8c6
42 changed files with 5461 additions and 0 deletions
59
backend/src/routes/status.js
Normal file
59
backend/src/routes/status.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue