Files
HeatGuard/internal/action/selector.go
vikingowl 21154d5d7f feat: add heating support with heat pump modeling and cold risk detection
Model heating mode when rooms have net heat loss in cold weather (<10°C).
AC units with heat pump capability (canHeat) provide heating capacity,
with the same 20% headroom threshold used for cooling. Adds cold risk
detection, cold-weather actions, and full frontend support including
heating mode timeline colors, room budget heating display, and i18n.
2026-02-11 00:00:43 +01:00

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
}
}