feat: add OWM alerts, UV index support, and provider info UI

- Parse OWM One Call 3.0 weather alerts and map to Warning structs
- Map hourly UVI from OWM response to HourlyForecast.UVIndex
- Add severity helper mapping OWM alert tags to severity levels
- Extract UVIndex through compute layer to timeline slots
- Smart warnings: use provider-supplied alerts, fall back to DWD
- Show UV index in dashboard timeline tooltips
- Add provider description below forecast dropdown with i18n
This commit is contained in:
2026-02-11 01:56:09 +01:00
parent 41649380e2
commit 201e5441cb
13 changed files with 243 additions and 30 deletions

View File

@@ -122,6 +122,7 @@ func BuildDashboard(req ComputeRequest) (DashboardData, error) {
cloudPct := 50.0
sunMin := 0.0
pressureHpa := 0.0
uvIndex := 0.0
if i < len(dayForecasts) {
if dayForecasts[i].CloudCoverPct != nil {
cloudPct = *dayForecasts[i].CloudCoverPct
@@ -132,6 +133,9 @@ func BuildDashboard(req ComputeRequest) (DashboardData, error) {
if dayForecasts[i].PressureHpa != nil {
pressureHpa = *dayForecasts[i].PressureHpa
}
if dayForecasts[i].UVIndex != nil {
uvIndex = *dayForecasts[i].UVIndex
}
}
budgets, worstStatus, worstMode := computeRoomBudgets(req, h.Hour, h.TempC, cloudPct, sunMin, toggles)
@@ -144,6 +148,7 @@ func BuildDashboard(req ComputeRequest) (DashboardData, error) {
TempC: h.TempC,
HumidityPct: h.HumidityPct,
PressureHpa: pressureHpa,
UVIndex: uvIndex,
RiskLevel: dayRisk.Level.String(),
BudgetStatus: worstStatus.String(),
IndoorTempC: indoorTempC,

View File

@@ -30,6 +30,17 @@ func makeForecasts(baseTime time.Time, temps []float64) []Forecast {
return forecasts
}
func makeForecastsWithUVI(baseTime time.Time, temps []float64, uvis []float64) []Forecast {
forecasts := makeForecasts(baseTime, temps)
for i := range forecasts {
if i < len(uvis) {
uv := uvis[i]
forecasts[i].UVIndex = &uv
}
}
return forecasts
}
func TestBuildDashboard_NoForecasts(t *testing.T) {
req := ComputeRequest{
Profile: Profile{Name: "Test", Timezone: "Europe/Berlin"},
@@ -850,3 +861,40 @@ func TestBuildDashboard_MultipleRooms(t *testing.T) {
t.Errorf("got %d room budgets, want 2", len(data.RoomBudgets))
}
}
func TestBuildDashboard_UVIndex(t *testing.T) {
loc, _ := time.LoadLocation("UTC")
base := time.Date(2025, 7, 15, 0, 0, 0, 0, loc)
temps := make([]float64, 24)
uvis := make([]float64, 24)
for i := range temps {
temps[i] = 30
if i >= 10 && i <= 14 {
uvis[i] = 8.5
}
}
req := ComputeRequest{
Profile: Profile{Name: "Test", Timezone: "UTC"},
Forecasts: makeForecastsWithUVI(base, temps, uvis),
Toggles: map[string]bool{},
Date: "2025-07-15",
}
data, err := BuildDashboard(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(data.Timeline) != 24 {
t.Fatalf("got %d timeline slots, want 24", len(data.Timeline))
}
// Hour 12 should have UVI 8.5
if data.Timeline[12].UVIndex != 8.5 {
t.Errorf("Timeline[12].UVIndex = %v, want 8.5", data.Timeline[12].UVIndex)
}
// Hour 0 should have UVI 0
if data.Timeline[0].UVIndex != 0 {
t.Errorf("Timeline[0].UVIndex = %v, want 0", data.Timeline[0].UVIndex)
}
}

View File

@@ -99,6 +99,7 @@ type Forecast struct {
SunshineMin *float64 `json:"sunshineMin"`
ApparentTempC *float64 `json:"apparentTempC"`
PressureHpa *float64 `json:"pressureHpa"`
UVIndex *float64 `json:"uvIndex"`
}
// Warning holds a weather warning sent from the client.
@@ -172,6 +173,7 @@ type TimelineSlotData struct {
TempC float64 `json:"tempC"`
HumidityPct float64 `json:"humidityPct"`
PressureHpa float64 `json:"pressureHpa"`
UVIndex float64 `json:"uvIndex"`
RiskLevel string `json:"riskLevel"`
BudgetStatus string `json:"budgetStatus"`
IndoorTempC float64 `json:"indoorTempC"`