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:
jtricerolph 2026-07-26 18:00:42 +00:00
commit 276c04f8c6
42 changed files with 5461 additions and 0 deletions

View file

@ -0,0 +1,27 @@
import { Thermometer, BatteryLow } from 'lucide-react'
import type { ZoneStatus } from '../types'
import { ROOM_STATE_LABELS } from '../types'
export default function ZoneCard({ zone, onClick }: { zone: ZoneStatus; onClick: () => void }) {
const unhealthy = zone.devices.filter(d => d.health_state !== 'healthy').length
const lowBattery = zone.devices.some(d => d.battery_pct != null && d.battery_pct < 30)
return (
<div className={`card zone-card st-${zone.room_state}`} onClick={onClick}>
<div className="zone-card-title">
<span>{zone.name}</span>
{!zone.auto_mode && <span className="badge badge-outline">Manual</span>}
</div>
<div className={`badge badge-st-${zone.room_state}`}>{ROOM_STATE_LABELS[zone.room_state]}</div>
<div className="zone-card-temp">
<Thermometer size={16} strokeWidth={1.75} style={{ verticalAlign: '-2px', marginRight: 4 }} />
{Number(zone.target_temp).toFixed(1)}°C
</div>
<div className="zone-card-meta">
<span>{zone.device_count ?? zone.devices.length} device{(zone.device_count ?? zone.devices.length) === 1 ? '' : 's'}</span>
{unhealthy > 0 && <span style={{ color: 'var(--health-degraded)' }}>{unhealthy} needs attention</span>}
{lowBattery && <BatteryLow size={13} strokeWidth={1.75} color="var(--health-degraded)" />}
</div>
</div>
)
}