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
128 lines
2.3 KiB
Go
128 lines
2.3 KiB
Go
package action
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"github.com/cnachtigall/heatwave-autopilot/internal/heat"
|
|
"github.com/cnachtigall/heatwave-autopilot/internal/risk"
|
|
)
|
|
|
|
// Matches checks if an action's conditions are met for a given hour context.
|
|
func Matches(a Action, ctx HourContext) bool {
|
|
w := a.When
|
|
|
|
// Hour range check (only if at least one is non-zero)
|
|
if w.HourFrom != 0 || w.HourTo != 0 {
|
|
if ctx.Hour < w.HourFrom || ctx.Hour > w.HourTo {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Temperature threshold
|
|
if w.MinTempC > 0 && ctx.TempC < w.MinTempC {
|
|
return false
|
|
}
|
|
if w.MaxTempC > 0 && ctx.TempC > w.MaxTempC {
|
|
return false
|
|
}
|
|
|
|
// Night only
|
|
if w.NightOnly && ctx.IsDay {
|
|
return false
|
|
}
|
|
|
|
// Risk level
|
|
if w.MinRisk != "" {
|
|
required := parseRiskLevel(w.MinRisk)
|
|
if ctx.RiskLevel < required {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Budget status
|
|
if w.BudgetStatus != "" {
|
|
required := parseBudgetStatus(w.BudgetStatus)
|
|
if ctx.BudgetStatus < required {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// High humidity
|
|
if w.HighHumidity && ctx.HumidityPct <= 70 {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// SelectActions returns all matching actions for a given hour, sorted by priority.
|
|
func SelectActions(actions []Action, ctx HourContext) []Action {
|
|
var matched []Action
|
|
for _, a := range actions {
|
|
if Matches(a, ctx) {
|
|
matched = append(matched, a)
|
|
}
|
|
}
|
|
sort.Slice(matched, func(i, j int) bool {
|
|
return priority(matched[i]) > priority(matched[j])
|
|
})
|
|
return matched
|
|
}
|
|
|
|
// priority scores an action: impact * 10 + (4 - effort)
|
|
func priority(a Action) int {
|
|
return impactScore(a.Impact)*10 + (4 - effortScore(a.Effort))
|
|
}
|
|
|
|
func impactScore(i Impact) int {
|
|
switch i {
|
|
case ImpactHigh:
|
|
return 3
|
|
case ImpactMedium:
|
|
return 2
|
|
case ImpactLow:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func effortScore(e Effort) int {
|
|
switch e {
|
|
case EffortNone:
|
|
return 0
|
|
case EffortLow:
|
|
return 1
|
|
case EffortMedium:
|
|
return 2
|
|
case EffortHigh:
|
|
return 3
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func parseRiskLevel(s string) risk.RiskLevel {
|
|
switch s {
|
|
case "moderate":
|
|
return risk.Moderate
|
|
case "high":
|
|
return risk.High
|
|
case "extreme":
|
|
return risk.Extreme
|
|
default:
|
|
return risk.Low
|
|
}
|
|
}
|
|
|
|
func parseBudgetStatus(s string) heat.BudgetStatus {
|
|
switch s {
|
|
case "marginal":
|
|
return heat.Marginal
|
|
case "overloaded":
|
|
return heat.Overloaded
|
|
default:
|
|
return heat.Comfortable
|
|
}
|
|
}
|