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.
178 lines
3.8 KiB
Go
178 lines
3.8 KiB
Go
package risk
|
|
|
|
import "math"
|
|
|
|
// RiskLevel represents the severity of heat risk.
|
|
type RiskLevel int
|
|
|
|
const (
|
|
Low RiskLevel = iota
|
|
Moderate
|
|
High
|
|
Extreme
|
|
)
|
|
|
|
func (r RiskLevel) String() string {
|
|
switch r {
|
|
case Low:
|
|
return "low"
|
|
case Moderate:
|
|
return "moderate"
|
|
case High:
|
|
return "high"
|
|
case Extreme:
|
|
return "extreme"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// HourlyData holds weather data for a single hour.
|
|
type HourlyData struct {
|
|
Hour int
|
|
TempC float64
|
|
ApparentC float64
|
|
HumidityPct float64
|
|
IsDay bool
|
|
}
|
|
|
|
// RiskWindow represents a contiguous block of hours with elevated heat risk.
|
|
type RiskWindow struct {
|
|
StartHour int
|
|
EndHour int
|
|
PeakTempC float64
|
|
Level RiskLevel
|
|
Reason string
|
|
}
|
|
|
|
// DayRisk holds the overall risk assessment for a day.
|
|
type DayRisk struct {
|
|
Level RiskLevel
|
|
PeakTempC float64
|
|
MinNightTempC float64
|
|
PoorNightCool bool
|
|
MinTempC float64
|
|
ColdRisk bool
|
|
Windows []RiskWindow
|
|
}
|
|
|
|
// isNightHour returns true for hours 21-23 and 0-6.
|
|
func isNightHour(hour int) bool {
|
|
return hour >= 21 || hour <= 6
|
|
}
|
|
|
|
// riskLevelForTemp returns the risk level based on temperature and thresholds.
|
|
func riskLevelForTemp(tempC float64, th Thresholds) RiskLevel {
|
|
switch {
|
|
case tempC >= th.ExtremeDayC:
|
|
return Extreme
|
|
case tempC >= th.VeryHotDayC:
|
|
return High
|
|
case tempC >= th.HotDayC:
|
|
return Moderate
|
|
default:
|
|
return Low
|
|
}
|
|
}
|
|
|
|
// AnalyzeDay analyzes 24 hourly data points and returns the overall day risk.
|
|
func AnalyzeDay(hours []HourlyData, th Thresholds) DayRisk {
|
|
if len(hours) == 0 {
|
|
return DayRisk{Level: Low, MinNightTempC: math.Inf(1), MinTempC: math.Inf(1)}
|
|
}
|
|
|
|
result := DayRisk{
|
|
Level: Low,
|
|
MinNightTempC: math.Inf(1),
|
|
MinTempC: math.Inf(1),
|
|
}
|
|
|
|
// Find peak temp, min night temp, and global min temp
|
|
for _, h := range hours {
|
|
if h.TempC > result.PeakTempC {
|
|
result.PeakTempC = h.TempC
|
|
}
|
|
if h.TempC < result.MinTempC {
|
|
result.MinTempC = h.TempC
|
|
}
|
|
if isNightHour(h.Hour) {
|
|
if h.TempC < result.MinNightTempC {
|
|
result.MinNightTempC = h.TempC
|
|
}
|
|
if h.TempC >= th.PoorNightCoolingC {
|
|
result.PoorNightCool = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no night hours were seen, set MinNightTempC to 0
|
|
if math.IsInf(result.MinNightTempC, 1) {
|
|
result.MinNightTempC = 0
|
|
}
|
|
if math.IsInf(result.MinTempC, 1) {
|
|
result.MinTempC = 0
|
|
}
|
|
result.ColdRisk = result.MinTempC <= th.ColdDayC
|
|
|
|
// Find contiguous risk windows (hours where temp >= HotDayC)
|
|
var currentWindow *RiskWindow
|
|
for _, h := range hours {
|
|
level := riskLevelForTemp(h.TempC, th)
|
|
if level >= Moderate {
|
|
if currentWindow == nil {
|
|
currentWindow = &RiskWindow{
|
|
StartHour: h.Hour,
|
|
EndHour: h.Hour,
|
|
PeakTempC: h.TempC,
|
|
Level: level,
|
|
}
|
|
} else {
|
|
currentWindow.EndHour = h.Hour
|
|
if h.TempC > currentWindow.PeakTempC {
|
|
currentWindow.PeakTempC = h.TempC
|
|
}
|
|
if level > currentWindow.Level {
|
|
currentWindow.Level = level
|
|
}
|
|
}
|
|
} else {
|
|
if currentWindow != nil {
|
|
currentWindow.Reason = reasonForLevel(currentWindow.Level)
|
|
result.Windows = append(result.Windows, *currentWindow)
|
|
currentWindow = nil
|
|
}
|
|
}
|
|
}
|
|
if currentWindow != nil {
|
|
currentWindow.Reason = reasonForLevel(currentWindow.Level)
|
|
result.Windows = append(result.Windows, *currentWindow)
|
|
}
|
|
|
|
// Overall level = max of all windows
|
|
for _, w := range result.Windows {
|
|
if w.Level > result.Level {
|
|
result.Level = w.Level
|
|
}
|
|
}
|
|
|
|
// Poor night cooling elevates by one level (capped at Extreme)
|
|
if result.PoorNightCool && result.Level > Low && result.Level < Extreme {
|
|
result.Level++
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func reasonForLevel(level RiskLevel) string {
|
|
switch level {
|
|
case Moderate:
|
|
return "hot daytime temperatures"
|
|
case High:
|
|
return "very hot daytime temperatures"
|
|
case Extreme:
|
|
return "extreme heat"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|