Add server-side rendered setup UI accessible via `heatwave web`. The dashboard is now re-rendered per request and includes a nav bar linking to the new /setup page. Setup provides full CRUD for profiles, rooms, devices, occupants, AC units (with room assignment), scenario toggles, and forecast fetching — all via POST/redirect/GET forms. - Add ShowNav field to DashboardData for conditional nav bar - Extract fetchForecastForProfile() for reuse by web handler - Create setup.html.tmpl with Tailwind-styled entity sections - Create web_handlers.go with 15 route handlers and flash cookies - Switch web.go from pre-rendered to per-request dashboard rendering - Graceful dashboard fallback when no forecast data exists
94 lines
2.1 KiB
Go
94 lines
2.1 KiB
Go
package heat
|
||
|
||
// DeviceMode selects which power draw to use for heat gain calculation.
|
||
type DeviceMode int
|
||
|
||
const (
|
||
ModeIdle DeviceMode = iota
|
||
ModeTypical
|
||
ModePeak
|
||
)
|
||
|
||
// Device represents a heat-producing device in a room.
|
||
type Device struct {
|
||
WattsIdle float64
|
||
WattsTypical float64
|
||
WattsPeak float64
|
||
DutyCycle float64 // 0.0–1.0
|
||
}
|
||
|
||
// ActivityLevel represents metabolic activity of room occupants.
|
||
type ActivityLevel int
|
||
|
||
const (
|
||
Sleeping ActivityLevel = iota
|
||
Sedentary
|
||
LightActivity
|
||
ModerateActivity
|
||
HeavyActivity
|
||
)
|
||
|
||
// metabolicWatts maps activity level to per-person heat output in watts.
|
||
var metabolicWatts = map[ActivityLevel]float64{
|
||
Sleeping: 70,
|
||
Sedentary: 100,
|
||
LightActivity: 130,
|
||
ModerateActivity: 200,
|
||
HeavyActivity: 300,
|
||
}
|
||
|
||
// Occupant represents people in a room.
|
||
type Occupant struct {
|
||
Count int
|
||
Activity ActivityLevel
|
||
}
|
||
|
||
// ParseActivityLevel converts a string to ActivityLevel.
|
||
func ParseActivityLevel(s string) ActivityLevel {
|
||
switch s {
|
||
case "sleeping":
|
||
return Sleeping
|
||
case "sedentary":
|
||
return Sedentary
|
||
case "light":
|
||
return LightActivity
|
||
case "moderate":
|
||
return ModerateActivity
|
||
case "heavy":
|
||
return HeavyActivity
|
||
default:
|
||
return Sedentary
|
||
}
|
||
}
|
||
|
||
// DeviceHeatGain returns heat output in watts for a device in the given mode.
|
||
func DeviceHeatGain(d Device, mode DeviceMode) float64 {
|
||
var base float64
|
||
switch mode {
|
||
case ModeIdle:
|
||
base = d.WattsIdle
|
||
case ModeTypical:
|
||
base = d.WattsTypical
|
||
case ModePeak:
|
||
base = d.WattsPeak
|
||
}
|
||
return base * d.DutyCycle
|
||
}
|
||
|
||
// OccupantHeatGain returns total metabolic heat output in watts.
|
||
func OccupantHeatGain(count int, activity ActivityLevel) float64 {
|
||
return float64(count) * metabolicWatts[activity]
|
||
}
|
||
|
||
// TotalInternalGains sums device and occupant heat gains in watts.
|
||
func TotalInternalGains(devices []Device, mode DeviceMode, occupants []Occupant) float64 {
|
||
var total float64
|
||
for _, d := range devices {
|
||
total += DeviceHeatGain(d, mode)
|
||
}
|
||
for _, o := range occupants {
|
||
total += OccupantHeatGain(o.Count, o.Activity)
|
||
}
|
||
return total
|
||
}
|